Carving a monolith into services safely
Breaking a monolith into microservices trades deploy and scaling independence for distributed-systems pain — the trade-off is where you cut, how you migrate, and when not to.
Why split at all — and the cost
A monolith slows teams as it grows: every change redeploys the whole thing, one hot path forces you to scale the entire app, and a bug anywhere can take down everything. Microservices promise independent deploy, independent scaling, and team autonomy — at the price of network calls, partial failure, distributed transactions, and operational sprawl. So the real questions aren’t “monolith or microservices” but where to cut, how to migrate without a rewrite, and when the split isn’t worth it.
Find the seams
Cut along business capabilities / bounded contexts, not technical layers. “Payments,” “search,” “notifications” are good services — each owns a cohesive slice of the domain. “The database layer” or “the controllers” are bad services — they cut across every feature and force chatty cross-talk. The test is high cohesion, low coupling: a good boundary is one where most changes stay inside it and the interface to the rest is narrow and stable. Look for the natural fault lines — different rates of change, different teams, different scaling needs.
The strangler fig
Never big-bang rewrite. Use the strangler fig: stand up new services beside the monolith and incrementally route functionality to them behind a façade or API gateway, leaving everything else untouched, until the old code is “strangled” and can be removed:
┌──────────── gateway / façade ────────────┐
client ───> │ /payments -> new Payments service │
│ /search -> new Search service │
│ /* -> legacy monolith (for now) │
└───────────────────────────────────────────┘
Each route moves one capability at a time, shippable and reversible, so you’re never betting the company on a flag day.
The database is the hard part
Two services sharing one database is a distributed monolith — they’re coupled through the schema and can’t deploy or scale independently. Real decomposition means splitting the data so each service owns its store, which is genuinely hard:
- A cross-service join becomes an API call or denormalized copy.
- A foreign key across the boundary disappears; integrity moves to application logic and eventual reconciliation.
- During the transition you dual-run — keep the data in sync with CDC or an outbox (see the change-data-capture lesson) while both old and new paths read it.
- A transaction that spanned two now-separate tables becomes a saga with compensating actions, or you redraw the boundary so it doesn’t have to span.
If you can’t cleanly separate the data, you probably haven’t found the right seam.
When not to split (or how far)
Over-splitting is its own failure. Symptoms: services that always deploy together (you found a fake boundary), a flood of synchronous calls to serve one request (chatty, fragile, slow), and a saga where a single local transaction would have done. Sometimes the answer is a modular monolith — clean internal boundaries, one deployable — or extracting just the one capability that needs independent scaling, and leaving the rest.
Where it shows up
Amazon’s and Netflix’s monolith-to-services migrations; the strangler pattern behind most “we modernized our platform” stories; Domain-Driven Design’s bounded contexts as the sizing tool.
The interview cue
When asked to break up a monolith, lead with discipline, not enthusiasm: “I’d cut along bounded contexts, not layers, and migrate with the strangler-fig pattern — route one capability at a time behind a gateway, never a big-bang rewrite. The hard part is splitting the database: I’d give each service its own store, replace cross-service joins with APIs, and dual-run with CDC during the transition. And I’d resist over-splitting — services that must deploy together are a distributed monolith.” Naming the seams, the strangler migration, and the database problem is the trade-off interviewers want.