Splitting a 20+ Year Old Monolith
- Architecture
- Engineering Leadership
- Legacy Systems
- Spring Boot
The codebase had been running in production for over two decades. It worked, but we faced more and more problems.
Every new feature had to be woven into a framework that nobody outside the company had ever heard of — an in-house invention from a different era of software development. Onboarding new developers took months before they could ship anything meaningful. Teams coordinating across the monolith had to synchronise constantly, because a change in one corner could quietly break something in another. In the last years we had improved the system a lot. It wasn't failing, but it held us back.
The question wasn't whether to modernise. That decision was inadvisable. The question was how to do it without turning every ongoing project into a migration risk.
The trap we wanted to avoid
The "big rewrite" is appealing in theory. You draw a clean architectural diagram, you stand up new infrastructure, you declare a migration timeline. And then reality arrives: the old system keeps getting features added because the business can't pause, the new system is always six months from ready, and eventually you're maintaining two full codebases at once. Teams get split between the legacy world and the new one. Morale suffers. Timelines slip.
We'd seen this pattern before. We didn't want to repeat it.
Instead, we set a constraint: the modernisation work had to happen through normal project delivery, not alongside it. If a feature was being built anyway, that was the opportunity to build it on the new stack. The monolith would shrink incrementally — not because we scheduled a migration, but because we made the right choice the obvious choice each time a project started.
Making the new stack the easier path
This is where the approach got interesting. Choosing a technology is not the same as making it adoptable.
We moved to Spring Boot — a widely understood framework with a large ecosystem and, importantly, a far gentler learning curve than what we'd been working with. That part was deliberate. We didn't want to bet the company on a completely different stack we didn't understand yet, just because it looked modern on a conference slide. The change was already hard enough. Keeping the technology familiar let us reduce one category of risk while dealing with all the others.
But we didn't just hand teams a blank Spring Boot project and wish them luck. We built Spring Boot Starters for the patterns that would repeat across every new application: authentication, logging, shared configuration, common integrations. The infrastructure decisions were already made. Teams could focus on the actual business logic.
That initial investment was real. It took time to make the new stack the easier path. It was a team effort, and there were doubts along the way because, in the beginning, the old path was still the known path. We were effectively applying the Strangler Fig pattern: keep the existing system alive, grow the new capabilities around it, and make sure each new piece reduces the surface area of the monolith instead of creating a second system beside it.
This mattered more than it might sound. The productivity gap between the old and new stack was significant — not because Spring Boot is magic, but because developers could move in a framework they recognised, with community resources available, without navigating an undocumented internal abstraction layer. Features that would have taken weeks took days. That margin was what made it possible to absorb the migration overhead within project timelines. We weren't asking teams to do extra work — we were giving them back time.
The starters also enforced consistency without requiring it through policy. New services followed the same patterns not because someone reviewed every PR for compliance, but because the starter was the starting point. That kind of structural guardrail is worth more than a style guide.
At some point we crossed a turning point. Building things the new way was no longer the disciplined choice; it was simply the easier and more enjoyable one. Developer experience improved, turnaround times got shorter, and teams could move with less ceremony. Once that happened, the migration stopped feeling like a burden and started compounding on its own.
What was actually hard
The coordination problem didn't disappear — it just changed shape.
Splitting a monolith means drawing boundaries. And drawing boundaries in a 20-year-old system means confronting twenty years of accumulated coupling. Some of it was intentional — shared data models, deliberately reused business logic. Some of it was accidental — things that had grown together over time because it was easier than separating them. Untangling the two required a lot of careful decisions about what belonged where, and not all of those decisions were obvious.
There were also moments where the pressure to ship something on the new stack had to be balanced against the risk of building the boundary in the wrong place. A service boundary is much harder to move than a class boundary. We got some of them right on the first try. Others needed revisiting.
One coincidence helped us. We were already in the middle of a major technical renovation of the backend, which meant touching a lot of the system anyway. In that context, introducing proper abstractions and extracting clear services often wasn't much more work than doing the renovation in place inside the monolith. Once you have to open the walls, adding structure is cheaper than it looks from the outside.
And the old system kept running throughout. Which meant keeping it stable, handling bugs, and doing so without the attention it would have received if it had been the only thing people were working on. That's the hidden cost of incremental migration: the legacy system doesn't get to wind down — it just slowly gets smaller while still requiring care.
The one thing worth taking from this
If you're facing something similar, the most useful frame is probably this: the goal isn't to migrate — it's to make the new thing so much easier to work in that migration happens naturally as a byproduct of normal work.
That requires investing in the developer experience of the new stack early. Not documentation, not training sessions — actual tooling, starters, templates that remove friction. When developers can be productive faster on the new stack than the old one, the architectural migration mostly takes care of itself. It becomes the path of least resistance, which is the only sustainable kind of path.