Migrating a legacy system means replacing an old monolith, mainframe or core platform while the business keeps running on it. The strangler fig pattern puts a routing layer in front of the old system and moves one capability at a time to new code until the old system has nothing left to do. The anti-corruption layer is the translator at the border, so the new system never inherits the old one’s codes and quirks. Together they turn a risky multi-year rewrite into many small, reversible steps.
Picture it first: rebuilding a junction station
A busy junction station needs rebuilding, but it cannot close for two years. So the railway builds one new platform beside the old ones, and the announcer starts sending one route’s trains there. Passengers just follow the announcements. After a quiet month, the next route moves. Temporary footbridges link old and new platforms, and one day the last old platform sees no trains and is demolished.
The old platforms also carry decades of odd signage (“4A old side”, “goods loop 2”). Rather than copy it onto the new platforms, the railway posts an interpreter at the footbridge.
Map it back: the station is your legacy system, platforms are capabilities, the announcer is the routing façade, the footbridges are transitional architecture, and the interpreter is the anti-corruption layer. The station never closes, and if a new platform misbehaves, trains go back to the old one.
Why big-bang rewrites fail
The plan sounds simple: build a new system that does what the old one does, in better technology, then switch over. Martin Fowler writes that he has seen it “go down in flames most of the time”. Four forces work against it:
- The target moves. If the legacy team ships 15 changes a month (illustrative), a three-year rewrite must also absorb
15 × 36 = 540of them, or the business must freeze. Ian Cartwright, Rob Horn and James Lewis describe an organisation whose 3–5-year programmes were each overtaken by changing needs and restarted, with no legacy ever switched off. - Feature parity is a trap. Nobody knows everything the old system does: the batch job that patches yesterday’s data, the workaround that became a process. Fowler’s 2004 post notes that “even old bugs often need to be added”. And much old behaviour is no longer wanted, so rebuilding it is waste.
- Years without value. Money flows out for years before any user benefits, an easy target when budgets tighten.
- One cutover weekend. All the risk lands on one date, with no cheap way back: after a day of bookings on the new system, the old one is a day out of date.
The incremental route has the opposite shape: value within months, and risk spread over dozens of small moves, each reversible.
The strangler fig idea
In the Queensland rainforest in 2001, Fowler saw strangler figs: vines that germinate high in a host tree, grow down to the soil and eventually stand on their own, while the host may die. In June 2004 he posted “Strangler Application”, comparing them to rewriting a system gradually around the edges of the old one. In 2019 he renamed it “Strangler Fig Application”, since “strangler” alone sounded violent, and in 2024 he rewrote it.
The rewrite leans on four activities from Cartwright, Horn and Lewis’s Patterns of Legacy Displacement: understand the outcomes you want, break the problem into parts, deliver the parts, and change the organisation so this keeps happening. Outcomes decide the first move: “end the mainframe contract by 2028” and “survive the next tatkal rush” start in different places.
Expect to build things you throw away. Routers, adapters and sync jobs that exist only so old and new can coexist are transitional architecture: scaffolding you will pay for, as the authors bluntly put it, and must remove at the end. In return, cost and value arrive gradually.
How the strangling works
Four moves, repeated for each capability:
- Put a façade in front. An API gateway or reverse proxy sits between clients and the legacy system. Its first release routes 100% of traffic to legacy, which proves the extra hop is safe.
- Build one capability on the new side, with its own code, data and team.
- Shift traffic gradually, by route, segment or percentage, behind a flag you can flip back in seconds.
- Retire the legacy piece once its traffic has stayed at zero: delete its code, tables and jobs.
When nothing is left behind it, Azure’s guidance adds a final step: remove the façade too, or keep it as an adapter for clients you cannot update.
The routing table is the migration plan made executable. At stage 2 it might look like this (illustrative syntax):
# First match wins; anything unmatched goes to the monolith.
routes:
- match: { path: /api/trains/search }
to: search-service # carved: 100%
- match: { path: /api/pnr/* }
split: { pnr-service: 25, legacy: 75 } # ramping, hashed on user ID
fallback_on_error: legacy # GET only
- match: { path: /api/refunds/*, header: { x-user-segment: staff } }
to: refund-service # staff first, then everyone
- match: { path: /* }
to: legacy-monolith
fallback_on_error retries a failed read on legacy, hiding early bugs from users. Never fall back on a write: if the new refund service committed and then timed out, re-sending it to legacy pays the customer twice. Writes need one owner and an idempotency key.
Seams to cut along
A seam is a place where you can change which code runs without editing the code around it. Cut along:
- Routes:
/trains/searchis trivial to route, if there is an HTTP front door. - Segments or product lines: staff, then one city or quota. In Cartwright and colleagues’ middleware example, a router moved single products by ID, then whole product types, until the old middleware was switched off.
- Business capabilities: search, PNR status, refunds and notifications, which map onto teams; see finding service boundaries with DDD.
- Events and messages, when the legacy system talks through queues, files or database writes.
Event interception
A bank’s core system may have no HTTP front door at all: terminal transactions, nightly files, an old message queue. Event interception puts a component in that path: a content-based router on the queue, a proxy on internal HTTP, a script in the legacy web pages that redirects one form, or, as a last resort, a trigger or change data capture (CDC) feed from the database. Each event can then go to legacy, to the new system, or to both.
A case study on the Event Interception page moved one domain in four stages: dark-launch new services fed by CDC until reconciliation showed 100% parity; redirect reads; redirect writes, making the new services the system of record; only then move the business logic.
Branch by abstraction: seams inside the code
When the seam must be built inside one codebase, say a dozen modules call a mainframe ledger client directly, use branch by abstraction, a name Paul Hammant introduced (he credits Stacy Curl): put an interface in front of the old supplier, move every caller onto it, add a new implementation behind a flag, then delete the old one. The main branch releases at every step, so there is no long-lived branch to merge.
The anti-corruption layer
Eric Evans named the anti-corruption layer (ACL) in his 2003 book Domain-Driven Design. Its intent, in his words: “Create an isolating layer to provide clients with functionality in terms of their own domain model.” It calls the other system through that system’s existing interface, so the other side barely changes, and it translates in both directions.
You need one because old and new are different bounded contexts, and the legacy side carries decades of encoding tricks. A hypothetical railway booking monolith might store status as one string (CNF, RAC/12, WL/017), dates as DDMMYYYY, fares as rupees in text, and tatkal bookings as a field called FLG1. A bank’s core might pack branch, product and currency into one ledger account string. Model those as they arrive and your new system is legacy on day one.
A domain polluted by legacy fields
Here is a booking service that skipped the ACL:
// Before: the new service models the legacy record as it arrives
interface Booking {
pnrNo: string;
pnrStat: string; // "CNF", "RAC/12", "WL/017", "CAN"
clsCd: string; // "3A", "SL", "2S", ...
jrnyDt: string; // "27102026", i.e. DDMMYYYY
fareAmt: string; // "1245.50": rupees, as text
flg1: string; // "Y" means tatkal (undocumented)
}
// ...and every caller decodes the codes again, slightly differently
const waitlisted = b.pnrStat.startsWith("WL");
const position = parseInt(b.pnrStat.split("/")[1]);
The same service with an ACL: the domain uses the business’s words, and one translator is the only code that knows the legacy codes.
// domain/booking.ts: the new model, in the business's own words
export type BookingStatus =
| { kind: "confirmed"; coach: string; berth: number }
| { kind: "rac"; position: number }
| { kind: "waitlisted"; position: number }
| { kind: "cancelled" };
export type TravelClass = "ac-first" | "ac-2-tier" | "ac-3-tier" | "sleeper" | "second-sitting";
export interface Booking {
reference: string;
status: BookingStatus;
travelClass: TravelClass;
journeyDate: string; // ISO 8601, e.g. "2026-10-27"
fare: { paise: number }; // integer money, never floats
quota: "general" | "tatkal";
}
// acl/legacy-pnr-translator.ts: the only file that knows legacy codes
import type { Booking, BookingStatus, TravelClass } from "../domain/booking";
export interface LegacyPnrRecord {
PNR_NO: string; PNR_STAT: string; COACH_BERTH: string;
CLS_CD: string; JRNY_DT: string; FARE_AMT: string; FLG1: string;
}
export class TranslationError extends Error {
constructor(field: string, value: string) {
super(`cannot translate ${field}=${JSON.stringify(value)}`);
}
}
const CLASSES: Record<string, TravelClass> = {
"1A": "ac-first", "2A": "ac-2-tier", "3A": "ac-3-tier", SL: "sleeper", "2S": "second-sitting",
};
export function toBooking(r: LegacyPnrRecord): Booking {
const travelClass = CLASSES[r.CLS_CD.trim()];
if (!travelClass) throw new TranslationError("CLS_CD", r.CLS_CD);
const date = /^(\d{2})(\d{2})(\d{4})$/.exec(r.JRNY_DT.trim());
if (!date) throw new TranslationError("JRNY_DT", r.JRNY_DT);
const fare = /^(\d+)\.(\d{2})$/.exec(r.FARE_AMT.trim());
if (!fare) throw new TranslationError("FARE_AMT", r.FARE_AMT);
return {
reference: r.PNR_NO.trim(),
status: toStatus(r.PNR_STAT.trim(), r.COACH_BERTH.trim()),
travelClass,
journeyDate: `${date[3]}-${date[2]}-${date[1]}`,
fare: { paise: Number(fare[1]) * 100 + Number(fare[2]) },
quota: r.FLG1 === "Y" ? "tatkal" : "general",
};
}
function toStatus(code: string, coachBerth: string): BookingStatus {
if (code === "CAN") return { kind: "cancelled" };
if (code === "CNF") {
const seat = /^([A-Z]+\d*)\/(\d+)$/.exec(coachBerth);
if (!seat) throw new TranslationError("COACH_BERTH", coachBerth);
return { kind: "confirmed", coach: seat[1], berth: Number(seat[2]) };
}
const queued = /^(RAC|WL)\/(\d+)$/.exec(code);
if (!queued) throw new TranslationError("PNR_STAT", code);
const position = Number(queued[2]);
return queued[1] === "RAC" ? { kind: "rac", position } : { kind: "waitlisted", position };
}
Make the translator fail loudly on values it has never seen, and graph those failures: a guess here becomes silent corruption. Translate, don’t decide: Azure’s guidance keeps business rules and orchestration out of the layer. And test it with recorded legacy records, because the odd ones are the point.
Where the ACL lives
| Placement | What you gain | What you pay |
|---|---|---|
| A module inside the new service | No extra hop; ships and scales with the service | Every service that talks to legacy writes its own |
| A separate translation service | One team owns the legacy knowledge | Another hop and on-call; a possible shared bottleneck |
| Mapping rules in the façade | Neither system changes | Domain logic hidden in gateway config |
| A facade class inside the legacy code | Unmigrated modules call the new service unaware | You change legacy code, and must remove the class later |
Azure lists the running costs: extra latency on every call between the systems, and one more component to deploy, monitor and scale. Give it correlation IDs and logs of translation failures. It lives exactly as long as the legacy piece behind it. If that piece stays for good, the ACL becomes a permanent, owned component. That is fine, as long as it is a decision, not an accident.
ACL or legacy mimic?
They point in opposite directions. An ACL protects the new model from the old one. A legacy mimic makes the new system look like the old one to legacy consumers, by emitting old-format messages or writing the old reporting tables. Cartwright and colleagues treat mimics as transitional, while an ACL in front of a system that stays can endure.
Data during the migration
Moving code is the easy half; the legacy database is shared by dozens of modules, reports and batch jobs. One rule keeps you out of trouble: for each piece of data, exactly one system accepts writes at any moment. The other side only receives copies.
For one capability, say refunds, ownership moves in three stages:
- Legacy owns; the new service reads a copy. CDC streams legacy changes through the translator into the new service’s store. Calling the legacy API through the ACL on every request is simpler, but ties the new service’s latency and availability to the old system.
- The new service owns; legacy gets a copy. At the flip, writes go to the new service. A back-sync, events from a transactional outbox applied to the old tables by a legacy mimic, keeps legacy reports and unmigrated modules working.
- The legacy copy is retired once nothing reads the old tables. Finding every reader is harder than it sounds: in Cartwright and colleagues’ example, products seemed to vanish from business-critical reports fed from the old tables by data-warehouse batch jobs nobody had listed.
The mechanics (dual writes versus CDC, backfills, shadow reads, the brief write freeze at the flip) are the same as for any move between stores; online schema and data migrations covers them. Two points are specific to legacy work.
Avoid two masters. If both systems accept writes for the same refund, they will conflict: legacy cancels it while the new service approves it, and whichever sync lands last silently wins. If both sides must write during the transition, split by key: refunds raised after the flip belong to the new service, older ones finish their life in legacy.
Reconcile continuously. Compare counts and checksums per day or key range, and send every mismatch to a queue with an owner. Small rates add up: at 3 million bookings a day, 0.01% drift is 3,000,000 × 0.0001 = 300 wrong bookings a day, each one a passenger at a counter.
Proving the new path behaves the same
Legacy systems are full of behaviour nobody wrote down, so specification tests miss it. Test against production traffic instead, without users seeing the results.
Parallel run. Run old and new on the same input, use the old answer, compare. GitHub’s Scientist library is the reference design: the old path is the control, the new one the candidate. Scientist runs both in random order, returns the control’s result, swallows any candidate exception and records mismatches. In a 2015 post, GitHub replaced the code behind its Merge button this way: 1% of requests first, four days of fixing mismatches and slow cases, then 24 hours at 100% with no mismatches across tens of millions of merges, and only then the switch.
Size the sample. At 2,000 requests a second, 1% sampling gives 20 comparisons a second, or 20 × 86,400 ≈ 1.7 million a day. A quirk that hits one request in 100,000 then appears about 1,728,000 ÷ 100,000 ≈ 17 times a day: enough to find and fix. Three rules keep the comparison useful:
- No side effects from the candidate: no SMS, no card charge, no real writes. On write paths, compare the decision (“refund ₹1,245.50 to this account”) instead of performing it.
- Normalise known noise (timestamps, ordering, number formats), or the mismatch graph drowns. GitHub had to skip runs where the old code timed out, and pass both paths the same commit time.
- Budget the capacity. At 100%, every compared request does the work twice.
Sometimes legacy is the one that’s wrong
Several of GitHub’s mismatches were bugs in the old code. A file with exactly 768 conflicts was reported as merged cleanly: the old script returned the conflict count as its exit code, and the shell keeps only the low 8 bits, so 768 = 3 × 256 became 0. Every mismatch needs a verdict: copy the old behaviour, or fix it and tell whoever depends on it.
Dark launch a whole new service by calling it without using its results, to test load and data sync. Then canary by segment (staff, one city, 1%, 10%), comparing errors and a business metric such as refunds per minute against legacy, with the gates of deployment strategies. Contract tests replay recorded request and response pairs against the new service in CI.
A way back at every step. A route flag off undoes a traffic shift; pointing reads at legacy undoes a read move; flipping writes back works only while the back-sync runs. Removing the back-sync is the point of no return, so do it last, after a long bake.
Choosing what to carve first
Pick a first slice that teaches the most for the least exposure:
- Read-only before read-write. A wrong search result costs a retry; a wrong booking costs a berth.
- Busy but low-stakes. In the illustrative mix of Fig. 1, carving search moves 70% of requests and no money.
- Changes often, so the move pays off with its first release.
- Clean seam, few dependencies. Notifications are often ideal: event-driven, and they write nothing back.
- A thin slice, end to end. One narrow route through every new piece (façade, ACL, CDC, pipeline, dashboards, on-call) proves the path for everything after it.
- New features on the new side, where Fowler’s fig starts, so the old system stops growing while you shrink it.
A bank modernising for UPI-era volumes might go: statements and balance enquiries, notifications, beneficiary management, payment initiation through an ACL to the core, and the ledger last, or never.
Don’t start with the ledger
The core (seat allocation in a booking system, the ledger in a bank) has the most coupling, the strictest consistency and the most to lose. Starting there makes your first slice your hardest, on an untested façade, sync and team. Move it last, or decide it stays behind an ACL.
| Big-bang rewrite | Strangler fig | Branch by abstraction | Lift-and-shift or replatform | |
|---|---|---|---|---|
| What moves | Everything, at one cutover | One capability at a time, behind a façade | One component in a codebase, behind an interface | The hosting, not the design |
| First value | At cutover, often years away | Weeks to months | Days to weeks per swap | Weeks |
| Risk | All of it, on one weekend | Spread over many small moves | Small, per call site | Mostly operational |
| Rollback | Hard: the old system is stale within hours | Flip a route back while data still syncs | Toggle back to the old implementation | Keep the old hosting until stable |
| What you gain | A clean slate; no transitional parts | Early value; can stop at any point | Works on the main branch | Leave a contract or failing hardware fast |
| What you pay | Feature-parity chase; no value for years | Façade, ACL, sync and two systems to run | Needs code access and tests | Same coupling and cost of change |
| Pick it when | The system is small, or truly frozen | A large, live system with interceptable traffic | Replacing a library, client or module | The goal is hosting cost or end-of-life hardware |
The organisation half
Cartwright, Horn and Lewis put technology at “at most only 50%” of the legacy problem; ways of working, team structure and leadership are the rest.
- Conway’s law. A system’s structure mirrors the communication structure of the organisation that builds it. One central team building the whole replacement will build a new monolith. Give each carved capability to a team that owns its service, data and ACL.
- Keep the legacy team in. They know the archaeology, such as what
FLG1really means. Pair them with the new teams and give them new work too. - Hold the target still. Agree a soft freeze on the capability being carved: legacy changes there wait, or are made on both sides.
- Measure progress in deletion. Track requests on the new system, routes carved (say 42 of 60), legacy modules, tables and jobs deleted, transitional parts still running, and the legacy bill. Traffic can reach 90% new while the mainframe invoice stays the same.
- Know when to stop. The last 10% is the hardest: the core, the year-end batch job, the admin screen five people use. If a leftover piece rarely changes and blocks no expensive switch-off, leave it behind an ACL for good, funded as a permanent component. A strangler migration that stops early has still paid back; a rewrite cancelled at 80% has not.
In the interview
Two sentences carry the most weight: “Every step is reversible: the façade can send a route back to legacy, and data keeps syncing until I remove the back-sync last.” And: “Each piece of data has exactly one writer at a time; the other side only gets copies.”
Going deeper: how migrations go wrong
The façade becomes a bottleneck, or a new monolith
Every request crosses the façade, so Azure warns it must not become a single point of failure or a bottleneck: run it like your most critical service. Keep it dumb, too: routing, authentication, rate limits. Once teams add “just one rule” of business logic, you are building the next monolith in gateway config.
Carving too fine: the distributed monolith
Six in-process calls become six network calls. At 2 ms each, in sequence, that adds 12 ms, and availability multiplies: six services that must all answer, each at 99.9%, give 0.999^6 ≈ 0.994, about six times the failure rate of one. If the new services must deploy together or share a database, you have a distributed monolith: every cost of distribution, none of the independence. Carve along business capabilities, not classes; see monolith vs microservices.
Chatty calls back into legacy
A hop inside one data centre costs a millisecond or two; chattiness costs far more. An order-history page that calls legacy once per order through the ACL, at 80 ms a call, takes 20 × 80 = 1,600 ms for 20 orders. Batch the calls, or read from a CDC-fed copy.
Transactions that span old and new
If the booking is created in the new service but payment is still captured by legacy, no transaction covers both. Model the flow as a saga: local steps with compensating actions (release the berth, refund the payment), an idempotency key on every step, and events sent through an outbox.
Sharing the legacy database
Pointing the new service straight at legacy tables is fast, but couples new code to the legacy schema (corruption through the back door) and freezes those tables for both systems. Allow it briefly, read-only, through views; otherwise use the ACL or a CDC copy. The monolithic persistence anti-pattern is where shared databases end up.
The migration that never finishes
The most common failure is not a bad cutover but stopping at 70%, then running two systems, two on-call rotations, two bills and a sync layer indefinitely. Cartwright and colleagues saw programmes end “unable to decommission any of their legacy stack”. The defences: a decommission date per capability, every transitional component tracked as debt with an owner, and progress reported in things deleted.
Cheat sheet
| Topic | Remember it as |
|---|---|
| Why not big bang | Moving target, feature-parity trap, years without value, one irreversible cutover |
| Strangler fig | Façade in front; carve one capability; shift traffic by flag; retire legacy at zero traffic |
| Seams | Route, segment or product, business capability; event interception when there is no HTTP front door |
| Inside one codebase | Branch by abstraction: interface, move callers, new implementation behind a flag, delete old |
| Anti-corruption layer | Evans, 2003: translate the legacy model into your own domain types, both ways, in one place |
| ACL costs | An extra hop, one more component, and permanence by accident |
| Data | One writer per piece of data; CDC into new, back-sync to legacy after the flip; reconcile |
| Verify | Parallel run (control vs candidate), dark launch, segment canary, contract tests |
| Carve first | Read-only, busy, low-stakes, changes often, clean seam; core ledger last or never |
| Measure | Traffic on new, routes carved, legacy code and tables deleted, legacy bill |
| Pitfalls | Fat façade, distributed monolith, chatty legacy calls, cross-system transactions, shared DB, never finishing |
How to say it in the interview
“I wouldn’t rewrite the booking monolith in one go: the business keeps changing it, and one cutover puts years of risk on a single weekend. I’d put the API gateway in front of it, routing everything to legacy, which gives me a seam. Then I’d carve out capabilities one at a time, starting with search: read-only, most of the traffic, no money. The new service gets an anti-corruption layer, so legacy codes never leak into its model, and a CDC-fed copy of the data. A parallel run compares old and new answers before I shift traffic by segment, behind a flag I can flip back. When a capability takes over its writes, a back-sync keeps the legacy tables current until nothing reads them. Seat allocation and payments move last.”
How do you choose the first thing to migrate? Read-only, busy, low-stakes, changes often, few dependencies, and a thin slice through the façade, ACL, data sync and monitoring. Never the ledger first.
Where does the anti-corruption layer live, and does it go away? Usually as an adapter module inside each new service; as a separate service when many callers share one legacy system. It is deleted with the legacy piece behind it. If that piece stays for good, the ACL becomes a permanent, owned component.
How do you keep data consistent while both systems run? One writer per piece of data at a time. Before the flip, legacy owns writes and CDC feeds a translated copy; after it, the new service owns writes and a back-sync updates legacy. Reconciliation compares counts and checksums, and flows that span both systems become sagas.
How do you know the new system is right before switching? A parallel run on sampled real traffic that returns the legacy answer and logs differences, as GitHub did with Scientist; then a canary by segment. Some mismatches will be legacy bugs, each needing a decision.
When wouldn’t you use a strangler fig? Azure’s list: when requests cannot be intercepted, when the system is small enough to replace whole, or when the old one must go quickly. If the goal is only to leave a data centre, a lift-and-shift may come first.
Where to go next
- Online schema and data migrations: dual writes vs CDC, backfills and the cutover.
- Deployment strategies: canaries, shadow traffic and feature flags for each traffic shift.
- API gateway: the usual home of the strangler façade.
- Finding service boundaries with DDD: bounded contexts, and where to cut.
- Monolith vs microservices: what you are migrating towards, and when not to.
- Change data capture and transactional outbox: moving data reliably between old and new.
- Two-phase commit vs sagas: flows that span both systems.
- Observability: tracing one request across façade, ACL and legacy.
References and further reading
- Strangler Fig Application (Martin Fowler, 2024)martinfowler.comWhy simple replacements usually fail, gradual modernisation from new features outwards, seams, transitional architecture and Conway’s law.
- Original Strangler Fig Application (Martin Fowler, 2004)martinfowler.comThe June 2004 post, the risks of the big cut-over date, “even old bugs” in rewrites, and the 2019 rename from “Strangler Application”.
- Patterns of Legacy Displacement (Cartwright, Horn and Lewis, martinfowler.com)martinfowler.comThe legacy replacement treadmill, the feature-parity trap, the four activities, and a worked middleware-removal example with routing by product.
- Transitional Architecture (Cartwright, Horn and Lewis)martinfowler.comScaffolding software that lets old and new coexist, why you will build things you throw away, and removing it afterwards.
- Event Interception (Cartwright, Horn and Lewis)martinfowler.comIntercepting messages, HTTP, page events and database writes, and a four-stage domain extraction using dark launch and CDC.
- Legacy Mimic (Cartwright, Horn and Lewis)martinfowler.comService-providing and service-consuming mimics, Evans’s statement of the anti-corruption layer’s intent, and why an ACL can outlive the migration.
- Branch By Abstraction (Martin Fowler, 2014)martinfowler.comReplacing a component inside one codebase behind an abstraction layer while the main branch keeps releasing.
- Strangler Fig pattern (Azure Architecture Center)learn.microsoft.comThe façade’s four phases, the bottleneck and single-point-of-failure risk, and when the pattern does not fit.
- Anti-Corruption Layer pattern (Azure Architecture Center)learn.microsoft.comTranslation between subsystems with different semantics, its latency and operating cost, permanent versus temporary layers, and keeping business rules out.
- Move Fast and Fix Things (GitHub engineering blog, 2015)github.blogReplacing GitHub’s merge code with Scientist experiments: control and candidate, 1% sampling, four days of fixes, and 24 hours at 100% with no mismatches.