Monolith to Microservices Migration: 6 Proven Strategies and a Practical Roadmap
Splitting a monolith is one problem; migrating safely to production is another. This article covers 6 battle-tested migration strategies — Strangler Fig, Event Interception, Database-First Split, Side-by-Side Shadow Mode, Feature-Flag Cutover, and Bulk Sync with Reconciliation — with step-by-step instructions and a practical roadmap. For architects planning a safe cutover. [Includes strategy comparison table →]
Bottom Line: Splitting and Migrating Are Two Different Problems
Many teams think “extracted from the monolith” equals “migrated.” They carve out a module, create a new project, deploy it, and call it done. But a true migration only ends when the old system is safely decommissioned.
The core question of splitting is “where does the boundary go.” The core question of migration is “how do I shift traffic without breaking the business.”
This article covers 6 production-validated migration strategies, ordered by risk (low to high), each with use cases, step-by-step instructions, and common pitfalls.
Strategy 1: Strangler Fig Pattern
Risk: ★★☆☆☆ | Recommendation: ★★★★☆
How it works
Intercept traffic at the API gateway or reverse proxy layer: new features go to the new service; old features continue hitting the monolith. Over time, the monolith is “strangled” out of existence.
Client → Gateway / Reverse Proxy
├── New feature → New microservice
└── Old feature → Monolith (to be removed)
Prerequisites
- The monolith already has a unified API gateway or can be fronted by a reverse proxy (Nginx / Envoy / APISIX)
- New and old features can be distinguished by URL path or domain
- The business cycle allows running old and new code in parallel
Step-by-step
- Configure routing rules at the gateway layer; new endpoints point to the new service
- Old endpoints in the monolith stop receiving new traffic but retain code for rollback
- After all endpoints in the monolith have been replaced, decommission it
- Clean up old routing rules from the gateway
Common pitfalls
- Route explosion: every extracted service adds a new route; N services produce an unmanageable mess of routing rules. Solution: use service discovery (Consul / Nacos) instead of hardcoded routes.
- Incomplete strangulation: utility functions called everywhere (date formatting, ID generation, validation) often remain in the monolith, preventing its decommission. Identify and extract shared libraries before migration.
Strategy 2: Event Interception
Risk: ★★★☆☆ | Recommendation: ★★★☆☆
How it works
If the monolith already uses a message queue or event bus, intercept at the event level: the new service subscribes to the monolith’s events, processes them, and publishes result events that the monolith consumes.
Event Source → Message Queue / Event Bus
├── Old consumer → Monolith (being phased out)
└── New consumer → Microservice (taking over)
Prerequisites
- The monolith already uses a message queue (RabbitMQ / Kafka / Redis Streams)
- Core business flows are event-driven (order state transitions, ticket processing)
- The system can tolerate eventual consistency
Step-by-step
- The new service subscribes to the existing event stream as an additional consumer
- After processing, the new service publishes a “processed” event
- Gradually degrade the monolith’s old consumer — stop writes first, then reads
- Verify data correctness from the new service, then remove the old consumer
Common pitfalls
- Double consumption: while both consumers run, the same event may be processed twice. Requires idempotent design or a deduplication mechanism.
- Event ordering: if the monolith depends on event order (create before update), the new service must preserve that order for the same entity — route events by entity ID to the same partition.
Strategy 3: Database-First Split
Risk: ★★★★☆ | Recommendation: ★★★☆☆
How it works
Move the database first, then the code. Isolate tables by business domain (schema split or instance split) while the API layer keeps running the original code against the new data source. After the data is stable, migrate the API layer.
Prerequisites
- The database is the biggest migration bottleneck (performance, table size, query coupling)
- Data domain boundaries are relatively clear
- A database proxy or middleware layer is available for read/write splitting
Step-by-step
- Identify which tables belong to each business domain
- Create a new database instance and sync target tables with 3-7 days of history
- Modify the data access layer in the monolith to point target table reads/writes to the new database
- Run dual-write + reconciliation (see below) to verify data consistency
- After 1-2 weeks of stability, remove those tables’ DDL and DML from the monolith
- Start extracting business logic for those tables into independent services
Common pitfalls
- Cross-domain JOINs: a single JOIN that once worked across two domains now requires API calls or data duplication. Scan all JOIN queries beforehand to identify which are truly cross-domain.
- Distributed transactions: splitting the database turns single-DB transactions into distributed ones. Prefer eventual consistency; use Saga only when unavoidable.
Strategy 4: Side-by-Side Shadow Mode
Risk: ★★★★☆ | Recommendation: ★★☆☆☆
How it works
The new service runs alongside the monolith, processing identical requests, but only the monolith’s result is returned to the client. Compare outputs to verify correctness.
Client → Gateway
├── Monolith (primary → returns to client)
└── New Service (shadow → records but does not return)
Prerequisites
- Correctness is critical (finance, trading, accounting)
- The new service can run completely independently without modifying the monolith’s state
- Enough computing resources to run both systems
Step-by-step
- Deploy the new service in “shadow mode” — receiving live traffic but not affecting the business
- Each request is executed on both the monolith and the new service
- Compare outputs (response body, database changes)
- Analyze and fix discrepancies
- Switch primary provider when the discrepancy rate is below 0.01% and all known differences are resolved
Common pitfalls
- Side effects: even in shadow mode, if the new service writes to a database, it creates “ghost data.” Shadow services must be strictly read-only or use an isolated database instance.
- Performance overhead: every request runs twice, doubling resource consumption. Assess peak capacity; consider comparing only a subset of traffic.
Strategy 5: Feature-Flag Cutover
Risk: ★★☆☆☆ | Recommendation: ★★★★☆
How it works
Embed feature flags in the code to control, via a configuration center, whether a function goes to the new service or the monolith. Flags can be scoped by user, tenant, or percentage.
if (featureFlag('order-service')) {
return await newOrderService.process(order);
} else {
return await legacyMonolith.process(order);
}
Prerequisites
- A feature-flag system is available (LaunchDarkly / custom config center) or can be introduced
- The function being migrated can be routed independently per request dimension
- Fast rollback capability is needed
Step-by-step
- Add feature flags in the monolith for the function being migrated
- Deploy the new service
- Turn on the flag for 1% of traffic (internal test users)
- Gradually expand to 10%, 50%, 100%
- After 1 week of 100% stability, remove old code and the feature flag
Common pitfalls
- Flag leakage: flag keys scattered across the codebase may be forgotten after migration. Centralize all flags in one config file / API; clean them up in one pass after migration.
- Perceived inconsistency: the same user might be routed to different systems across requests (new service vs. monolith). Ensure user-level routing consistency — all requests from the same user go to the same system.
Strategy 6: Bulk Sync + Data Reconciliation
Risk: ★★★☆☆ | Recommendation: ★★★★☆
This is not a standalone migration strategy, but infrastructure required by every migration strategy — data synchronization and reconciliation.
Dual-Write Modes
Monolith writes → DB_old
↘ DB_new (via CDC or application-level dual-write)
- Application-level dual-write: on every write, the monolith also calls the new service’s API or writes to the new database. Invasive, but provides the best real-time consistency.
- CDC sync: use Debezium / Canal to listen to the monolith’s binlog and sync to the new database in near-real-time. Non-invasive, but depends on binlog format and schema compatibility.
Reconciliation Job
Run hourly (or daily) to compare old and new databases:
| Check | Comparison | Alert Threshold |
|---|---|---|
| Record count | COUNT(*) difference | > 0.1% |
| Sum totals | SUM(amount) difference | > 0.01% |
| Latest timestamp | MAX(updated_at) difference | > 5 minutes |
| Sampling | Compare 100 random records field by field | Any field mismatch |
Discrepancy alerts must be automatic (Feishu / DingTalk / email) and pause cutover until the root cause is found and fixed.
Migration Roadmap: From Zero to Full Cutover
| Phase | Goal | Duration | Key Actions |
|---|---|---|---|
| Phase 0: Prep | Observability ready | 1-2 weeks | APM (distributed tracing), centralized logging, business monitoring dashboard |
| Phase 1: Pilot | Split one simple service | 2-4 weeks | Pick a stateless, low-coupling module (e.g., notification service); use Strangler Fig |
| Phase 2: Data Split | Database logical isolation | 3-6 weeks | Split schemas by domain; set up CDC sync + reconciliation |
| Phase 3: Core Migration | Split 2-3 core services | 4-8 weeks | Feature-flag grayscale cutover; independent verification per service |
| Phase 4: Decommission | Monolith shutdown | 2-4 weeks | Clean up residual code in the monolith; remove old gateway routes; archive the project |
Estimated total time: a medium-complexity system (~50 tables, 5-8 business domains) typically takes 3-6 months for a full migration.
Summary
| Strategy | Risk | Best For | Advantage |
|---|---|---|---|
| Strangler Fig | Low | Systems with API gateway | Controlled, incremental |
| Event Interception | Medium | Systems already using message queues | Natural decoupling |
| Database-First | High | Systems with database bottlenecks | Attack the hardest constraint first |
| Shadow Mode | High | Financial-grade correctness | 100% verification |
| Feature-Flag | Low | Need for grayscale rollback | Fine-grained traffic control |
The true sign of a successful migration is not “the new service is live” — it is “the old system can be safely shut down.”
Related reading
- Microservice Decomposition: An Engineering Decision Framework — the “to split or not to split” decision framework you need before migrating
- REST vs GraphQL vs gRPC: A Decision Framework for Backend API Protocols — inter-service communication protocol selection
- Event-Driven Architecture in Practice: Message Queues and Event Streams — async communication and event bus design during migration
Planning a monolith-to-microservices migration? Contact us — tell us about your current system and pain points, feasibility assessment within 24 hours.
FAQ
Should I split the code first or the database first?
Start with the database. Apply logical isolation first — schema separation, read/write splitting — so data boundaries are clear before you touch the API layer. If you split code before the database, you will end up with "separate services still sharing the same table," and fixing that later requires touching every already-split service. Code becomes easy once data boundaries are settled.
When is the Strangler Fig pattern appropriate?
Strangler Fig works best when your monolith already sits behind an API gateway or reverse proxy. You route new endpoints to the new service at the gateway layer while the monolith continues serving old ones. Its greatest strength is risk control — you migrate one endpoint at a time, and a failure only affects that single route. The downside is a long migration cycle with parallel maintenance of old and new code. Best for stable systems with moderate iteration pace.
How should traffic be cut over during migration?
Always use a gradual cutover, never a big bang. Use the gateway to shade traffic by user ID or tenant ID: start with 1% internal users for 3-5 days, expand to 10% real users, then 50%, then 100%. Each stage must have a rollback plan. Key acceptance metrics: P50/P99 latency within 10% of pre-migration baseline, error rate no higher than baseline, and all core business flows passing automated regression tests.
How do you maintain data consistency during migration?
Dual-write is the standard approach — the new service writes to its own database while also syncing back to the monolith (or vice versa) through an event bus. Run reconciliation jobs every hour comparing record counts, sum totals (especially for financial data), and the latest update timestamps. If discrepancies exceed a threshold, alert and pause the cutover. Only decommission the old code path after 1-2 weeks of stable dual-write.
Should I migrate the most complex module first, or the simplest?
Start with the simplest module. The goal is not "fast results" but "establishing the migration pipeline and team confidence." A simple module (dictionary service, notification service) has a small change surface and strong independence. Splitting it validates your CI/CD pipeline, monitoring, and deployment process end-to-end. Once the team is comfortable with the rhythm, move on to core business modules. A simple module takes 1-2 weeks; a core module can take 1-2 months.
This article comes from AI Enable Harness front-line delivery practice. Need a similar system or optimization service?
Subscribe to Updates
Get notified when new articles are published. No spam, occasional updates only.
Subscribe →