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. A successful migration is not marked by an increase in the number of services, but by improved constraints on the target system and the ability to manage the added complexity within operations.
Write Out the Split Assumption
A verifiable split rationale should include the current state, the goal, and a measurable outcome. For example:
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%.
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 monolithA secure migration typically involves the following steps:
- 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.
Writing Requests Cannot Blindly Fall Back
The following strategy is dangerous: after a new service times out, automatically fallback to the old monolith. A timeout only indicates that the client did not receive a response, it does not mean 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";
- Retries with backoff;
- Audit trails and manual recovery paths.
Read requests can be more easily retried when outdated data is acceptable, but the source and consistency differences must still be recorded.
Migrate Data Ownership
A direct dual-write of two databases results in partial success. A more reliable approach depends on the phase:
Historical Data Population + Change Capture
First, populate historical data via snapshot. Then, use Change Data Capture (CDC) to capture incremental changes. Before switching, reconcile the old and new systems using business primary keys, counts, checksums, and key invariants.
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. This eliminates the "database committed, message not sent" window, but consumers must still be idempotent.
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 | Total score sum, settlement per game occurs only once |
| Canaries | 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 tables, old APIs, and migration flags |
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