How to decompose a monolith without stopping delivery — where service boundaries come from, why the database is the hard part, and what the work costs.
Disclosure. legacymodernizationhub.com is operated by Chudovo OÜ. Timeline figures in this guide are the ranges Chudovo publishes from its own delivered projects. They are one vendor’s reference points, not industry averages. Patterns and technical guidance cited inline come from Martin Fowler, AWS, Microsoft and Chris Richardson.
Start with whether you should
Most writing on this subject assumes the decision is made. It usually should not be.
Microservices trade one problem — a codebase that is hard to change — for another: a distributed system that is hard to operate. That trade pays off under specific conditions, and when those conditions are absent the migration produces all of the cost and none of the benefit.
The decomposition is likely wrong if:
- The application is small or stable. Each service needs its own pipeline, monitoring and data store. Below a certain size the operational overhead exceeds the delivery gain
- The product is early and the domain model is still moving. Service boundaries drawn around a domain that has not settled will be wrong, and boundaries are the most expensive thing to change after deployment
- One team maintains everything. A small team running a dozen services spends its time on infrastructure and inter-service coordination rather than on features
- The load is uniform and predictable. Independent scaling is the headline benefit of microservices. If nothing needs to scale independently, you are paying for a capability you will not use
A monolith that is modular, tested and deployable in minutes is a better system than a distributed one that is none of those things. If the real problem is a tangled codebase, modularizing the monolith is cheaper and often sufficient.
Signs the monolith has actually become the constraint
The case for decomposition is operational, not aesthetic. These are the indicators that justify it:
Deployments are slow and risky. A one-line fix in billing goes through the same build, test and release cycle as a major feature. Teams respond by deploying less often, which makes each release larger and riskier — a loop that tightens on itself.
Parts of the codebase cannot be safely changed. Modules central to early development have accumulated dependencies nobody fully maps. Developers route around them. Work that should take a day is quoted in weeks because the blast radius is unknown.
Scaling is all-or-nothing. One component needs more capacity; you scale the entire application, paying for idle capacity everywhere else.
Teams block each other. Multiple teams in one release train, coordinating merges and waiting on each other’s testing. Delivery slows as headcount grows.
The technology stack is pinned. The framework version is end-of-life, but upgrading means upgrading everything at once, so it never happens.
If fewer than two of these are true, the monolith is not your bottleneck.
Where service boundaries come from
This is the decision that determines whether the migration succeeds. Everything else is execution.
Boundaries follow business domains, not technical layers. The instinct to extract “the data access layer” or “the authentication module” produces services that are chatty, coupled and unable to deploy independently — a distributed monolith, which is worse than what you started with.
The usable method is domain-driven: identify bounded contexts in the existing system — areas where the same term means a consistent thing and where data has a clear owner. Order in the fulfilment context and Order in the billing context are different concepts with different lifecycles; that seam is a service boundary.
Two practical tests for a proposed boundary:
- Data ownership. Can this service be the only writer to its data? If two services must write the same table, the boundary is wrong.
- Deployment independence. Can it ship without coordinating a release with anything else? If not, it is a module, not a service.
The order in which seams are cut is driven by an audit of the existing system — coupling, data ownership, integration dependencies — rather than by which part of the code is most annoying to work in.
The Strangler Fig pattern
Big-bang rewrites of production systems fail at a rate that makes them indefensible when an incremental path exists. The incremental path is Martin Fowler’s Strangler Fig pattern: new services grow around the monolith and take over its responsibilities piece by piece until the original can be retired. AWS documents the same approach in its guidance on decomposing monoliths.
The mechanism is a routing layer in front of the system. Each extracted capability is registered with the router; traffic shifts to the new service gradually, and rolls back instantly if it misbehaves.
What makes this work is the facade, not the services. It gives you per-capability traffic control, so a new service can be validated against real production load at 1% before it carries 100%, and rolled back in seconds rather than through a release.
What makes it fail is leaving the facade in place forever. Teams extract three services, lose momentum, and run a permanent hybrid — two deployment models, two operational models, double the cost. Decide at the outset which capabilities leave the monolith and what happens to the remainder.
The database is the hard part
Service extraction is mostly tractable. Data extraction is where projects stall, because the monolith’s database is usually a single schema with foreign keys across every domain, and those keys encode business rules nobody wrote down.
The target is one database per service — each service the sole writer to its own data. Getting there is a staged process, and the middle stage is where teams spend most of the time.
Stage 2 deserves specific attention. While a service owns its data but other components still read the old schema, both copies must stay consistent. The naive implementation — write to both — fails the first time one write succeeds and the other does not, and it fails silently. The durable approach is a single authoritative write followed by an event that updates the other side, so there is always one source of truth.
Stage 2 is also where migrations become permanent by accident. Nobody decides to stay; the next service is more urgent, and three years later the old schema is still there.
Distributed transactions: what replaces the rollback
In a monolith, a multi-step operation spanning orders, inventory and payment is one database transaction that either commits or rolls back. Split across services, that guarantee is gone.
The replacement is the Saga pattern: a sequence of local transactions, each publishing an event that triggers the next, and each with a defined compensating action for failure. Payment fails after inventory was reserved, so a compensating transaction releases the reservation.
Two consequences teams underestimate:
- Compensation is business logic, not plumbing. “Refund the payment” is not always the correct reversal, and someone has to decide what is
- Intermediate states become visible. An order can be briefly paid-but-unconfirmed. The UI and support processes need to handle states that could not previously exist
If a core workflow needs genuine atomicity across what you are planning to split, that is evidence the boundary is wrong.
Which service to extract first
The first extraction is where you discover what the audit missed, so pick for learning rather than for value.
Good first candidates: clear data ownership, few dependencies, meaningful but not critical traffic, easy to validate correctness.
Poor first candidates: anything touching payments, anything central enough that failure is an incident, anything whose data is referenced everywhere — typically the user or customer entity.
The characteristic mistake is starting with the hardest part because it is the most painful. The first service should prove the pipeline, the routing, the observability and the rollback path. Those capabilities are what make the next ten extractions routine.
Work in phases that each end with something deployed. A phase that ends with work in progress does not reduce risk.
Timeline and cost
Published ranges from Chudovo, which delivers monolith to microservices migration projects on the .NET and Java stacks:
| Scope | Duration |
|---|---|
| Extracting 2–3 services from a mid-size monolith | 3–6 months |
| Full decomposition of a large enterprise monolith | 12–24 months |
| Monolith-to-microservices rebuild, mid-sized, as part of a cloud migration | 3–9 months, around $100,000 |
Two things worth reading off those numbers. First, the gap between partial and full decomposition is roughly fourfold — which is why “extract the two services that are actually hurting” is often the right scope, and full decomposition rarely is. Second, these are elapsed times for teams doing this alongside feature delivery, which is the normal condition; a figure that assumes a frozen roadmap is not describing your project.
Where these migrations fail
Boundaries drawn around technical layers. Produces services that cannot deploy independently — a distributed monolith, with the operational cost of microservices and none of the delivery benefit.
The database was treated as a later phase. Services were extracted while all of them kept writing to the shared schema. Nothing was actually decoupled.
The transition stage became permanent. Two architectures, two operating models, indefinitely.
Observability came last. Debugging a distributed system without distributed tracing is qualitatively harder than debugging a monolith with a stack trace. Tracing, correlation IDs and centralized logging belong in the first phase, not the fifth.
Organizational structure unchanged. Independent deployment requires teams that own services end to end. Keep a central release train and you keep the coordination cost you were trying to remove.
No retirement plan for the monolith. Without a defined end state, extraction stops when enthusiasm does.
Measuring whether it worked
The architecture is not the outcome. These are:
- Deployment frequency per service — rising, independently
- Lead time from merge to production — falling
- Change failure rate — not rising; a spike means boundaries or testing are wrong
- Mean time to recovery — should improve as blast radius shrinks; if it worsens, observability is behind
- Cross-team coordination per release — the thing you were buying
If deployment frequency has not improved six months in, the migration is not delivering its central benefit, and adding services will not fix that.
FAQ
How long does a monolith-to-microservices migration take? Three to six months to extract two or three services from a mid-size monolith. Twelve to twenty-four months for full decomposition of a large enterprise monolith, depending on the number of services.
Should every monolith be decomposed? No. Small applications, stable domains, single teams and uniform predictable load are all cases where the operational overhead exceeds the benefit.
What is the most common reason these projects fail? Service boundaries drawn around technical layers instead of business domains. The result is a distributed monolith that is harder to operate than the original.
Can we keep one shared database? Temporarily, during transition. Permanently, no — if services share a database they cannot deploy or scale independently, which removes the point of the exercise.
How do we handle transactions that span services? With the Saga pattern: local transactions chained by events, each with a compensating action. Compensation logic is a business decision, not an implementation detail.
Which service should we extract first? One with clear data ownership and few dependencies, carrying real but non-critical traffic. The first extraction exists to prove the pipeline and the rollback path.