All articles
13 September 2026
7 min read

The strangler fig pattern in practice

By Tim Rutte, Cloud & Software ArchitectTopicLegacy & Modernization

An old weathered wooden post with a new white lattice frame built around it; one of the connecting clamps is blue.

Of all the modernization patterns this is the best described and the worst told. The comparison with the strangler fig growing around a tree until the tree is gone appears in every text. What rarely appears is the part the implementation actually hangs on.

The part it hangs on is not the routing. Putting a switch in front of the application is an afternoon. It is the data. As long as two systems both claim the same records, nothing has been replaced, it has been duplicated.

This article describes the pattern the way it runs in a live system: the order, the cut, the handling of the data, and the five places where it tears.

The switch first, and it changes nothing

The first step produces no benefit, and that is precisely its job. In front of the application goes a routing layer that sends one hundred percent of the traffic to the old system.

# Step one: the switch is in place and sends everything to the
# old side. No user notices anything. That is the point.
location / {
    proxy_pass http://old;
}

# Step two, days later: one path, and only one.
location /orders/status {
    proxy_pass http://new;
}

The reason for that separation is very practical: ship the switch and the first feature together, and when something breaks you cannot say which of the two is at fault. A routing layer that has been running unremarkably for a week is ruled out as a cause.

The load balancer you already have is enough for this layer. It needs no gateway with a product name, and above all it needs no logic inside it: the moment the switch decides what happens in business terms, it is the third system somebody will have to replace later.

Where the first cut goes

The common choice is the area that looks technically easiest: a peripheral feature with no data, an export, a status page. That is understandable and costs a year.

Because afterwards, visibly nothing has happened. The replacement is running, the team is busy, and nobody in the company can say what got better. Support crumbles exactly when the hard part starts.

The cut that holds follows not the technology but the ownership: an area that owns its data can be pulled out. A layer cutting across everything cannot. And among the areas that own their data, take the one that changes most often. That is where the replacement pays off every week, and where it is demonstrable after three months.

Which area should be the first one at all is decided beforehand: Which service to pull out of the monolith first.

The hard part

Before the first line in the new service, three questions get answered, and they are not architecture questions but ownership questions.

Who writes? For every table in the area there is exactly one writer from the switch-over onwards. A second one is not a replacement, it is a data set with two truths, and the divergence surfaces months later.

Who reads? The old side almost always keeps reading, and that is fine as long as it is known. The list of readers is the part that gets forgotten in the inventory: reports, nightly runs, a second repository, an interface for a partner.

How does the other side get the data? Three routes, in this order of preference: an event the owner publishes; a query through an interface; and last, when there is no other way, continued direct read access to the table, but with a written expiry date.

For the migration itself the mechanics are the same as for any data move in a live system: write both, backfill, reconcile, switch.

How dual writing, backfilling and reconciling run in production is covered in Dual write and backfill.

Computing along before anybody sees it

Between "the new path is finished" and "the new path gets traffic" sits a step that costs little and catches most of the surprises: the new path processes real requests, but its result does not go to the user, it goes into a comparison.

// The old path answers as always. The new one computes along,
// and only the divergence gets logged.
$old = $this->oldCalculation->for($order);

try {
    $new = $this->newService->for($order);
    if (!$this->equal($old, $new)) {
        // Do not log the whole record: the divergence is enough,
        // and personal fields have no business in a
        // comparison log.
        $this->logger->warning('divergence', [
            'case'   => $order->id,
            'fields' => $this->differences($old, $new),
        ]);
    }
} catch (Throwable $e) {
    // A fault in the new path must never touch the old one.
    $this->logger->warning('shadow run failed', ['error' => $e->getMessage()]);
}

return $old;

The effort is two days, and what comes out of it is regularly uncomfortable: rounding that behaves differently, a special case from 2018, a sort order somebody relied on. All of it would have surfaced in production anyway, just later and with an audience.

Switching in steps

When the comparison stays quiet for a week, you switch, and not all at once. One percent, ten, fifty, one hundred, with a metric per step that was agreed beforehand.

Two rules for that, both learned the hard way. Rolling back has to be a switch, not a deployment. Anyone who needs a release for the way back rolls back too late, because twenty minutes is too long. And the step is measured against the same metric as the old path, not a new one: a comparison is robust against the rhythm of the day, an absolute threshold is not.

What those steps look like technically is a question of the load balancer, not of the platform. For the share per step the same mechanics as a canary are enough.

Switching the old path off is part of the job

Here sits the most common mistake, and it is not technical. After the switch-over the old path stays, because nobody wants to take it away: it worked, after all, and you never know.

The result is the worst of all situations. Two systems get operated, both receive security updates, both have to be considered at the next schema change, and the effort is higher than before the replacement. The modernization has made things worse.

So switching the old path off belongs in the same task as switching over, not in a follow-up ticket. A follow-up ticket is an intention, and intentions hold until the next tight week. What may stay is the code, for four weeks, in a branch nobody operates.

How far this route should be pushed, and where it stops, is covered in From monolith to microservices.

The five places where it tears

Finally the list that is missing from the descriptions of the pattern, because it only forms on the third attempt.

A second writer. A nightly run or a report keeps writing to the table that now belongs to the new service. The case does not get reported, it becomes visible as inconsistent data, weeks later.

The session. Users move between the old and the new path, and both sides have to understand the same sign-in. Notice that at switch-over time and you rebuild authentication under pressure.

Shared identity. Two systems, two number ranges, and an order number that exists twice. Identifiers get settled before the first cut, not after it.

The path without a request. Queues, batch jobs and scheduled tasks run past the switch. They get assigned to one side deliberately, or they run twice or not at all.

The order inside the company. The fifth point is the only one without technology: if nothing visible exists after six months, the project gets stopped, regardless of how well it is going. Which is why the area with the highest rate of change goes first, not the one with the lowest data load.

Anyone wanting to apply the pattern to a running system will find the term in the glossary and the approach as a service on its own page.

This article continues a series about systems that already exist. The retrospective on the first three months orders its articles by situation.