13.2 Risk-Driven Service Extraction and Strangler Migration
Service decomposition isn't about copying directories into a new repository; it's about shifting the boundaries of deployment, data, failure, and team ownership. Success means improving the target metrics while keeping the added operational work manageable; counting services does not establish that.
Write Out the Split Assumption
A verifiable rationale includes the current state, goal, and measurements. These numbers are hypothetical and illustrate how to state the decision:
The scoring module accounts for 70% of peak CPU usage, yet it must scale together with the low-load registration module. After extracting scoring into a standalone service, we aim to reduce infrastructure costs by 25% while maintaining a settlement success rate of at least 99.95%.
This is still a hypothesis. CPU share does not directly predict savings. Measure instance, network, storage, and added operating costs under comparable traffic and time windows. Count logical settlement requests for success rate, including timeouts and unknown results; retries must not inflate the denominator.
Common valid drivers include:
- Independent scaling can significantly reduce costs or eliminate resource contention;
- Differences in deployment cadence create real waiting times;
- Fault isolation delivers clear business value;
- Compliance or data residency requirements establish hard boundaries;
- Teams can own end-to-end responsibilities for building, deploying, and operating the service.
"Microservices are more advanced" or "there's a lot of code" are not sufficient justification for a split.
Choose the First Cut
Prioritize capabilities with clear business boundaries, well-defined data ownership, and manageable fan-in dependencies. The first step should be small enough to validate platform capabilities, yet large enough to generate independent value.
Before splitting, conduct at least the following inventory:
- Upstream and downstream callers and their protocols;
- Tables, files, and caches involved in read and write operations;
- Transactional and consistency requirements;
- Peak traffic volumes, latency budgets, and failure semantics;
- On-call responsibilities, alerting, capacity planning, and recovery ownership.
Strangler: Gradual Traffic Shift
The Strangler Fig pattern begins by establishing routing boundaries before fully migrating from the old system, enabling both old and new implementations to coexist:
Client
│
▼
Gateway / Facade
├── /scores/query ──> New scoring service
├── /scores/write ──> Legacy monolith
└── All other requests ──> Legacy monolithAt this stage the new read path needs synchronization from the old writer and an explicit freshness promise. Monitor the router’s capacity and failures too. Prefer stable tenant or user cohorts so one session does not jump between incompatible data views. Shadow reads must not trigger real charges or notifications.
A migration typically involves:
- Define observable contract boundaries around existing capabilities;
- Implement the new service to match the same contract, performing shadow reads or offline comparisons;
- Route read requests by tenant, user, or traffic proportion;
- Transfer ownership of write operations to the new service;
- Shut down the old path and monitor a full business cycle;
- Remove old code and compatibility infrastructure.
The final step must never be skipped. Permanent dual-track operation risks turning the temporary migration layer into the next generation of legacy systems.
Write Requests Cannot Blindly Fall Back
The following strategy is dangerous: after a new service times out, automatically fall back to the old monolith. A timeout means the client did not receive a result, not that the new service failed to commit. Falling back in this way could result in duplicate deductions or duplicate settlements.
A write path must include:
- A stable idempotency key;
- Queryable operation status;
- Clear distinction between "definitively failed" and "result unknown";
- Retry budgets and backoff;
- Reconciliation and manual recovery paths.
Reads are easier to fall back when stale data is acceptable, but record the source and consistency difference. After the new service takes writes, an unsynchronized old reader cannot promise read-your-writes. Reversing a route does not reverse the data. Deduplication must also be shared across paths or requests routed to one write owner; separate idempotency stores do not prevent cross-path duplicates.
Migrate Data Ownership
Direct dual writes to two databases can partially succeed. A more reliable approach depends on the phase:
Historical Data Population + Change Capture
The snapshot and incremental stream need a coordinated log position. Let the connector align a consistent snapshot with the change-stream starting point, retaining required logs and handling changes during backfill. Scanning tables and only then enabling CDC can lose intervening updates. Test updates, deletes, duplicate events, and restart recovery; the exact procedure depends on the database and connector.
Before switching, reconcile business keys, counts, content checksums, and invariants, and measure replication lag. To hand off writes for a cohort, fence old writes, drain in-flight transactions, record the final position, wait for the new side to reach it, then enable only the new writer. Without a brief write pause, an equivalent fencing and ownership-transfer protocol is required.
Transactional Outbox
In the old system, business data and outbox entries are written within the same database transaction. An independent publisher then delivers the changes to the new service. The intent to send is persisted with the business commit, closing the gap in which that intent could be lost. Publication still lags behind commit. The relay must recover and retry; crashes can cause duplicate delivery, so consumers still need idempotency and business-specific ordering.
Expand and Contract
First, extend the protocol or schema so both old and new versions can accept it. After migrating producers and consumers, remove the old fields. Destructive changes must not be tied to a traffic switch in a single irreversible step.
Once data ownership has been fully migrated, the old system should no longer treat the new service’s database as a shared table.
Treat Failure Models as Design Inputs
After in-process calls transition to network calls, new concerns emerge:
- Connection failures, timeouts, and partial responses;
- Retry amplification and request queuing;
- Coexistence of multiple versions;
- Call chain tracing and cross-service authentication;
- Services appearing healthy while dependencies degrade, what we call gray failures.
Every remote call should have a timeout, but not every failure should trigger a retry. Only idempotent or idempotent-protected operations are suitable for automatic retry logic. Retries must also have limits on count, total duration, and jitter to prevent synchronous storms.
Every Step Must Preserve Exit Conditions
A migration plan must explicitly state:
| Item | Example |
|---|---|
| Success metrics | p95 latency, success rate, cost, time to release |
| Invariants | Score deltas follow the rules; each match has one effective settlement |
| Traffic cohort | Internal account → 5% users → 25% → 100% |
| Stop conditions | Error rate exceeds baseline by 0.2 percentage points |
| Rollback strategy | Route traffic back to the old read path; pause write path and reconcile |
| Final cleanup | Remove old write paths, old APIs, and migration flags |
Define a measurement window and minimum sample size for stop thresholds. With a baseline error rate of 0.1%, an increase of 0.2 percentage points gives 0.3%, not 0.1002%. Read fallback needs a fresh-enough old source. After transferring writes, rollback requires another ownership handoff and data catch-up, not just a reversed switch.
After migration is complete, reevaluate the original assumptions. If the independently deployed services fail to improve target metrics, allow consolidation or termination of further splitting, do not drive additional service decomposition with sunk costs.
The next chapter discusses foundational communication patterns after service boundaries have already been established: how discovery, gateways, timeouts, retries, and circuit breakers combine to form predictable call chains.
References
Martin Fowler, Strangler Fig Application
AWS Prescriptive Guidance, Strangler fig pattern
Chris Richardson, Transactional Outbox