I recently started moving a large application from traditional Spring to Spring Boot. We are upgrading from Spring 5 and Java 17 to Spring Boot 3 and Java 27, while keeping the existing industry and regulatory interfaces. After more than ten years of working on the system, the upgrade brought me back to a question we had considered when we designed it: would we eventually regret keeping so much functionality in one application?
The system has grown considerably since then. We kept it as a monolith, with code organized by module and shared services underneath. A framework upgrade reaches across those modules, so this was a practical opportunity to look at the consequences of that decision. Had keeping everything together made the application harder to change, or were the connections still useful?
To explain why we made that choice, it helps to understand who uses the system. There are two sides. On the industry side, people working for regulated facilities submit information and manage their environmental reporting. On the regulatory side, state and federal staff review that information, track compliance, and prepare for inspections. They have different responsibilities, but their work revolves around the same facilities.
The industry UI uses facility-based access, while regulators work through a separate regulatory UI and security boundary. Both sit inside one Java/Spring application, packaged as a single WAR. The modules share services and database access, and users can move between them as their work requires.
Within each side, a person’s work often spans several modules. That was an important reason to keep the application integrated.
Take the industry side first. Someone responsible for a facility’s environmental compliance might use myRCRAid to submit a Site Notification Record in the Handler module. The same person might also need to submit notices for international waste shipments. We deployed myRCRAid as part of the industry application in December 2016. When we added WIETS, the Waste Import Export Tracking System, in January 2022, that person could handle the additional work using the same account and facility records.
For users who already held the Site Manager role—the role for managing a facility’s access and data—the shared permission model extended access to WIETS without another permission request. They could just start using the new feature in the system they already knew.
On the regulatory side, consider someone preparing to inspect a facility. They may start with its inspection and enforcement history, but they also need a broader picture. Has the facility met its biennial reporting obligations? What has it submitted? What manifests is it receiving? From the same site record, the regulator can run reports and follow links into submissions and manifests across those modules. Preparing for one inspection requires information from several parts of the application.
That is what we were trying to preserve when we kept the system together. The modules represented different functions, but the users needed to move between them as part of their work. The facility records, permissions, and services were already there for the modules to use.
The remaining question was whether that choice would become a burden for the people maintaining it. The Spring Boot upgrade put that to the test.
Hibernate, the library we use for database access, had removed its older query API. The initial migration brief identified eight direct calls. Once we looked more closely, we found roughly a thousand uses of the older query mechanism across more than two hundred files. Rewriting all of those queries at once would have made the upgrade much bigger, with a lot more application behavior to check.
Because the modules used a shared persistence layer, we could introduce a translation layer there. The existing code kept expressing its queries in the familiar form, with mostly import changes at the callers. The shared layer translated those queries into the modern API underneath.
We still had to solve the compatibility problem, but we could concentrate that work in a common layer. We could also run the application as a whole and test the affected workflows together. We did not have to coordinate separate applications adopting and deploying their own versions of the change. A dependency used throughout the system created a large migration task, and the shared structure gave us a way to contain it.
The translation layer is temporary. We plan to update the code using the old query API module by module, then remove the translation layer. That lets us separate the platform upgrade from rewriting every query, with smaller changes to verify along the way.
Of course, keeping everything together comes with tradeoffs. A shared dependency change can affect several modules, so we have to test beyond the area we changed. We still need clear boundaries in the code and need to retire old infrastructure when it has done its job. Keeping one application does not mean keeping everything in it forever.
But the upgrade has given me more confidence in the original decision. The same shared structure that made the system useful to its users also helped us upgrade it. We could handle the main compatibility work in a shared layer and test the workflows together. The system grew, but we retained a way to work on it as a whole.
Separate services make sense when team ownership, scaling, or independent releases justify them. For this application, keeping it together has worked well for the people using it and for us maintaining it. Ten years and several upgrades later, I still see a reason for them to stay together.