The legacy system arrives at the architecture discussion with two offers on the table, and both are bad. The first is the rewrite, a new system built beside the old one, complete in a year, and the year is always three and the definition of complete is always missing the features the old system accumulated in its second decade. The second is the settlement, the legacy stays forever, and the team's best engineers spend their careers as its archaeologists, afraid of every change and priced accordingly. The strangler fig pattern is the third path, named for the vine that grows around a tree and eventually replaces it, and its engineering meaning is precise, the new system grows around the old one, intercepting traffic one route at a time, until the old system is empty and can be turned off. This article builds that growth properly, the routing seam that makes it possible, the data strangling that makes it survive, the anti corruption layer that keeps the old world out of the new, and the retirement discipline that actually turns the old system off.
Why the rewrite fails and what the fig borrows from its failure
The rewrite's failure is structural rather than a matter of willpower. The legacy system's real specification is its behavior, twenty years of edge cases, regulatory accommodations, customer specific branches and bug fixes that became features, and the specification exists nowhere except in the code and in the customers' expectations. The rewrite must reproduce it without being able to read it, and the gap between the remembered specification and the actual one is exactly the defect budget the timeline never carries. The second structural failure is the value delivery, the rewrite ships value at the end, and the organization that funds a three year project with no intermediate value is the organization that cancels it in year two.
The fig pattern borrows the rewrite's ambition and changes its two structural properties. The new system ships from the first month, because it takes real traffic for one real slice, and the specification is reproduced incrementally, one route at a time, with the old system still answering everything the new one has not taken. The intermediate state is a feature, the fallback that catches every gap the migration missed, and the value delivery is continuous, each strangler increment is a deployable improvement on a production path. The pattern's cost is honest too, the seam's operational complexity and the dual running period, and the articles sections below are the engineering that pays for it.
The routing seam as the pattern's single prerequisite
The strangler needs a place to intercept traffic, and the seam's quality decides everything. The best seam is a layer that already exists, the reverse server or the API gateway in front of the application, and the routing becomes a configuration change:
# gateway routing table
route /api/orders -> new-orders-service # strangled slice
route /api/ -> legacy-app # everything else
The route is the unit of strangling, and the route's size is a judgment the migration reviews, one endpoint, one page, one bounded context, each increment small enough to verify and large enough to matter. The seam that does not exist yet is built first, a routing layer in front of the monolith, and the teams that resent building the seam should price the alternative, the rewrite with no seam and no fallback. The dark launch pattern completes the seam's toolkit, the new service receives mirrored traffic, a copy of the requests with responses discarded, and the comparison between old and new answers runs before any user's request depends on it, the shadow traffic that finds the specification gaps while they are still free.
Data strangling, the harder half of the pattern
The traffic seams are the visible half, and the data seams are where migrations die. The new orders service needs the order data, and the order data lives in the legacy database with forty other tables entangled, and the three standard answers cover the transition. The first is the shared database period, the new service reads the legacy tables directly, the fastest path with the highest coupling cost, and it is legitimate as a transition step if it has an exit date, the database view or the read model that isolates the new code's surface, so the exit is a change of the view rather than an archaeology project.
The second is the synchronization, the change data capture machinery this series met in the outbox discussion, the legacy writes stream to the new store, the new service serves from its own schema, and the synchronization is one directional during the strangling, the legacy remains the system of record until its last route is gone. The third is the cutover per slice, the new service becomes the record for its slice, the writes route to it, and the synchronization reverses or the legacy's slice tables go read only and eventually die. The data strangling rule that keeps the migration alive is the slice discipline, the same route granularity on the data side, one table family moves at a time, and the data migration that tries to move the whole schema at once is the rewrite wearing a fig costume.
The anti corruption layer at every boundary
The new system's model must stay new, and the legacy's model, its customer table with its four decades of overloaded flags, its status codes that mean three things each, must not leak through the seam. The anti corruption layer is the translator the domain driven design discussion built for exactly this, the legacy types are converted at the boundary, the new domain speaks only its own language:
class LegacyOrderTranslator:
def to_domain(self, legacy_row):
status = self.decode_status(legacy_row.sts) # 'P' = paid or pending?
return Order(status=status, placed_at=parse(legacy_row.dt, LEGACY_EPOCH))
The layer's cost is real and its absence costs more, because the model that absorbs the legacy's semantics to save the translation carries them into the new system's second decade, and the strangler that reproduces the old chaos is a rewrite with extra steps. The translator is also the specification's recovery mechanism, every ambiguity the translation meets, the status code with three meanings, the date format with two epochs, is a specification question with a named answer, and the answers accumulate into the migration's real requirements document, written in code where it can be tested.
One slice migrated on a calendar
The slice walkthrough makes the pattern's rhythm concrete. The orders slice, chosen first because its traffic is high and its logic is stable, runs on a fourteen week cadence that the mature teams report as typical. Weeks one and two are the seam and the read, the new service builds against a view over the legacy tables, the shadow traffic starts, and the diff log records every disagreement between old and new answers. Weeks three to six are the reconciliation, the disagreements triaged daily, most are the specification's edge cases surfacing, the tax rule that the legacy applies to one region's customers only, and each becomes a test case on the new service. Weeks seven and eight are the write cutover rehearsal, the new service takes writes, the legacy reads them back through the synchronization, and the finance reconciliation runs on the dual written week.
Week nine is the cutover, the gateway route flips, the new service is the record for orders, the legacy's order paths go read only, and the rollback is one configuration change with a rehearsed procedure. Weeks ten to fourteen are the observation and the cleanup, the traffic dashboards confirm the flip held, the legacy's order tables stop receiving writes, and the first scheduled delete of the dead code begins. The calendar's property that makes it repeatable is that every phase has a rollback and every phase ends with production better than it started, the shadow traffic is thrown away, the reconciliation is a report, the cutover is a route, and the migration never holds the organization hostage to a big bang it cannot cancel.
Testing in the dual running period
The strangler's testing strategy is the comparison, and the tools are the shadow traffic and the diff log. Every request the seam sees is replayed to the new service, the responses are serialized into a normalized form, timestamps and identifiers excluded, and the diff log records the mismatches with the request attached, so the triage meeting reviews a queue of concrete disagreements rather than an argument about theory:
def compare_responses(legacy_json, new_json):
legacy_norm = normalize(legacy_json)
new_norm = normalize(new_json)
if legacy_norm != new_norm:
diff_log.write(request_id, legacy_norm, new_norm)
The suite the comparison cannot run is the legacy's internal state, the batch jobs and the side effects, and those get the manual verification with the finance and operations teams, the month end close run twice, the exports diffed, and the sign offs that the strangler's acceptance tests are organizational as well as technical. The regression safety is the pattern's quiet gift, the legacy keeps answering every strangled route until the flip, so the new slice's defects are found by the shadow, not the users, and the cutover's risk is the one slice's risk rather than the system's.
The retirement discipline, turning the old system off
The pattern's final discipline is the one its namesake promises, the old tree actually falls, and the discipline is a list of honest steps. The traffic metrics per route, the old system's dashboard of what still reaches it, and the milestone is the empty dashboard rather than the optimistic estimate. The data verification, the counts and checksums across the seam, the finance team's reconciliation as the strangling's acceptance test. The dependency sweep, the cron jobs, the file exports, the integration points nobody documented, found by monitoring the legacy's actual I/O in its quiet hours. The freeze conversations, the feature requests that arrive for the legacy during its final year, each one an opportunity to accelerate the strangling or a temptation to reinforce the tree, and the migration's political discipline is saying the second one out loud and declining it.
The off switch is a moment worth celebrating properly, the last route, the last table, the decommission runbook, and the teams that skip the celebration report the same regret, the migration's final milestone passed as a routine deploy. The tree is dead, the vine is the system, and the architecture that everyone said would take three years took fourteen months of continuous delivery, because every month shipped something, which is the pattern's whole argument.
The working rules for the fig
The pattern's discipline compresses into the short list, and the list is the migration's contract with itself:
- Build the seam first, route the legacy through it, and strangle per route, never per rewrite;
- Dark launch each slice with mirrored traffic, and compare before any user depends on the new path;
- Migrate data per slice with an exit date on every shared database period;
- Translate at every boundary, the anti corruption layer is the new model's immune system;
- Track the legacy's traffic metrics, and let the empty dashboard, not the estimate, name the end date;
- Decline the legacy feature requests, each one is a branch grafted back onto the dead tree.
The legacy system that terrified the team was never one system, it was a hundred routes and a hundred tables, and the strangler's insight is the same one every large problem needs, the decomposition that makes each piece small enough to verify and valuable enough to ship. The fig grows because it is planted in traffic, watered by production, and pruned by monitoring, and the migration that grows the same way ends, unlike the rewrite, with a date everyone believes.