Every monolith-to-microservices migration I’ve been part of started the same way — somebody at the top said “we need microservices” and the engineering team started drawing boxes. The ones that succeeded had a few things in common, and the ones that collapsed had a different set of things in common. This article is about both.
The question that should come first
Not “how do we split this?” but “what does splitting actually buy us?“. If you can’t answer that in one sentence, you’re not ready. Valid answers:
- “Three teams keep stepping on each other’s deploys, we need independent release cadence.”
- “One part of the system needs to scale 10× independently.”
- “This subsystem needs a completely different tech stack.”
Invalid answers: “microservices are best practice”, “our architect said so”, “we want to be like Netflix”.
Step 0 — make the monolith modular first
Most teams skip this step. They shouldn’t. A well-modularized monolith is the best starting point for extraction — boundaries are already drawn, dependencies are already documented, and you can extract one service at a time without chaos.
Practical indicators that the monolith is ready:
- Each business domain lives in its own package/module
- Cross-module calls go through explicit facades, not internal classes
- There’s a tool (ArchUnit, Nx, jdepend) enforcing boundaries in CI
- Each module has its own tests and they run independently
If the monolith is a spaghetti ball, do not start extracting services. You’ll extract the spaghetti into seven network-connected spaghetti balls.
Step 1 — pick the first service carefully
The first extraction sets the pattern. Pick something that:
- Has clear bounded context — a domain that doesn’t leak into everything
- Has team alignment — one team owns it end-to-end
- Is not critical path for everything — if it breaks, 90% of the product still works
- Has real value to extract — independent deploy, different scaling, different stack
Good first candidates: notifications, search, analytics, reporting, authentication. Bad first candidates: “orders” or “users” — they touch everything.
Step 2 — strangler fig, not big-bang
Never do a complete rewrite in parallel with the monolith still handling production. The strangler fig pattern (coined by Martin Fowler) says: extract one capability, route traffic to it, leave the monolith alone for everything else. Over time the new services surround and choke the old code.
Concretely:
- Stand up the new service with empty endpoints
- Add a feature flag: 0% traffic uses the new path
- Dark-launch — the new service is called in parallel but its response is discarded
- Compare results for divergence
- Gradually shift percentage: 1% → 10% → 50% → 100%
- Remove the old code
A migration done this way is reversible at every step. A big-bang migration is not.
Step 3 — data is the hard part
You can split services along business boundaries in a week. Splitting data takes months. Three approaches, in order of preference:
Copy via events. The old monolith publishes events about the data domain; the new service subscribes and builds its own database. Eventual consistency is the price.
Sync RPC. The new service calls the monolith whenever it needs data it doesn’t own. Tightly coupled but simple. Treat as a stepping stone.
Shared database. Tempting, always a trap. Every team I’ve seen go this route ended up with a distributed monolith that was worse than the original.
What will go wrong
Even done carefully, expect these:
Latency creeps up. Every in-process call that becomes a network call adds 1-5 ms and a failure mode. Without disciplined timeouts and circuit breakers, you’ll see cascading failures the old monolith never had.
Observability lags behind the architecture. You can’t debug five services the way you debugged one. If distributed tracing isn’t in place before the first extraction, debugging will be painful for months.
Team topology doesn’t match service topology. Two teams sharing a service re-creates the monolith’s coordination problems. Conway’s Law enforces itself whether you plan for it or not.
Testing becomes harder. Integration tests across services need real infrastructure (Testcontainers saves you here). End-to-end tests get slow and flaky — use them sparingly.
How long does it take?
In my experience: one quarter per service for the first three services. After that, the team builds muscle memory and it speeds up. A full monolith-to-microservices migration of a medium-sized system (30-50 engineers) typically spans 18-36 months.
If someone promises you 6 months, they either have a tiny codebase or they’re about to deliver a disaster.
Checklist before you start
- Clear business reason for the migration, one sentence
- Monolith is already modularized (or you’ll do that first)
- Observability stack in place: logs, metrics, distributed tracing
- CI/CD can deploy services independently
- First candidate service has a single team owner
- Feature flags available for traffic shifting
- Team has capacity — migration is not a side project
The boring truth
The successful migrations I’ve been part of all felt boring. Slow, methodical, unglamorous. The failed ones were full of big redesigns, parallel rewrites, and sprint goals that always slipped. Microservices are a long game — treat them like one.