Evolutionary Design
Architecture that changes in small, measured steps instead of betting everything on one big plan. After all, who can predict what the business will need in three years?
31 min read

On this page
- Introduction
- The Rewrite That Never Shipped
- What Evolutionary Architecture Actually Means
- Decide at the Last Responsible Moment
- Reversible and Irreversible Decisions
- Fitness Functions: Guardrails for Change
- Kinds of Fitness Functions
- How to Adopt Fitness Functions
- 1. Start from the characteristics that really matter
- 2. Make them run in the pipeline, not in a slide deck
- 3. Treat a failing fitness function as a conversation, not a wall
- 4. Include production signals
- Incremental Change with Feedback Loops
- Metrics That Guide Evolution
- Techniques for Changing a Running System
- 1. Strangler Fig
- 2. Branch by Abstraction
- 3. Feature Flags
- 4. Parallel Run
- Modularity and Low Coupling: The Real Enablers
- Architecture Decision Records: Remember Why
- YAGNI vs Designing for Change
- Technical Debt as a Conscious Tool
- Tradeoffs
- Short-Term Speed vs Structural Integrity
- The Cost of Keeping Options Open
- Tradeoffs with Reliability
- Tradeoffs with Security
- Tradeoffs with Cost Optimization
- Tradeoffs with Performance Efficiency
- Tradeoffs with Operational Excellence
- Conclusion
- Next Steps
Introduction
Every system you'll ever work on will change. New features, new regulations, new traffic patterns, a new CEO with a new strategy, a cloud provider that deprecates the service you built everything on. The only real question is whether your architecture was built to absorb change or to resist it.
Evolutionary Design is the principle that treats change as the normal state of software, not as an exception. Instead of trying to get the whole design right on day one, you design enough to start, build in small steps, measure what happens, and let real feedback guide the next decision. The architecture grows with the product, protected by automated checks that keep it from rotting along the way.
When a team ignores this principle, the symptoms show up in one of two opposite flavors, and sometimes both at the same time:
- Months spent on diagrams and specifications before a single line of code reaches a real user;
- Decisions locked in early, based on guesses, that nobody dares to revisit later;
- The opposite extreme: no design at all, where every sprint adds another shortcut until the codebase becomes a big ball of mud;
- Changes that should take a day taking a month, because everything is coupled to everything;
- Fear of deploying, so releases get bigger, rarer and scarier;
- The famous "let's rewrite it from scratch" meeting, held every two years or so;
- Nobody remembers why the system is the way it is, so nobody knows what's safe to change.
Yep, it's rare, but it happens all the time... Who hasn't inherited a system where changing a field on a screen required touching eleven projects and asking for permission from three teams?
Easy there, Junior! That's the dream of every architect who has never seen a system survive contact with real users. The problem isn't that designing up front is bad; it's that the future is unknowable. The requirements you're so sure about today are hypotheses, and some of them are wrong. You just don't know which ones yet.
Let me tell you a story that shows how that dream usually ends.
The Rewrite That Never Shipped
A company had a monolith that was, honestly, a mess. Slow deploys, tangled code, a database with four hundred tables and no clear owner. Leadership approved "the new platform": a clean rewrite, designed from scratch by the best people in the company, with every future need anticipated.
The architecture team spent six months on the design. Microservices, event sourcing, a service mesh, a custom framework "so every team builds things the same way". Beautiful diagrams. Then development started.
Here's what happened over the next three years:
- The old system kept changing, because the business couldn't stop for three years. Every new feature had to be built twice, or the rewrite fell further behind;
- The requirements the design was based on became obsolete: the company entered a new market, dropped a product line and changed its pricing model twice;
- The custom framework consumed a whole team just to maintain it;
- Nothing reached production until "everything was ready", so there was zero real feedback for three years;
- The best engineers were on the rewrite, so the old system, the one paying the bills, was maintained by whoever was left.
In year three, a new CTO looked at the budget, looked at the delivery date (which had moved five times) and cancelled the project. The monolith is still running today. Slightly worse than before, because it was neglected for three years.
The big-bang rewrite fails for a simple reason: it trades a known, working system for an unknown one, and it delivers no value and no learning until the very end. By the time you find out your design assumptions were wrong, you've already spent the budget.
The alternative was never "don't improve the system". It was to improve it incrementally: carve out one piece at a time, put it in production, learn from it, and keep going. That's what Evolutionary Design is about.
What Evolutionary Architecture Actually Means
The term was popularized by Neal Ford, Rebecca Parsons and Patrick Kua in the book Building Evolutionary Architectures. Their definition is short and precise: an evolutionary architecture supports guided, incremental change across multiple dimensions.
Let's unpack those three words, because each one matters:
- Incremental: change happens in small steps, both in how the software is built (small commits, small deploys) and in how it's released (progressive rollout, not big-bang);
- Guided: the change isn't random. There are objective criteria (the fitness functions, which we'll see in a moment) that tell you whether the architecture is still healthy after each step;
- Multiple dimensions: architecture isn't only code structure. It includes performance, security, data, operability, cost, compliance. A change can be fine on one axis and terrible on another.
This sits between two extremes that everyone has seen in the wild.
Big Design Up Front (BDUF) assumes you can know the requirements well enough to design everything before building. It works for bridges, where physics doesn't change after the blueprint is signed. Software isn't a bridge: the ground moves under it every quarter.
No design is what many teams fall into when they reject BDUF and misread agile as "we don't plan". Every decision is local and short-term, and the architecture that emerges is accidental. It's fast for six months and then it's slow forever.
Evolutionary Design is not the absence of design. As Martin Fowler put it years ago in Is Design Dead?, it's design that happens continuously, supported by practices (tests, refactoring, continuous integration) that make changing the design cheap. You still think hard about architecture; you just don't pretend you can think about all of it at once.
Decide at the Last Responsible Moment
If you can't know everything up front, when should you make a decision? The Lean software community has a good answer: at the last responsible moment. That's the point where delaying further would eliminate an important option or cost more than deciding now.
Notice the word responsible. It's not the last possible moment, where the team is paralyzed and the decision gets made by accident. It's the moment where you have as much information as you're going to get without paying too much for waiting.
Why delay at all? Because every week you wait, you learn something:
- Real usage data shows which features matter and which were someone's guess;
- The load profile becomes visible, so you size for reality instead of a spreadsheet;
- The team understands the domain better, so the boundaries you draw are better;
- New options may appear (a managed service, a library, a platform feature).
Reversible and Irreversible Decisions
Not every decision deserves the same care. Amazon made famous a simple way to sort them: one-way doors and two-way doors.
- A two-way door is a decision you can walk back through if it turns out wrong: a library choice behind an interface, a UI layout, a caching strategy, a feature behind a flag. Make these quickly, with small teams, and watch the results;
- A one-way door is hard or impossible to reverse: the primary database technology for core data, a public API contract that external customers integrate with, a data model with years of history, a vendor contract with a three-year lock-in. These deserve slow, careful thought, prototypes and a written record.
The trap is treating every decision as a one-way door (and becoming slow and bureaucratic) or treating one-way doors as two-way doors (and discovering the lock only when it's too late).
The bottom-left quadrant is the sneaky one. Small decisions that look harmless but quietly become permanent: a date stored as a string in a table that ten services read, an internal event format that everyone copies, an ID generated in a way that leaks into URLs. Nobody writes an ADR for these, and five years later they cost a migration.
And here's the real architect's move: turning one-way doors into two-way doors. Put the database behind a repository layer. Version your public API from day one. Keep vendor-specific code behind an adapter. You pay a little now to buy the right to change your mind later.
Nice try, Junior, but no. Not deciding is also a decision, usually the worst one, because it gets made by default, by whoever writes the code first, with no thought at all. The last responsible moment has a deadline. And for decisions that are cheap to reverse, delaying is pure waste: pick something reasonable, ship it, and learn. Save the long thinking for the one-way doors.
Fitness Functions: Guardrails for Change
Here's the part that separates evolutionary architecture from "we just refactor when we feel like it". If the architecture is going to change continuously, how do you know it's still healthy? How do you stop a thousand small, reasonable-looking changes from slowly destroying the structure?
The answer from Ford, Parsons and Kua is the architectural fitness function: an objective, preferably automated, check of how well the system meets an architectural characteristic. The name comes from evolutionary computing, where a fitness function measures how close a candidate solution is to the goal.
In plain terms: you take the architectural rules that usually live in a wiki page nobody reads ("the domain layer must not depend on the web layer", "the checkout page must load in under two seconds", "no service calls another service's database") and you turn them into tests that run in the pipeline. When a change breaks a rule, the build fails, just like a failing unit test.
A fitness function turns an architectural intention into an executable check. It's the difference between hoping the architecture is respected and knowing it is, on every commit.
Kinds of Fitness Functions
| Type | What it checks | Example |
|---|---|---|
| Structural (dependency rules) | Layers, module boundaries, forbidden dependencies, cycles | ArchUnit (Java), NetArchTest (.NET), dependency-cruiser (JavaScript) failing the build when the domain imports the infrastructure layer |
| Performance budgets | Latency, throughput, bundle size, page weight | A load test in the pipeline that fails if p95 latency exceeds 300 ms; a front-end budget that fails if the JavaScript bundle grows past 200 KB |
| Security | Vulnerable dependencies, secrets, insecure configuration | Dependency scanning and secret detection as blocking pipeline steps |
| Operability | Health checks, logs, metrics, traces present | A contract test that fails if a new service doesn't expose health and metrics endpoints |
| Data and contracts | Schema compatibility, API compatibility | Consumer-driven contract tests; a check that blocks breaking changes to a published event schema |
| Cost | Resource sizes, tags, spend per unit | Policy as Code that rejects untagged resources or oversized SKUs in non-production |
| Holistic (in production) | Behavior of the running system | SLO burn-rate alerts; chaos experiments that verify the system survives losing an instance |
Some fitness functions are atomic (they check one thing, like a dependency rule) and some are holistic (they check a combination, like "the system stays within its latency SLO during a zone failure"). Some run on every commit, some on a schedule, some continuously in production. You need a mix.
How to Adopt Fitness Functions
1. Start from the characteristics that really matter
Goal: protect the few architectural qualities that would hurt the most if they degraded.
Don't try to automate every rule on day one. Ask the team: "Which architectural property, if it silently eroded, would cause us the most pain in a year?" Maybe it's the separation between bounded contexts, maybe it's checkout latency, maybe it's the absence of cycles between modules. Start there.
Benefit: fast return on effort and a team that sees the value before being asked to maintain dozens of checks.
2. Make them run in the pipeline, not in a slide deck
Goal: architectural rules are enforced by machines, on every change.
A rule that exists only in a document will be broken the first time a deadline is tight. A rule that fails the build gets respected, or gets consciously changed through a discussion, which is just as good.
Benefit: architecture reviews stop being about catching violations and start being about making better decisions.
3. Treat a failing fitness function as a conversation, not a wall
Goal: keep the rules alive and relevant.
Sometimes the rule is right and the code must change. Sometimes the rule is outdated and the architecture must evolve. Both are fine, as long as the decision is explicit and recorded. What's not fine is disabling the check quietly "just for this release".
Benefit: the fitness functions evolve together with the architecture instead of becoming a museum of old opinions.
4. Include production signals
Goal: check the architecture where it actually lives.
Pipelines test what you predicted. Production tells you what actually happens. SLOs, error budgets, cost per transaction and latency percentiles are fitness functions too, and they're the ones customers feel. This is where evolutionary design meets Observability First.
Benefit: you detect architectural drift that no static test could see, such as a chatty integration that only hurts under real load.
Incremental Change with Feedback Loops
Fitness functions tell you whether a change is safe. The other half of the principle is making changes small and frequent, so each one produces feedback quickly. It's the classic build, measure, learn loop, applied to architecture instead of just product features.
The shorter this loop, the cheaper it is to be wrong. If a lap takes a quarter, a bad decision costs a quarter. If it takes a day, it costs a day. That's why evolutionary design is so tightly connected to continuous integration, continuous delivery and the practices described in Operational Excellence: without them, the loop is too slow for the architecture to evolve safely.
Metrics That Guide Evolution
How do you know whether your architecture is getting easier or harder to change? Opinions aren't enough. The research program behind the Accelerate book and the annual DORA reports identified a small set of metrics that correlate with both delivery performance and organizational outcomes:
| Metric | What it tells you about the architecture |
|---|---|
| Deployment frequency | How often you can ship. Low frequency often means coupling: too many things must be released together. |
| Lead time for changes | Time from commit to production. Long lead times point to slow pipelines, manual gates or tangled dependencies. |
| Change failure rate | Share of deployments that cause a failure. High rates suggest missing tests, missing fitness functions or changes that are too big. |
| Time to restore service | How fast you recover. Slow recovery points to poor observability and no easy rollback. |
These aren't vanity numbers for a dashboard. Watch the trend. If lead time keeps growing quarter after quarter while the team size stays the same, the architecture is accumulating friction, and that's a signal to invest in decoupling before the next big feature.
Techniques for Changing a Running System
Evolution happens while the system is in production and customers are using it. You can't stop the plane to change the engines, so you need techniques that let old and new coexist safely.
1. Strangler Fig
Named by Martin Fowler after the fig trees that grow around a host tree until they replace it. You put a facade (a router, gateway or proxy) in front of the legacy system, then move one capability at a time to the new implementation. The facade decides where each request goes. Over time, the legacy shrinks until it can be switched off.
This is exactly what the company in our rewrite story should have done. Each slice goes to production, delivers value, produces feedback and can be rolled back by changing a route. I wrote a full practical guide on this in The Strangler Fig Migration Blueprint.
2. Branch by Abstraction
When the thing you need to replace lives inside the codebase (an ORM, a payment provider client, a logging library), you can't put a router in front of it. Branch by Abstraction solves that:
- Create an abstraction (an interface) around the component you want to replace;
- Move all callers to use the abstraction instead of the concrete component;
- Build the new implementation behind the same abstraction;
- Switch callers gradually (often with a flag) until the old implementation has no users;
- Delete the old implementation, and the abstraction too if it no longer earns its keep.
The key point: the main branch stays releasable the whole time. No long-lived feature branch that takes three weeks to merge and breaks everything when it finally does.
3. Feature Flags
Feature flags (or toggles) separate deploying code from releasing behavior. The code goes to production switched off, and you turn it on for internal users, then 1% of customers, then 10%, then everyone. If something goes wrong, you flip the switch back without a new deploy.
For architecture, flags let you run the old and new paths side by side, compare results, and migrate with confidence.
4. Parallel Run
For critical logic (pricing, tax calculation, financial reconciliation), you can run the old and new implementations at the same time, serve the old result to the customer, and compare both in the background. When the differences reach zero for long enough, you switch. It costs extra compute, but for high-stakes changes it's a bargain.
Easy there, Junior! Flags are powerful, but every flag is a small branch in your code that someone has to understand, test and eventually remove. A codebase with three hundred forgotten flags has 2300 theoretical combinations, and nobody knows which ones are actually running in production. Flags need an owner, an expiration date and a cleanup task created the same day the flag is created. Release flags should live for weeks, not years. That's what we call flag debt, and it's real debt with real interest.
Modularity and Low Coupling: The Real Enablers
All these techniques depend on one thing: being able to change one part of the system without changing everything else. That's what modularity and low coupling give you.
A system where every module reaches into every other module's internals can't evolve incrementally, because there are no increments. Every change is a big change. This is why the "distributed monolith" is so painful: you have all the operational cost of microservices and none of the independence, because services still have to be deployed together.
Some practical ways to keep the architecture evolvable:
| Approach | Benefit |
|---|---|
| Draw boundaries around business capabilities. Use domain modeling to find where one context ends and another begins (see Bounded Contexts). | Changes in one business area stay inside one module, so teams can move independently. |
| Communicate through explicit contracts. APIs and events with versioned schemas, never shared database tables. | You can replace the implementation behind a contract without the consumers noticing. |
| Hide volatile decisions behind interfaces. Vendors, storage engines, external APIs. | Turns one-way doors into two-way doors at a small upfront cost. |
| Prefer a modular monolith before microservices. Strong internal boundaries, one deployable unit. | You get evolvability without paying the distributed-systems tax before you need it, and you can extract services later along boundaries that have been proven. |
| Enforce boundaries with fitness functions. Dependency rules in the pipeline. | Boundaries don't erode silently under deadline pressure. |
A good test for evolvability: pick a real feature from last quarter and count how many modules, repositories and teams it touched. If the answer is "most of them", your architecture is resisting change, no matter how modern the technology stack looks.
Architecture Decision Records: Remember Why
Evolutionary design means decisions will be revisited. That only works if people know why a decision was made in the first place. Otherwise you get one of two bad outcomes: nobody dares to change anything ("there must have been a reason"), or someone changes it and reintroduces the exact problem the original decision solved.
An Architecture Decision Record (ADR) is a short document, usually a markdown file in the repository, that captures one decision:
- Context: what situation and forces led to the decision;
- Decision: what was chosen;
- Alternatives considered: and why they were rejected;
- Consequences: the good, the bad and the trade-offs accepted;
- Status: proposed, accepted, superseded (with a link to the newer ADR).
ADRs are cheap, versioned with the code, and reviewed in pull requests like everything else. When a decision changes, you don't delete the old ADR; you write a new one that supersedes it. The history of the architecture becomes readable, and new team members can understand the system's shape without an archaeology expedition.
Write ADRs for the one-way doors and the important two-way doors. You don't need one for every library upgrade.
YAGNI vs Designing for Change
You Aren't Gonna Need It (YAGNI) is one of the pillars of evolutionary design: don't build features or abstractions for needs you only imagine. Every speculative abstraction has a cost: code to maintain, concepts to learn, indirection to debug. And most of the time, the future you designed for never arrives, or arrives looking different from what you expected.
But YAGNI is often misread as "never think about the future". That's not it. The distinction is subtle but important:
- Speculative generality (bad): building a plugin system because "someday we might support other payment providers", when you have one provider and no plans for another;
- Designing for change (good): keeping the payment provider behind a clean interface, which costs almost nothing and makes the future change cheap if it ever happens.
The rule of thumb: don't build the future, but don't block it either. Keep the code clean, the boundaries clear and the tests good, and most future changes become affordable without you having predicted them.
The best preparation for an unknown future isn't a flexible framework. It's a codebase that is easy to change: well tested, loosely coupled and simple. Simplicity is the most future-proof architecture there is.
Technical Debt as a Conscious Tool
Technical debt isn't always a sin. Like financial debt, it can be a smart investment when taken consciously: you ship faster now, validate a market hypothesis, and pay it back later. The problem is debt taken without knowing, and debt that is never paid.
Martin Fowler's Technical Debt Quadrant is a useful way to think about it, crossing deliberate vs inadvertent with prudent vs reckless:
- Deliberate and prudent: "We know this won't scale past ten thousand users, but we need to launch and learn. We'll revisit it when we reach five thousand." This is healthy;
- Deliberate and reckless: "We don't have time for design." This is how the big ball of mud starts;
- Inadvertent and reckless: the team didn't know good practices and didn't care to learn;
- Inadvertent and prudent: "Now that we've built it, we understand how we should have built it." This happens to everyone, and it's exactly why evolution matters.
Making debt a conscious tool means:
- Record it when you take it (an ADR, a ticket, a comment with a link), with the trigger that will make you pay it back;
- Make it visible in the backlog, not hidden in someone's head;
- Reserve capacity to pay it down continuously (many teams use a fixed share of each iteration) instead of waiting for a "refactoring sprint" that never gets approved;
- Watch the interest: if lead time and change failure rate are growing in an area, the debt there is charging interest, and it's time to pay.
Tradeoffs
Evolutionary design is powerful, but it's not free. Like every principle, it pulls against others, and pretending otherwise is how teams end up disappointed. Shall we look at the main tensions?
Short-Term Speed vs Structural Integrity
The daily pressure is always to ship the feature now. Writing fitness functions, keeping boundaries clean and paying down debt all compete with that. Skip them for too long and you slide toward "no design"; overdo them and you slide back toward BDUF with extra steps. The balance moves over time: an early-stage product validating a market can accept more debt than a mature platform with hundreds of customers.
The Cost of Keeping Options Open
Every abstraction that keeps a door two-way has a price: more code, more indirection, sometimes a little performance. Keeping every option open is its own form of over-engineering. Keep options open only where the probability of change multiplied by the cost of change justifies it.
Tradeoffs with Reliability
Frequent, incremental changes mean more deployments, and each one is an opportunity for failure. Without solid automation, observability and rollback, "evolve continuously" becomes "break continuously". Coexistence periods (strangler facades, parallel runs, dual writes) also add moving parts and new failure modes. The practices in Reliability are a prerequisite, not an afterthought.
Tradeoffs with Security
Every flag, facade and transition path is additional surface to protect. Old and new systems running in parallel may have different security models, and the seam between them is a classic place for gaps. Fitness functions help here too: security checks belong in the same pipeline (see Security Shift-Left).
Tradeoffs with Cost Optimization
Running legacy and new side by side during a migration means paying for both. Parallel runs double compute for the logic under test. Pipelines full of fitness functions, load tests and contract tests consume build minutes. These costs are usually far smaller than a failed rewrite, but they must be budgeted and time-boxed, or the "temporary" coexistence becomes permanent.
Tradeoffs with Performance Efficiency
Abstractions, facades and adapters add latency and hops. A strangler facade is one more network call on every request. Usually negligible, sometimes not. Performance budgets as fitness functions keep this visible, so the cost of evolvability never grows silently.
Tradeoffs with Operational Excellence
Flags, multiple versions and coexisting systems increase the cognitive load on whoever operates the system. "Which version is this customer on? Is that flag on in this region?" Without discipline (flag ownership, cleanup, clear dashboards), operational complexity grows faster than the system itself.
Welcome to architecture, Junior! There's no configuration that's right forever. You know you're on the right track when changes stay cheap over time: lead time is stable or falling, deployments are boring, the team isn't afraid to touch old code and nobody is scheduling the "big rewrite" meeting. The trade-offs don't disappear; you just make them consciously, write them down in ADRs, and revisit them when the context changes.
Conclusion
Evolutionary Design accepts a simple truth: you'll never know less about your system than you do on day one. So instead of betting everything on an upfront plan, you design enough to start, decide at the last responsible moment, and let real feedback shape the architecture over time.
It's not the absence of design. It's design supported by guardrails: fitness functions that turn architectural rules into automated checks, feedback loops short enough to make mistakes cheap, techniques like strangler fig and branch by abstraction to change a running system safely, modularity that keeps changes local, and ADRs that remember why things are the way they are.
Most importantly: evolutionary design is what keeps you out of the "rewrite from scratch" trap. A system that evolves continuously never gets so bad that throwing it away seems like the only option.
Next Steps
-
Map your one-way doors List the decisions in your system that would be very expensive to reverse. Check whether each one has an ADR and whether some could be made reversible with a small abstraction.
-
Write your first fitness function Pick the architectural rule that hurts most when broken (a layer dependency, a latency budget, a module boundary) and make it fail the build. One real check beats fifty rules on a wiki.
-
Start measuring the DORA metrics Deployment frequency, lead time, change failure rate and time to restore. Track the trend quarterly and use it to decide where to invest in decoupling.
-
Adopt ADRs Create an
adrfolder in the repository and record the next significant decision. Then the one after that. -
Replace the next rewrite with a strangler plan If there's a legacy system everyone wants to rewrite, design the facade and the first slice instead. Ship it, learn, repeat.
-
Put flags on a diet Give every feature flag an owner and an expiration date, and remove the ones that have been fully rolled out.
- Building Evolutionary Architectures (Ford, Parsons, Kua)
- Martin Fowler: Is Design Dead?
- Martin Fowler: Strangler Fig Application
- Martin Fowler: Branch By Abstraction
- Pete Hodgson: Feature Toggles (aka Feature Flags)
- Martin Fowler: Technical Debt Quadrant
- DORA: DevOps Research and Assessment
- ArchUnit
- Architecture Decision Records
- Microsoft Azure Well-Architected Framework
Related Items
- PrincipleResilience PatternsEverything fails sooner or later, so design your code to bend instead of break. After all, who wants one slow API to take the whole checkout down with it?
- PrincipleCost TransparencyIf nobody can say what a request, a tenant or an order costs, the bill is just a rumor. Design the solution so its price tag is visible.
- PrincipleObservability FirstIf you only instrument after the first big incident, you're debugging in the dark. After all, who wants to find the root cause at 3am with nothing but console.log("here")?
Comments
Questions, corrections, or your own take are all welcome. Sign in with GitHub to join in.