Monolith Decomposition Framework
Decomposition is a sequence of small, reversible extractions, each justified by a measurable benefit. The framework favors seams and data ownership over technical slicing, and it treats a modular monolith as a legitimate stopping point.
Decomposition is justified by outcomes, not architecture fashion. Define success before starting, for example reducing lead time for a given team by half, and stop extracting when the measured benefit no longer exceeds the operational cost of another service.
How it works
First, map the domain and dependencies: modules, shared tables, call graphs, and change history. Candidates with high change frequency and low coupling go first. A seam is introduced in the monolith, for example an interface with a single implementation. A proxy or gateway routes selected requests to the new service. Data moves by giving the service its own store, populated through change data capture or dual writes, and the monolith later reads through the service API.
Implementation steps
- Build a dependency and data-access map from code and database query logs.
- Define bounded contexts with domain experts and rank extraction candidates.
- Make the module boundary explicit inside the monolith and remove cross-module table access.
- Stand up platform basics: CI/CD, observability, service templates, and a gateway.
- Build the new service and run it in shadow mode, comparing outputs to the monolith.
- Shift traffic gradually using percentage-based routing.
- Cut data ownership over, then delete the old code and tables.
- Review delivery metrics and decide on the next extraction.
Keep the extraction team small and stable, and give it explicit time to delete old code. Without deletion, the migration only adds systems and the maintenance burden grows instead of shrinking.
Common pitfalls
- Extracting services that still share a database, preserving coupling.
- Chatty synchronous calls between new services, causing latency and cascading failures.
- Leaving dead code in the monolith after cutover.
- Splitting by layer such as "the data service" rather than by capability.
Metrics to watch
Lead time and deployment frequency of affected teams, change failure rate, number of cross-service calls per request, lines and tables remaining in the monolith, p99 latency on migrated flows, and incident count related to integration.
Share the numbers with leadership every quarter. A decomposition without visible improvement in delivery speed or reliability erodes sponsorship, and the program should adjust or stop rather than continue by inertia.
Example routing rule
routes:
- match: /api/invoices/*
destination:
- service: invoicing-service
weight: 25
- service: legacy-monolith
weight: 75
shadow:
service: invoicing-service
compare_responses: true
rollback: set weight of invoicing-service to 0
Key takeaways
- Choose boundaries from the domain, not from layers.
- Move data ownership with the code, or coupling remains.
- Shadow and shift gradually; keep rollback trivial.
- A well-structured modular monolith can be the right end state.