Moving from a legacy monolith to services without downtime
10 May 2024 · 8 min read · SmartCG
A practical account of splitting a legacy monolith for a logistics company without a single maintenance window.
The legacy monolith problem
As a business grows, a monolith starts to crack. A deployment takes hours instead of minutes, one small mistake takes the whole system down, and onboarding a new developer turns into an archaeology project. The codebase is not the problem — the coupling is. Every module knows too much about its neighbours, so no change can be made safely in isolation.
The Strangler Fig pattern
We used the Strangler Fig pattern. The idea is simple: put an API gateway (Nginx or Kong) in front of the old monolith. Then take a single module — billing, for example — rewrite it from scratch as an independent service, and move its traffic through the gateway. The old code keeps serving everything that has not been extracted yet, and the new service takes over one domain at a time.
Migration stages:
- Set up the API gateway and route all traffic through it, so the monolith already sits behind a seam.
- Extract the "user management" domain into a separate service (NestJS) with its own release cadence.
- Synchronise data between the old and new stores using CDC (Change Data Capture via Debezium).
- Gradually switch off the old code path once the new service has proven itself in production.
Synchronising the data
Data was the hardest part. We could not simply switch the old database off, so we set up replication in both directions: while the new service writes to PostgreSQL, the old monolith still reads those records from MySQL. That gave us a rollback path for every module and let the two systems run side by side for weeks instead of forcing a single cutover night.
What made the migration survivable
Three things mattered more than the technology choice. First, we extracted domains by business capability, not by technical layer — a "billing service" is a team-sized unit, a "repository service" is not. Second, every extraction had a feature flag and a rollback plan. Third, we kept the deployment pipeline identical for old and new code, so nobody had to learn a second release process in the middle of the migration.
Services involved
Read next
Docker multi-stage builds: cutting images by 90%
Getting rid of ENOSPC alerts and making deploys fast again with a proper Dockerfile.
BackendNestJS vs Express: why we choose a framework with architecture
The end of spaghetti backends: what dependency injection and a strict structure actually buy you.
BackendPostgreSQL vs MongoDB: choosing a database for a B2B product
Relational against NoSQL, decided on real scenarios instead of fashion.
Need to modernise a legacy system without a risky big rewrite?
We help identify what to extract first, where the production risk is already critical, and which phased route actually pays off for the business.