Lesson 0035 · Master Designer · Module 3
Stability vs Speed of Change
Stable systems resist change; fast systems court it. The master's toolkit — feature flags, strangler fig, branch by abstraction — exists to buy both at once.
Mission tie-in: the first Evolution Trade-Off — where the techniques from lessons 0009 and 0025 become instruments of organizational speed.
Knowledge: speed is a design property
The naive framing: stable or fast, pick one. The reframe: change speed is a property of the design, not a personality. A system changes fast when changes are small, local, and reversible (Phase I's whole point). A system changes slowly when every change is big, cross-cutting, and risky.
The two forces are stability — the system keeps the promises it already made — and change speed — how fast a new fact about the world can reach production. The unit of decision is the change: one change, one instrument.
The reason the trade dissolves is that both forces are downstream of a single variable: batch size. Risk per release does not grow linearly with the number of changes in it, because changes interact and because diagnosis cost grows with the size of the set you must bisect. Halve the batch and you have both reduced the chance of a fault and shortened the time to find the one you get — safer and faster from the same move. Every instrument in this lesson is a way of making the deployed batch smaller than the unit of work: a flag separates deploy from release, a seam separates structure from behaviour, expand/contract separates a schema change from a code change.
Underneath the instruments sits one hard fact of modern deployment: during any rolling release, two versions of your code are live at the same time. That is not an edge case to handle, it is the normal state for several minutes of every deploy, and it is the reason each step must be compatible with the step before it. A change that is only correct once every instance has it is a change with a guaranteed broken window.
The mechanism has an edge. Machinery is not free: a flag is a permanent branch, and twelve live boolean flags describe 4,096 configurations of which your test suite exercises perhaps two. So the rule stops paying when the plumbing outlives the transition, and when the change is too small or too atomic to decompose — at which point the safest thing really is one careful deploy.
The three instruments that buy both stability and speed:
- Feature flags — ship the code dark; the behavior's visibility, not its presence, is the deploy. Roll back by flipping a flag, not reverting a release.
- Strangler fig (lesson reference: patterns sheet) — replace a system incrementally: new behavior routes to the new implementation; the old one shrinks slice by slice. The system stays stable while it's being replaced.
- Branch by abstraction — insert an interface over a component, migrate all users to it, swap the implementation behind it. Every step is a small, mergeable, reviewable change.
# feature flag: the code is in production; the behavior waits
def checkout(cart: Cart, flags: FeatureFlags) -> Order:
if flags.is_enabled("new_pricing"):
return new_pricing_checkout(cart) # dark: no users see it yet
return legacy_checkout(cart)
# branch by abstraction: every consumer migrates to the seam first
class StockService(Protocol): # the seam
def reserve(self, sku: str, qty: int) -> None: ...
# then: legacy impl swapped for new impl behind the same seam
The honesty clause: flags and seams are machinery — each carries complexity (lesson 0029's knob cost). A flag that lives forever is config debt; a seam for a component that never changes is indirection. The master removes the machinery when the transition is done.
There is a fourth instrument, for the change the other three cannot touch — data:
# expand / contract: never rename a column in one step
# 1. expand add `delivery_address`; write BOTH; keep reading `address`
# 2. backfill copy the historical rows, in batches, idempotently
# 3. switch read `delivery_address`; still writing both
# 4. contract stop writing `address`; only then drop it
Four deploys instead of one, and every single one is reversible. The one-step rename is the change that looks fastest and is the only one you cannot roll back once the first write lands — the exact profile lesson 0037 calls irreversible.
Worked: why the one-step rename is the expensive one
The returns service stores a shipping address in address; a new carrier integration needs it split out as delivery_address. The table holds 4.2 million rows. An engineer proposes the obvious thing: a migration that renames the column, deployed with the code that uses the new name.
The first question a lead asks is not "how long does the migration take?" but "what is running while it runs?" Deploys roll: six instances, roughly thirty seconds each, so for about three minutes half the fleet writes address and half writes delivery_address. Even at two requests a minute, that is a handful of returns whose address lands in a column the other half of the fleet is no longer reading — and you will not find them now. You will find them weeks later, by their absence, when a warehouse agent cannot print a label.
The second question is what an undo looks like. The four-step version is reversible at every step: stop the rollout, and the old column is still there, still written to. The one-step rename is reversible only until the first new write lands, after which restoring means a backup restore and reconciliation of everything that happened in between — the change has quietly moved from lesson 0037's reversible class into the irreversible one, in exchange for saving three deploys.
The alternative the lead rejects is the compromise that feels responsible: a 3 a.m. maintenance window. Price it. Backfilling 4.2 million rows in batches of 5,000 at about two seconds a batch is 840 batches, roughly 28 minutes — before verification, and before anything goes wrong. So the window is either too short to finish or long enough that its own failure is the incident. The maintenance window buys the illusion of atomicity and charges for it in the one currency that matters at 3 a.m.: nobody rested is making the rollback decision.
The four-step version is three more deploys and a backfill script, perhaps a day of work spread across a week, during which the service never stops being correct. A year later the difference is cultural rather than technical: teams that have done expand/contract twice stop treating schema changes as events, and schema changes stop being the reason a release is delayed to a Thursday.
† The un-safety of the alternative: a big-bang rewrite is the speed play that buys neither — the system is unstable for months and changes slower (every migration conflict lands in one pile). Stability and speed are both served by small reversible steps.| The change | The instrument | Remove it when… |
|---|---|---|
| a feature not ready for users | feature flag | The day it is on for 100% of traffic. |
| swapping a component with 40 callers | branch by abstraction | When the last caller uses the seam and the old implementation is deleted. |
| replacing a whole legacy system | strangler fig | When the last route stops going to the old system. |
| renaming or dropping a column | expand / contract | After the read path has stopped using the old column. |
- "A feature flag means we can ship without testing it."
- A flag adds a branch; it removes none. You now owe tests for both sides and, more importantly, for the transition — the request that starts on the old path and finishes on the new one. Untested flags convert a release risk into a runtime risk and call it progress.
- "We can always roll back, so it is reversible."
- Reverting code does not revert consequences. Emails have been sent, webhooks fired, rows written in the new shape, downstream systems have already believed you. Reversibility is a property of the whole change including its side effects, not of your deployment tool.
- "Long-lived branches are safer than unfinished code in main."
- They trade a visible risk for an invisible one. The unmerged branch accumulates integration debt that arrives all at once, at the worst possible moment, in the largest possible batch — the exact shape this lesson exists to avoid.
- "The strangler fig is a microservices pattern."
- It is an incremental-replacement pattern and works just as well inside one process: route calls through a seam, move one behaviour at a time, delete the old branch when the last caller stops using it. No network required.
When this is the wrong move
Slicing is not free, and there are changes where the machinery costs more than the risk it removes. A one-line copy fix behind a flag is theatre. A two-week migration that needs forty hours of flag plumbing has spent its own budget. And a rewrite is occasionally the honest answer — not when the code is ugly, but when the constraint is not code at all: a platform going end-of-life, a licence expiring, a compliance regime the old design cannot satisfy, or a domain that changed so much that preserving the old behaviour is the mistake rather than the goal.
The other counter-case is self-inflicted. A codebase with thirty long-lived flags is not agile; it is a configuration graveyard where no two environments run the same program and no one can say what production actually does. When flags outnumber transitions, the instrument has become the instability, and the fix is a deletion sprint rather than another flag.
In the lab: the companion Phase 3 exercise asks you to change the shape of a field the returns service already persists, without a maintenance window. You plan the expand, backfill, switch and contract steps, and demonstrate that stopping after any one of them leaves the service correct.
Skill: which instrument, when?
You want to ship code that isn't visible to users yet. Choose:
A one-step column rename breaks during a rolling deploy because:
A permanent flag is best treated as:
Practice on your own code
Find a change you're about to make that feels "big". Split it: what's the smallest invisible slice (flag), the smallest structural seam (branch by abstraction), or the smallest routed slice (strangler)? Write the slice list — then do the first slice.
Reveal: a sliced transition
Replacing a payments provider: (1) insert the PaymentGateway seam (already there from lesson 0009's ports — the seam was the whole point); (2) migrate callers to it behind a flag that still uses the old provider; (3) swap the adapter, flag on for 1% of traffic; (4) 100%, remove the flag. Four slices, each mergeable, each reversible.
- In two sentences, explain to a junior why smaller deploys are simultaneously safer and faster rather than a trade between the two.
- Find one change your team is currently calling "big". Name what makes it big — a schema, a shared component, an unslice-able behaviour — and what one incident during it would cost in hours and customer contacts.
- Predict the one-year state if the machinery is never removed: how many live flags will exist, and what will the team say when asked what production is actually running?
Your win
You can slice any big change into small, reversible, mergeable steps using flags, seams, and strangler routing — and you know to remove the machinery when the transition ends.
Read and watch deeper
- "Strangler Fig Application", Fowler — the incremental-replacement pattern.
- "Technical Debt", Fowler — the loan framing for what flags and shortcuts accumulate.
- Working Effectively with Legacy Code, Feathers — ch. 1–2: changing software without breaking it, slice by slice.
- Monolith to Microservices, Newman — ch. 3: strangler fig, branch by abstraction and parallel run, with their failure modes.
- Accelerate, Forsgren, Humble & Kim — ch. 2 and 4: the measured relationship between small batches, deploy frequency and change-failure rate.
- Next: lesson 0036 — Isolation vs Integration.
- Reference: Trade-offs — stability vs speed of change; Patterns — strangler fig, branch by abstraction, expand / contract, feature flag.
Bring your big change to your agent-teacher and slice it together before writing code.