Monolith to Microservices: A Practical Migration Guide
Migrating from a monolith to microservices is one of the highest-stakes architectural shifts a team can undertake. Done well, it unlocks independent deployability, team autonomy, and targeted scaling. Done poorly, it produces a distributed monolith that is harder to operate than what you started with. This guide walks through the strategies, patterns, and pitfalls that separate the two outcomes.
Why Migrate at All?#
Before writing a single line of migration code, confirm you actually need microservices. Valid reasons include:
- Independent deployment — different parts of the system ship on different cadences.
- Team scaling — Conway's Law friction means a single codebase bottlenecks multiple teams.
- Targeted scaling — one hot path needs 10x the compute while the rest idles.
- Technology heterogeneity — a subsystem would benefit from a different language or data store.
If none of these apply, a well-modularized monolith is almost always simpler to operate.
Phase 0: Understand What You Have#
Map the Domain#
Use event storming or domain storytelling to surface bounded contexts hidden inside the monolith. Gather engineers, product managers, and domain experts in the same room and walk through real user journeys.
┌─────────────────────────────────────────────────┐
│ Monolith │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Orders │ │ Inventory│ │ Payments │ │
│ │ │──│ │──│ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Users │ │ Shipping │ │
│ │ │──│ │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
Identify Coupling#
Static analysis tools (e.g., Structure101, Lattix) can produce dependency graphs. Look for:
- Circular dependencies between modules.
- God classes that touch every domain.
- Shared mutable state — tables read and written by multiple modules.
Document every cross-module database join. These are the hardest things to untangle.
Phase 1: The Strangler Fig Pattern#
The strangler fig is the safest migration strategy. Rather than rewriting the monolith, you grow new services around it, gradually routing traffic away from the old code until nothing remains.
Step-by-Step Execution#
- Insert a routing layer — an API gateway or reverse proxy sits in front of the monolith.
- Pick a bounded context — start with one that has low coupling and high business value.
- Build the new service — implement the extracted domain in a standalone service.
- Dual-write or shadow traffic — run the new service in parallel, comparing outputs.
- Cut over — route production traffic to the new service.
- Delete dead code — remove the old implementation from the monolith.
Client
│
▼
┌──────────┐
│ API GW / │
│ Proxy │
└────┬─────┘
│
├──── /orders ────► New Orders Service
│
└──── /* ─────────► Monolith (everything else)
Choosing What to Extract First#
Score each bounded context on three axes:
| Factor | Weight |
|---|---|
| Business value of independent deployment | High |
| Coupling to other modules (lower is better) | High |
| Data isolation (own tables vs. shared) | Medium |
Extract the context with the best composite score. Early wins build organizational momentum.
Phase 2: Domain Decomposition#
Defining Service Boundaries#
A service should own a single bounded context. If two concepts always change together, they belong in the same service. If they change independently, they are candidates for separation.
Use these heuristics:
- Single Responsibility — can you describe the service in one sentence without "and"?
- Data Ownership — does the service own its data, or does it need another service's tables?
- Team Alignment — can one team own the service end-to-end?
Anti-Corruption Layers#
When the new service must still communicate with the monolith, introduce an anti-corruption layer (ACL). The ACL translates between the monolith's internal model and the service's domain model, preventing legacy concepts from leaking into new code.
New Service ──► ACL ──► Monolith API
│
Translates legacy
DTOs to domain objects
Phase 3: Database Splitting#
Shared databases are the number-one reason microservice migrations stall. Two services reading from the same table are coupled at the data level regardless of how cleanly you separated the code.
Migration Strategies#
1. Database-per-service from day one
The new service gets its own database. Data is synchronized via change data capture (CDC) using Debezium or similar tools.
2. Schema separation first
Move the extracted service's tables into a separate schema within the same database instance. This gives logical isolation without the operational overhead of a second database.
3. View-based decoupling
Create database views that present a stable interface. The monolith queries the view while the underlying tables migrate to the new service's database.
Handling Cross-Service Queries#
Joins across service boundaries become API calls or event-driven projections.
-- Before: single SQL join
SELECT o.id, u.name
FROM orders o JOIN users u ON o.user_id = u.id;
-- After: two calls + application-level join
GET /orders/123 → { userId: 42 }
GET /users/42 → { name: "Alice" }
For read-heavy aggregation, maintain a denormalized read model updated via events.
Phase 4: Shared Library Extraction#
Monoliths accumulate utility code — logging, auth middleware, serialization helpers — that every module depends on. Naively copying this into each service creates drift; publishing it as a shared library creates coupling.
Guidelines#
- Thin libraries only — shared code should be infrastructure-level (HTTP clients, logging, tracing). Never share domain logic.
- Semantic versioning — services pin to a major version and upgrade on their own schedule.
- Inner-source model — any team can contribute, but a core team reviews and releases.
Avoid a "platform SDK" that grows to contain business logic. That path leads back to a distributed monolith.
Phase 5: Team Restructuring#
Microservices without team realignment is just distributed code. Follow the inverse Conway maneuver: design the team structure you want, then let the architecture follow.
Stream-Aligned Teams#
Each team owns one or more services end-to-end — from the API contract to the database schema to the CI/CD pipeline. They deploy independently and carry their own on-call rotation.
Platform Teams#
A platform team provides self-service infrastructure: container orchestration, observability, CI/CD templates, and service mesh configuration. They reduce cognitive load on stream-aligned teams.
Enabling Teams#
Temporarily embedded specialists (e.g., a database expert helping with a particularly gnarly split) accelerate stream-aligned teams without creating long-term dependencies.
Measuring Migration Success#
Track these metrics throughout the migration:
| Metric | Target |
|---|---|
| Deployment frequency per service | Increasing over time |
| Lead time for changes | Decreasing |
| Change failure rate | Stable or decreasing |
| Mean time to recovery | Decreasing |
| Cross-service incident rate | Low and stable |
| Developer satisfaction (survey) | Improving |
Anti-Patterns to Watch For#
- Distributed monolith — every deploy requires coordinating multiple services.
- Shared database creep — new tables keep appearing in the old shared database.
- Service sprawl — more services than the organization can operate.
- Synchronous call chains — service A calls B calls C calls D, creating latency and fragility.
Communication Patterns#
Synchronous (REST / gRPC)#
Best for queries where the caller needs an immediate response. Keep call chains shallow — two hops maximum.
Asynchronous (Events / Messages)#
Best for commands and notifications. Use an event broker (Kafka, RabbitMQ, NATS) to decouple producers from consumers.
Orders Service ──publish──► [OrderPlaced event] ──► Inventory Service
──► Shipping Service
──► Analytics Service
Saga Pattern#
For transactions spanning multiple services, use choreography-based or orchestration-based sagas with compensating actions for rollback.
Migration Timeline Expectations#
Realistic timelines for a mid-sized monolith (500k–2M lines of code):
- Phase 0 (domain mapping): 2–4 weeks
- Phase 1 (first service extraction): 1–3 months
- Phase 2–3 (subsequent extractions): 2–6 months each
- Full migration: 1–3 years
Patience matters more than speed. Each extraction teaches the team something that makes the next one faster.
Key Takeaways#
- Validate that microservices solve a real problem before migrating.
- Use the strangler fig pattern — never attempt a big-bang rewrite.
- Domain decomposition comes before code decomposition.
- Database splitting is the hardest part; plan for it early.
- Keep shared libraries thin and infrastructure-only.
- Restructure teams to match the target architecture.
- Measure DORA metrics and developer satisfaction throughout.
If you found this guide helpful, explore the rest of our engineering blog — we have published 369 articles and counting on software architecture, DevOps, and engineering best practices. Browse all articles to keep leveling up.
Try it on Codelit
GitHub Integration
Paste any repo URL to generate an interactive architecture diagram from real code
Related articles
Build this architecture
Generate an interactive architecture for Monolith to Microservices in seconds.
Try it in Codelit →
Comments