Home Resume
Homeβ€Ί Blogβ€Ί AWS Architecture Series #29 β€” Strangler Fig: The Routing Layer Is the Migration…
AWS Architecture AWS Architecture Series

AWS Architecture Series #29 β€” Strangler Fig: The Routing Layer Is the Migration

The monolith cannot be replaced in one release, so it gets replaced a piece at a time behind a routing layer. Every account of this pattern spends its time on how to split the code and almost none on the router β€” which is the component that decides whether the migration is reversible, and the one with the hard quotas.

Verified against current vendor documentation on 22 August 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.

Business Challenge

The strangler fig is the least controversial pattern in migration work. Put a routing layer in front of the old system, build each new capability behind it, move traffic per capability, and delete the old system when nothing routes to it any more. Nobody argues with it.

What gets argued about is the code: where the seams are, which service comes out first, whether the boundary is right. Those are real questions, and they are also the questions that a team can answer at a whiteboard. The router is treated as plumbing — an ALB, some rules, done.

It is not plumbing. It is the only component that touches every slice of the migration, it is the mechanism by which every rollback happens, and unlike the code, it has quotas that were set by somebody else.

1The router is the rollback plan

In a strangler fig migration there is no release to revert. The new service is already deployed; what changed was a routing decision. So the quality of your rollback is exactly the quality of your router — how fast a weight change takes effect, whether it can be scoped to one slice, and whether anything else moves when it does.

A team that has not measured that has not got a rollback plan. It has an intention.

2Progress is measured in rules, and rules run out

Each slice in flight needs its own routing expression, and slices overlap: one at 100%, three ramping, two held at zero pending a data backfill. That is six live expressions for a migration that has barely started.

An ALB gives you 100 rules, which is generous and adjustable. It gives you 5 target groups per forward action, which is neither. If your mental model is "one target group per version per slice", you meet that ceiling sooner than the rule count suggests.

3Dual-write is a consistency decision wearing a migration costume

Routing reads is easy. Routing writes means both systems believe they own the data, and the moment you write to both you have taken on a distributed transaction without calling it one. Whichever way you resolve it — old system authoritative, new system authoritative, or events reconciling both — you have chosen a consistency model.

The pattern does not choose it for you, and the routing layer cannot enforce it.

4Knowing when to cut is a decision nobody schedules

The failure mode of this pattern is not a bad cutover. It is a migration that reaches 95% and stops, because the remaining 5% is the hard part and the pain has already gone. The organisation now runs two systems permanently and calls it a transition.

The router makes that state comfortable, which is precisely why it needs a scheduled end.

Architecture

There are three places on AWS to put the split, and they behave differently enough that the choice is architectural rather than a matter of taste.

Diagram: the three places to split traffic in a strangler fig migration on AWS, comparing Route 53 weighted records, Application Load Balancer listener rules and CloudFront continuous deployment, with the documented limit and the counter-intuitive failure mode of each, alongside the dual-write decision the router cannot make

Route 53 weighted records: coarse, slow, and it lies about zero

Weighted records take an integer between 0 and 255, and Route 53 answers queries in proportion to a record's weight over the sum of all weights. A pair weighted 1 and 255 sends about 0.39% of traffic to the first — a genuinely small canary, which is the main argument for using DNS at all.

Two things make it a poor primary mechanism. The first is TTL: every record in the group must share one, and if you set them differently “Route 53 changes the value of TTL for all of the records to the last value that you set”. Your rollback is not fast; it is TTL-fast, and resolvers that ignore TTL are not fast at all.

The second is the trap. “If you set Weight to 0 for all of the records in the group, traffic is routed to all resources with equal probability.” The instinct under pressure — zero everything, stop the bleeding — produces an even split across every resource in the group. The control that looks like an off switch is a shuffle.

Application Load Balancer: the right default, with a hard ceiling

Listener rules are where most strangler fig migrations belong. Routing is per-request rather than per-resolver, changes take effect immediately, and conditions can express the seam properly — path, header, method, source.

Plan against the two quotas that do not move. A forward action references at most 5 target groups, and a rule allows at most 5 condition values and 5 match evaluations. None of the three is adjustable. The 100-rule limit is adjustable and is rarely what you hit first.

CloudFront continuous deployment: not a migration tool

It looks like exactly the right thing — a staging distribution, a weight, real production traffic. It is worth reading the quota page before building on it.

The weight ceiling is 15%, so it cannot complete a cutover. Session stickiness is bounded at 300–3600 seconds. It cannot be used at all with a distribution that supports HTTP/3. And the decisive one: “CloudFront sends all requests to the primary distribution during peak traffic hours, regardless of what's specified in the continuous deployment policy” — where peak refers to load on the CloudFront service, not on your distribution.

Your canary can therefore stop being a canary at a time you did not choose and cannot observe, and it will do so under exactly the traffic conditions you most wanted to test. That is acceptable for validating a CDN configuration change, which is what the feature is for. It is not a basis for migrating an application.

The dual-write decision sits underneath all three

None of these primitives has an opinion about your data. Once writes are in scope you are choosing between the old system remaining the system of record while the new one shadows it, the new one taking the record with a backfill and a reconciliation job, or both emitting events into a log that is the actual source of truth.

The reason this belongs in a routing discussion is sequencing: the write decision determines how reversible a routing change is. If the new service has begun accepting authoritative writes, moving the weight back does not undo them, and the router's rollback story quietly stops being true.

Why This Architecture Holds Up

Every one of the limits above is knowable in advance and none of them is discovered comfortably. A team finds the five-target-group ceiling when it adds the sixth slice, which is several months in, with the routing topology already committed to. A team finds the CloudFront peak-hour behaviour when its canary metrics look implausibly clean and somebody eventually asks why.

The deeper point is that this pattern moves risk out of the release process and into configuration. That is a real improvement — configuration changes faster and reverts faster than deployments. But it also means the safety properties everyone assumes are now properties of a router's documented behaviour rather than of a deployment pipeline that was designed and tested.

Read the quota pages for the primitive you have chosen. They are short, and each of the three contains at least one sentence that contradicts what an experienced engineer would assume.

Key Architecture Decisions

DecisionTake thisBecause
Where to put the split ALB listener rules by default Per-request, immediate, and the seam can be expressed as a condition rather than a hostname
Using CloudFront continuous deployment Only for CDN configuration changes, never for application migration 15% ceiling, no HTTP/3, and all traffic reverts to primary during CloudFront-wide peak hours
Using Route 53 weights Reserve for whole-stack or cross-Region moves, with a short uniform TTL TTL is shared across the group and rewritten to the last value set; rollback is only as fast as resolver caches
Emergency stop on weighted DNS Set the target's weight to 0 and leave the others non-zero Zeroing every record in the group distributes traffic evenly instead of stopping it
Slice topology Budget against 5 target groups per action, not against the 100-rule limit The rule count is adjustable; the per-action target group count is not
Writes Decide the system of record before the first write routes, not after Once the new service writes authoritatively, reversing the route no longer reverses the change
The last 5% Put a date on decommissioning the old system when the first slice moves The pattern makes running both systems indefinitely comfortable, which is how migrations stall

Closing Thought

The strangler fig is usually taught as a way to think about decomposition. In practice the decomposition is the part teams are good at, and the router is the part that decides whether any of it can be undone.

Read the quota page before you draw the diagram. Three primitives, three documented behaviours that invert what an operator expects: CloudFront stops splitting under peak load, Route 53 turns all-zero into an even split, and an ALB forward action stops at five target groups and will not be raised. None of those is obscure. All of them are found late.

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent