SwadhyAI Notes

All System Design notes

System Design Patterns

Strangler Fig Pattern

Imagine a checkout team that must replace a decade-old monolith before peak season. A big-bang rewrite would risk every payment and cart for weeks. Instead they put a stable front door in front of the old system and peel off one slice at a time: search first, then catalog reads, then checkout, until the legacy path has nothing left to serve.

That is the : keep the business running while a routes some traffic to the new service and the rest to legacy, with rollback and at every step.

Migration journey

1

Clear contracts beat random endpoints.

2

Compare without user impact.

3

1% → 5% → 25% → 100% with gates.

4

Delete legacy path

Only after a stability window.

Problem → naive → break

Problem: peak season is twelve weeks away and the monolith owns checkout, catalog, and search in one deploy unit. Every release risks payments.

Naive move: freeze features, rewrite everything in a parallel stack, flip DNS on Saturday night. Counterexample: a team that cut over search + catalog + checkout together discovered a tax-rounding mismatch only after real charges: rollback meant restoring a database dump while carts were mid-flight.

Break: the big-bang leaves no reversible seam. Dual ownership appears overnight; support cannot tell which path served a user; partial write success has no clean undo.

Correctness invariant

For every route/cohort at time t, exactly one path is authoritative for the user-visible response. Write ownership for a domain never splits across two live writers. If the candidate has accepted writes, rollback must replay or quarantine those writes: traffic reversal alone is not recovery.

What actually happens

Users still hit one entrypoint. The asks which path owns this route, cohort, or flag. Migrated paths go to the new service and its owned database; everything else stays on the monolith. A bridge or API adapter keeps data moving while both worlds exist.

Before real users depend on the candidate, exercises it and dashboards gate promotion. Then a widens exposure. After a sustained stability window, unreachable legacy code and schemas get deleted on purpose, not left forever "just in case."

The migration duration depends on the amount of hidden behavior, the data boundary, and the evidence required before moving traffic. A read-only search slice may move quickly; checkout may require months of compatibility and recovery work. Track progress by retired dependencies and transferred ownership rather than service count. A new endpoint that still writes legacy tables has changed deployment shape without completing the migration.

Which path owns this route?

Legacy paths

Migrated paths

GatewayLegacyNew pathParity

Try it: split strangler traffic

Users still enter through one facade. A weight sends real responses from the candidate; shadow traffic duplicates requests for parity without changing what users see. Extra load is the sum of candidate QPS and mirrored shadow QPS.

Legacy QPS
900
Candidate QPS
100
Shadow QPS
200
Parity gate
Fail

Traffic split

user-visible

Legacy 900 req/s

Candidate 100 req/s

Migration load

includes shadow

Candidate serves 100 req/s to users. Shadow adds 200 req/s that do not change responses. Combined new-path work is about 300 req/s before legacy traffic (900 req/s).

Adjust how much user traffic hits the candidate, then add shadow traffic that exercises the new path without serving responses. Parity gates should compare error rates before you widen the slice. Recommendation: Hold promotion: candidate errors exceed the parity threshold.

Shadow cost. Twenty percent shadow on 1k QPS adds 200 mirrored requests per second on top of whatever slice already runs on the candidate. Budget CPU, databases, and observability for that overlap period.

Stop conditions. When candidate error rate exceeds roughly 125% of legacy baseline, hold or roll back even if product pressure wants a faster cutover.

How to choose a strangler migration

1. Is big-bang rewrite risk existential? If a failed cutover would halt checkout or payments, prefer reversible slices. For a healthy modular service that only needs cleanup, strangler overhead is often too heavy.

2. Do you have a clear seam? Start with a that has measurable outputs and known ownership: catalog reads beat tangled checkout workflows early.

3. Can you roll back and prove parity? Without feature-flag owners, dashboards, and deletion criteria, you will run two systems forever.

Simple rule of thumb

Treat migration as product work: measurable parity, staged traffic, and operational dashboards, not a one-shot PR. Pair routing changes with database ownership milestones so you do not accumulate hidden coupling behind new HTTP façades.

One writer plus outbox/ beats a . Translate legacy shapes through an so the new domain stays clean.

Forces that shape each slice

Intent: replace risky legacy behavior through reversible routing slices while keeping the business running.

Forces

Legacy coupling, unknown behavior, data ownership, customer tolerance, and team capacity decide how thin each slice should be.

Consequences

You operate old and new paths together for a while, which requires parity tests, dual dashboards, and deletion discipline.

Pain vs complexity

Worth it when big-bang rewrite risk is existential; too heavy for simple code cleanup inside a healthy modular service.

The incremental loop

Identify a seam, build the replacement, shadow or canary traffic, shift routing, delete dead code: repeat while observability proves parity.

Map bounded contexts

A useful seam has an identifiable input, output, and owner. Product search is often easier to isolate than order placement because its result can be compared without committing money or inventory. List hidden dependencies such as permissions, price rules, background jobs, and reporting before choosing the slice. The smallest URL path is not necessarily the smallest business boundary.

Stand up replacement

Deploy beside legacy; match external behavior before cutting traffic.

Route selectively

The gateway applies a versioned rule to choose the authoritative implementation for each request. For stateful journeys, assign a stable cohort such as tenant or customer so retries and later steps return to compatible ownership. Log the routing decision with the operation ID. Randomly routing each write independently can split one business workflow between old and new systems before their data is ready.

Retire module

After sustained stability, delete unreachable legacy paths and prune schemas deliberately.

Migration control plane

Before you move traffic

Define route key, cohort rule, feature flag owner, rollback owner, parity dashboard, diff thresholds, support contact, and deletion criteria for each slice.

Operating cost

During overlap, teams operate legacy + candidate + sync + observability. Product support and runbooks must know which path served a user.

Stop conditions

Parity diff or stop condition?

Yes: rollback

No: continue

Routing tactics

Path prefix

Route narrow modern paths first; fall through to the legacy stack for everything else.

# gateway routing sketch
location /api/search {
  proxy_pass http://search_service;
}
location /api/ {
  proxy_pass http://legacy_monolith;
}

Headers / flags

Opt users into candidates via headers or feature flags: pairs naturally with progressive delivery metrics.

Weighted split

yaml
routes:
  - destination: legacy
    weight: 90
  - destination: modern
    weight: 10

Migration split math

Traffic slice

At 20k requests/sec, a 1% split still sends 200 Queries Per Second (QPS) to the new path. The candidate must be production-ready even for small percentages.

Error gate

If the old path errors at 0.1%, a 1% candidate slice should compare against about 0.2 candidate errors/sec at 200 req/sec before widening.

Shadow cost

Shadowing doubles selected read traffic. Mirroring 25% of a 40k requests/sec read path creates 10k extra candidate QPS without user-visible responses.

plaintext
candidate_qps = total_qps * traffic_weight
20_000 qps * 0.01 = 200 qps

shadow_extra_qps = read_qps * mirror_percent
40_000 qps * 0.25 = 10_000 extra qps

backfill_time = rows / rows_per_sec
500_000_000 rows / 25_000 rows_per_sec ~= 5.6h

dual_run_cost = legacy_cost + candidate_cost + sync_cost + observability_cost

Stop conditions

Error gate

Candidate error rate exceeds legacy baseline plus agreed tolerance.

Latency gate

p95 or p99 latency regresses on the migrated cohort.

Parity gate

Response diff or business mismatch crosses severity threshold.

Data gate

Change Data Capture (CDC) lag, replay backlog, or reconciliation errors grow.

Support gate

Tickets, refunds, or manual operations spike after exposure.

Verification before promotion

Parity means equivalent business behavior, not necessarily byte-for-byte identical responses. Normalize timestamps, ordering, and generated IDs before comparing, but preserve meaningful differences such as price rounding, permission decisions, and missing records. Break mismatches down by cohort and operation type. A global 99.9% match can hide a complete failure for a small but important customer group.

Shadow / dark launch

Shadow traffic sends a copy to the candidate while legacy continues serving the user. Mirror reads only after accounting for their extra database and cache load. A shadowed write must not make a second payment, send another email, or reserve inventory; use a controlled sink or simulation for those effects. Shadowing proves behavior on sampled inputs, while a canary additionally tests the actual user-facing path.

Controlled experiments

ruby
experiment("search-refactor") {
  control { legacy.search(q) }
  candidate { modern.search(q) }
}

Data phases

Shared store (bridge)

Fastest start: risk coupling until boundaries firm.

Single writer domain

New service owns writes; legacy reads via API or replica rules.

Change Data Capture (CDC) sync

CDC can keep a candidate database current after an initial backfill by streaming committed changes from the existing owner. Record the backfill checkpoint, apply later changes in a defined order, and monitor how far behind the candidate is. At write cutover, ensure the new owner has caught up and fence the old writer. Copying rows continuously does not itself decide who may accept new writes.

Full separation

Foreign keys become service calls or async projections: expect eventual consistency trade-offs.

Break the improved design: facade failure timeline

Strangler does not remove risk. It relocates it into routing, sync, and dual-run ops. Walk one concrete timeline.

t=0      Canary at 25% for /api/checkout; candidate owns writes for that cohort
t=12m    Facade flag flips cohort back to legacy after p99 spike (ops panic)
t=12m+2s Candidate already committed order O-991 with payment auth
t=12m+5s Legacy path cannot see O-991; user retries → second charge attempt
t=15m    Support sees two paths; no shared idempotency key across writers
Outcome  Invariant broken: write ownership split + rollback without replay

Big bang rewrite

High cancel risk: prefer reversible increments.

Data coupling ignored

Ends as distributed monolith: plan ownership moves explicitly.

No rollback path

A rollback switch restores routing only while legacy can safely interpret all writes accepted by the candidate. Before promotion, define how those writes return to legacy or how legacy remains read-only during recovery. Test that plan with real state transitions. Keep the switch until the observation window and data compatibility conditions are satisfied, then retire it with the old path to avoid permanent dual operation.

Strangling too wide

Limit concurrent seams per team to preserve review depth.

+ silent facade drift

Two writers without shared idempotency, or a facade that routes inconsistently across retries, recreates the big-bang failure inside a "safe" migration.

Watch in production: route→backend map version, cohort mismatch rate, parity diff %, CDC lag, candidate write volume after a rollback switch, and refund/ticket spikes keyed by path.

What to say in a design review

Pattern selection

Choose strangler when risk comes from migration scale, not when a single refactor is enough. Start with a route that has clear ownership and measurable outputs.

Tradeoffs

Running two systems costs more temporarily, but buys rollback, learning, and production parity evidence.

Failure modes

Inconsistent routing, duplicate writes, stale replicated data, hidden legacy dependencies, and never deleting the old path.

Rollout and observability

At millions of users, use shadow traffic, diff dashboards, weighted canaries, error-budget gates, and per-slice rollback switches before deleting legacy paths.

Slice scorecard and migration math

Migration sliceGood candidateBad candidate
Catalog read APIClear contract, easy parity checks, reversible routingHidden side effects or mixed write ownership
Checkout workflowOnly after payments, inventory, and idempotency rules are explicitShared transactions and unclear rollback behavior
Reporting/export pathTolerance for temporary drift; async validation possibleBusiness-critical correctness with no parallel audit
Parity drain check: if the new path handles 1,200 requests/sec and shadow validation replays 900 requests/sec, a 9M-request backlog drains in roughly 9,000,000 / (1,200 - 900) ≈ 30,000 seconds, or about 8.3 hours. That math tells you whether nightly parity is realistic before cutover.

Anti-corruption boundary

Translate legacy customer codes, error semantics, dates, and side effects at one versioned adapter. The new service owns its domain language; callers do not spread legacy shapes into the replacement model.

The dual-write trap

Writing two databases from one request creates an ambiguous partial commit. Keep one writer and publish through an outbox/CDC stream, or make the second path explicitly rebuildable and reconcile every gap.

Cutover gate and rollback contract

Promote 1% → 5% → 25% → 100% only when semantic mismatch, p99, error rate, saturation, and business totals remain inside declared thresholds for a full observation window. Once the new path accepts writes, rollback must replay them into the legacy path or leave legacy read-only: traffic reversal alone is not recovery.

Trade-offs

Nearest alternative

Modular monolith extract (library boundaries first) when the risk is code hygiene, not existential cutover risk. Branch-by-abstraction inside one deploy unit is cheaper than a full facade when data already has clear ownership.

What you pay

Dual-run cost = legacy + candidate + sync + observability for months. Facade becomes a critical SPOF for routing truth: version and canary the gateway itself.

Exercises

  1. At 40k read QPS you want 10% shadow. How many extra candidate QPS do you provision, and what stop condition freezes the mirror if CDC lag exceeds 30s?
  2. Checkout canary is at 5%. A payment webhook only hits the candidate. Design the rollback contract so flipping the flag cannot double-charge.
  3. Pick catalog-read vs checkout as the first seam. Defend the choice with parity measurability and write-ownership risk, not team preference.

Failure timeline: dual-write and CDC lag at cutover

Even with a facade, dual-write or CDC lag can leave two truths during the window when both paths still matter.

T+0      Cutover: candidate writes orders; CDC streams to legacy read models
T+2m     CDC lag hits 90s under load; shadow parity still green on sampled traffic
T+3m     Support lookup hits legacy replica → order O-441 missing
T+3m+8s  User retries checkout; candidate already charged; idempotency key not shared
T+6m     Ops flips facade back to legacy without replaying candidate commits
Outcome  Dual-write / lag gap: user-visible state diverges; refunds become archaeology

Primary references

Topic sources for incremental migration, not generic system-design books.

Takeaway

Strangle by reversible slices with a facade, shadow parity, and canary ramps. Then delete the old path on purpose. Hold the single-writer invariant; never roll back writes with a flag flip alone.

Practice and further reading

Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.

Focus for this lesson: Migrate one business slice at a time through an anti-corruption layer, observable routing gates, data ownership transitions, rollback, and reconciliation.

Explain the decision

What does an anti-corruption layer do in a strangler migration?

Read the reasoning

It translates legacy models and semantics at the boundary so the new domain is not forced to reproduce legacy coupling and naming.

Apply it to a production scenario

Shadow reads differ by 0.8% after moving customer profiles. Can traffic cut over?

Read one approach

Classify differences, define a correctness threshold by field, repair data and translation bugs, then canary only after unexplained mismatches are below the gate.

Work through the numbers

Shadowing 25% of 40,000 reads/s to a new path adds how much request load?

Read the worked calculation

10,000 reads/s, plus downstream and egress amplification; shadow capacity must be isolated from serving capacity.

Reason through a failure

What must a rollback preserve after the new service has accepted writes?

Read the recovery reasoning

The legacy path must receive reconciled, ordered state or remain read-only; routing rollback cannot silently discard writes made under new ownership.