back to writing
architecture

Migrating a monolith database in 6 months.

2025 · 10 · 6 min read

A look at how a Java service from 2009 ends up migrated into a modern platform of microservices — and how was that done without AI

Every company has one. A service nobody fully understands anymore, running on a Java version that predates half the current engineering team. It handles something critical — payments, auth, the core domain model — and it has never been down for more than a couple of minutes in 11 years.

Where it started

Companies around the globe reach a moment in their life where they have to reduce their expenses in order to stay profitable. We have all seen it. For us that meant a database migration of our dear monolith. Given the rest of our platform was already running in a modern microservices architecture, on top of everything already connected to the desired database, we realized that this could be the moment we all avoided up until now, the refactor.

We rolled up our sleeves, mobilized a couple of very incredibly talented teams and we split the work brotherly.

How we did it

In the world of refactoring, you have to first understand what you are going to refactor to facilitate your decision on what is the best option you have.

The hand we were dealt

We started with doing reconnaissance. Our first go-to was active stack trace analysis that consisted of static code analysis, log monitoring, api trace calls. These steps were essential in understanding how, when, where and what is really being used.

Other techniques that helped us glue together bits of the bigger picture were indentifying business flows using custom logs, tracing database crud operation, exploratory testing within the old platform to discover edge cases. Once we had a clearer picture of how the system behaves and what is really used from each module we started designing ways of refactoring/migrating the product.

Given products similarity and the already existing infrastructure, we opted for integrating the monolith's submodules within the corresponding microservice. This is what our solution would look like:

One of the biggest challenges we faced was that the data model was in the same ballpark, but not quite there. We had an overlap of 70% of the data model which excluded completely the option of keeping both of them. Therefore, we went on with consolidating the data models by adding the missing pieces where needed and adapting the incomplete ones within the existing platform.

With that out of the way, now we could focus on data migration. As the old platform is still in use at this point, this was a 2 step procedure:

  • migrate old static data from B to A
  • synchronize incoming data from B to A

Summary

Six months. A monstrous monolith. A tightly coupled database. Given those constraints, the team had to strike a deliberate balance between efficiency and best practices — there was no room for purism.

We didn't cut corners everywhere. Codebase and database analysis got the attention they deserved — understanding what we were working with before touching anything was non-negotiable.

During component integration, we made a conscious decision to copy-paste into the existing microservice rather than refactor cleanly. This wasn't laziness — it was risk management. A cleaner refactor would have introduced more unknowns, demanded heavier testing, and eaten into the timeline. Preserving the original logic meant fewer bugs and less time lost chasing regressions. We also chose to integrate into an existing service rather than spin up a new one — infrastructure was already in place, ACLs and cluster network policies already resolved. Starting fresh would have meant revisiting all of that, a known source of painful, hard-to-predict delays.

The goal was met. In six months, the platform moved from a monolith to a microservice architecture pointing at the right database. A parallel refactor and bug fixes continued post-deadline, but the hard part was done.

Retrospective

Next time, I'd spend more time upfront extracting hardcoded values and static configuration into a dedicated file or project. Chasing down why a flow behaves differently in production because some value is buried somewhere in the code is exactly as painful as it sounds.

On that note — static config loaded from the database at startup is a bad idea by design. Hard to debug, harder to change in production. Just don't.

Looking back, I'd avoid tackling a database migration and a full refactor simultaneously in six months. If the goal is the migration, do the migration — the refactor can wait. We overdelivered and it worked out, which makes it a good story. But that same bet in a less forgiving situation would not have been a good story. Overachieving is great until it isn't.

When NOT to do it

From my experience, there are situations where you should just leave the monolith alone. If the codebase is coupled with stored procedures and triggers, you're not refactoring code — you're doing archaeological fieldwork. Same goes for static configuration loaded from the database at startup; it's painful to debug, dangerous to change in production, and it will cost you way more time than it should. And if the team doesn't understand the domain — not the code, the why behind it — every decision is an educated guess at best.

Amazon Prime Video did the whole journey in reverse and were kind enough to write about it. They had a distributed microservices architecture for their video monitoring system and collapsed it back into a monolith — cutting costs by 90% in the process. Turns out orchestrating a fleet of services to talk to each other was more expensive than the problem microservices were supposed to fix in the first place.

The takeaway isn't that microservices are bad — it's that they come with a price tag that only makes sense at the right scale and for the right problem. If your services spend more time talking to each other than doing actual work, you might just be paying a lot of money for a distributed monolith with extra steps.