Evolvability
A system is evolvable when the changes it is actually likely to receive stay local and understandable. The word "likely" carries the entire claim.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
Two systems both work today and both have tests. What makes one of them still cheap to change in three years?
Leadership wants feature delivery in year three to cost roughly what it cost in year one. Nobody is asking for a rewrite; they are asking why a ticket of the same size now takes four times as long.
Make everything flexible. Put interfaces at every seam, configuration in front of every constant, and events instead of calls, so that whatever gets asked for next, we can absorb it.
Flexibility is directional. An interface buys the freedom to swap implementations and buys nothing at all when the change is "add a field that must flow end to end" — which is the change that actually arrives most often.
- Flexibility is directional. An interface buys the freedom to swap implementations and buys nothing at all when the change is "add a field that must flow end to end" — which is the change that actually arrives most often.
- Universal flexibility has a universal cost: every reader traverses indirection on every path, including the ninety percent of paths where nothing ever varied (What an Abstraction Costs).
- Machinery built for unobserved variation gets in the way of the observed kind. When the real change turns out to be on a different axis, it has to be threaded through a structure designed for something else, which is worse than having had no structure (Speculative Generality).
- The naive version cannot lose an argument, and that is the tell. "This will make us flexible" survives any outcome; "this makes adding a second payment provider a one-module change" can be shown to be wrong.
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- The system is live and earning money, so evolvability has to be improved while shipping, never as a separate project.
- Nobody has a roadmap past the next two quarters, so "the changes we expect" has to be inferred from history rather than read off a plan.
- Team turnover is real: roughly a third of the people who made the original decisions have left, so any property that depends on remembered intent has already decayed.
- Behaviour visible to customers does not change while structure does — otherwise the improvement is indistinguishable from a regression (What Refactoring Actually Is).
- Every structural claim must name a change it makes cheaper. A claim that names none cannot be checked and cannot be wrong.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Whoever proposes a structure owns naming the specific requirement change it makes cheap, and the probability they attach to it.
- The team owns the evidence: which changes actually arrived over the last year, and which modules they landed in.
- Each module owns absorbing changes on its own axis; nothing owns absorbing every change, and pretending otherwise is where god objects come from (God Object).
- The useful boundary is the one a likely change lands inside. That is a claim about the domain, not about layering (Finding Seams).
- A boundary that no anticipated change respects is pure cost: it splits a thing that always moves together, so every change crosses it (Over-Decomposition).
- Boundaries chosen by rate of change beat boundaries chosen by technical type, because rate of change is what determines whether the boundary contains anything (Stability and Dependency Direction).
Evolvable for which change?
The question "is this design evolvable" has no answer. The question "does this design make a new tax jurisdiction a one-module change" does, and it can come back no. Any time someone argues for structure without completing the sentence, the argument cannot be evaluated — which is usually why the sentence is left incomplete.
The table below prices four ordinary requirement changes against two ordinary designs. Neither design wins every row, and that is the point: a design is a bet on which rows you will be asked for, and the bet has a losing side that is rarely mentioned when the design is proposed.
- Two of the four rows go the other way, which means the honest pitch for a capability split is "we expect rows two and one more often than rows three and four", not "it is better structured".
- The evidence for that expectation is available: it is in your merge history, and it takes an afternoon to extract (Change Amplification).
- Most real systems end up mixed — capability modules over a thin shared technical layer — precisely because the rows disagree (The Modular Monolith).
| Requirement change | Layered by technical type | Split by business capability | Which wins, and why |
|---|---|---|---|
| Add a field that must be captured, stored, shown and exported | Touches every layer, but each edit is obvious and mechanical | Touches one capability, plus its contract with anything downstream | Capability split, narrowly. The layered version is more edits but lower risk per edit — this is the row where the gap is smallest. |
| Add a second tax jurisdiction | Tax logic is spread across services and controllers; the edit is a search-and-hope | Lands inside the pricing capability, if the rule was given one home | Capability split, decisively. This is the change the split was bought for. |
| Swap the ORM | One layer, by construction; this is what layering is genuinely good at | Every capability owns its own persistence, so the change is repeated N times | Layers, decisively. The capability split actively loses here and nobody mentions it in the pitch. |
| Add request tracing to every inbound call | One place, in the layer everything passes through | Either a shared concern everyone must adopt, or N copies | Layers. Cross-cutting technical concerns are what technical layers are for (Separation of Concerns). |
Where "likely" is supposed to come from
The weakest link in every evolvability argument is the probability. Teams state it with total confidence and derive it from nothing — usually from what the proposer finds interesting, or from what a conference talk said mattered at a company with a different product.
There are four real sources and they differ enormously in quality. Ranking them explicitly is worth doing, because the most-used source is the worst one.
What is the evidence that this axis of variation is the one that will actually vary?
when The merge history shows this rule changed nine times in a year, and the same six files moved each time.
cost The strongest evidence available and the cheapest to gather. Its limit is that it describes the phase the product was in, which may be over — a module that churned during launch may simply be finished.
when Tax rules vary by jurisdiction; payment methods vary by market; retention periods vary by regulator. The variation is a property of the world, not of your roadmap.
cost Nearly as strong and it survives phase changes. The trap is treating a domain textbook as evidence about *your* business, which serves one country and may always serve one country.
when A signed contract requires a second payment provider by Q3.
cost Strong but brittle: one decision away from being false, and structures built on it outlive the commitment that justified them. Write the revisit trigger the same day (Revisit Triggers).
when Nobody can point to a past instance, a domain reason or a commitment.
cost Not evidence, and the source behind most speculative structure. The correct response is to wait for the second real case, when the shape of the variation can be seen rather than guessed (The Rule of Three).
The three properties that decide it
Evolvability is not primitive; it decomposes into three things that can be looked at separately, and the rest of this module takes them one at a time. How many places must move for one requirement. How many places notice when one implementation changes. And how much you must hold in your head to change any of them safely.
They are not independent — hiding a decision usually lowers amplification too — but they fail differently, and a system can be excellent at one and hopeless at another. A codebase with perfect information hiding and forty tiny modules has a small radius and appalling cognitive load, and it is not evolvable.
- Change amplification — one requirement, how many modules, interfaces, tests and deploys (Change Amplification).
- Encapsulation radius — change this implementation, and who has to be told (Encapsulation Radius).
- Local reasoning — what you must hold in your head to make the edit safely (Local Reasoning).
- Lowering all three at once is possible and it is not free: it is bought with indirection, coordination, and the risk of drawing a boundary in the wrong place.
How to build it
Most important first.
- Start from evidence, not intuition. Read the last year of merged changes and ask which files moved together — that co-change set is the empirical shape of your likely change, and it usually surprises people (Change Amplification).
- Name the axis. Write down the two or three kinds of change you expect — "new tax jurisdiction", "new notification channel", "new report" — and design so each lands in one module.
- Reduce the number of places that must move for one of those. That number is the thing being optimised; everything else in this module is a technique for lowering it (Change Amplification).
- Reduce who notices when an implementation changes, so that internal improvement does not become a cross-team negotiation (Encapsulation Radius).
- Keep the code understandable at the point of change. A one-module change is not cheap if nobody can tell what that module does (Local Reasoning).
- Accept that changes off your named axes will be expensive. Write that down too, so a future engineer knows it was a decision rather than an oversight.
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- The next change on a named axis costs one module, its tests, and one integration check — bounded, and roughly constant as the system grows.
- The next change off every named axis costs what it always did, or slightly more, because it now crosses the boundaries you introduced. This is the part left out of the pitch.
- The cost of *finding* where a change goes falls fastest and is least discussed: on a system with named axes, deciding where the code goes takes minutes rather than a design meeting.
- The cost of being wrong about the axis is not just wasted structure — it is that the structure must be dismantled under delivery pressure, which is when teams instead work around it and the system acquires two competing shapes.
- Designing for named changes means explicitly accepting that unnamed changes are more expensive. That is a real, chosen cost, not a technicality.
- The evidence-gathering is work with no shippable output, so it is the first thing cut under pressure.
- Boundaries drawn for change locality often cut against boundaries drawn for performance or for transactional consistency, and something has to give (Consistency Boundaries).
What can go wrong
- The named axis is wrong. The system absorbs the change nobody asked for and resists the one that arrives every sprint.
- The evidence is read as prophecy. Last year changed pricing constantly because pricing was being built; that phase is over and the co-change data is now describing the past.
- Evolvability becomes a standing justification for structure, and the team stops asking which change each piece makes cheap — which is the naive failure again, arrived at slowly.
- The mitigation fails on its own terms: the team measures amplification, sees a high number, and lowers it by merging modules that had genuinely different reasons to change (Divergent Change).
- Evolvability depends on being able to tell what broke. Without a suite that fails when behaviour moves, every structural improvement is an unverified rewrite (Refactoring Without Tests).
- It depends on release cadence: a design you cannot ship in slices cannot be improved incrementally, so delivery mechanics constrain structure (Incremental Migration).
- It couples to organisational shape — a boundary two teams share becomes a negotiation, whatever the code says (Code Ownership).
- "Evolvable means flexible." Flexibility is one technique and usually the wrong one. A rigid design where the likely change is a one-line edit is more evolvable than a configurable one where it is a redesign.
- "So we should predict the future." No — read the past and be explicit about the extrapolation. The claim is not that you know what is coming; it is that guessing silently is worse than guessing out loud (Design for the Known, Name What You Assumed).
- "This is just good design." It is a specific and falsifiable version of it: name the change, count the modules, price the cost. General good design cannot be checked against anything.
- "Evolvability requires microservices." Service boundaries are deployment and scaling boundaries that happen to also be code boundaries, and they make cross-cutting change dramatically *more* expensive. Choosing them for evolvability alone usually loses (The Modular Monolith).
Testing it, and how it ages
- Test at the boundary you claim is stable, so the tests survive the internal changes the boundary exists to allow (What a Unit Is).
- Before restructuring, characterize the behaviour you are promising not to change — otherwise "no behaviour change" is a hope (Characterization Tests).
- A useful design test: pick a change from the named axis and actually make it on a branch. If it touches five modules, the design does not do what the argument claimed.
- Named axes expire. The axis that mattered while the product was finding its market is rarely the one that matters once it has one, and nobody sends a notification when it flips (Revisit Triggers).
- As a system matures the dominant change kind usually shifts from "new capability" to "new variant of an existing capability", which moves pressure from decomposition to configuration and data modelling.
- Eventually some boundary starts being crossed by most changes. That is the signal to move it, and it is visible in the co-change data long before anyone complains.
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThat change cost is a function of how many places must move, and that this is decided by structure rather than by tidiness, holds across languages, paradigms and system sizes — what varies is which technique lowers the count.
- LIFETIME-SPECIFICFor code with a known end date — a migration tool, a launch campaign, a research prototype — evolvability is worth close to nothing and the correct design is the fastest one to write. The argument here assumes the system must keep absorbing requirements for years; applied to six-week code it is straightforward waste.
- CONTESTEDA serious opposing position holds that predicting likely change is so unreliable that the only defensible strategy is to keep code small and direct and rewrite modules when requirements move — that simplicity, not anticipation, is what makes systems evolvable. It has real support: teams that name axes are frequently wrong, and small direct code is genuinely cheap to replace. The counter is that rewrite-on-change works while modules are small and one team owns them, and stops the moment a module has several dependents who must be migrated with it.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — System Design — the same question at the grain of services, where "how many places must move" becomes "how many deployments must be coordinated", and the answer is usually much worse.
- — Testing & Reliability Engineering — every claim here assumes you can detect an unintended behaviour change, which is a confidence problem this domain leans on rather than solves.