Evolutionary Design
Most people assume good software design comes from thinking hard about the problem upfront, drawing the architecture on a whiteboard, and then building exactly that. This is called Big Design Up Front (BDUF). It works when the problem is well understood before you start.
Evolutionary design, a design principle, takes the opposite bet: you don't fully know the right design at the start, so you don't try to guess it. You build the simplest thing that solves today's problem, and you let the design grow — reshaping it — as you learn more about the actual requirements.
Why not design it all upfront?
Because early in a project you know the least you will ever know about it. Requirements shift, users behave differently than expected, and edge cases only show up once real usage begins. A design frozen at the point of maximum ignorance tends to be wrong in ways that are expensive to undo later.
Evolutionary design accepts this uncertainty instead of fighting it. Rather than trying to predict every future requirement and building flexibility for all of them upfront, you build for what you need now, and change the design when a new need actually arrives — not before.
The mechanism: continuous refactoring
Evolutionary design only works if the codebase can be reshaped cheaply and safely. That's what refactoring is for — restructuring existing code without changing its external behavior.
Without refactoring, evolutionary design collapses into just "not designing" — code that grows messier with every feature until nobody can safely touch it. Refactoring is the discipline that keeps the design honest as it evolves: every time you add something, you also clean up so the next addition stays easy.
This is why evolutionary design leans so heavily on automated tests. Tests are what make refactoring safe — they let you restructure code with confidence that you haven't broken anything. Take tests away, and refactoring becomes risky guesswork, and the whole approach falls apart.
YAGNI and simplicity
A natural companion principle here is YAGNI — "You Aren't Gonna Need It." Don't build the generic plugin system, the configurable strategy pattern, or the abstract base class for a case you're only imagining might happen someday. Build for the case in front of you. If the imagined future need actually arrives, you'll design for it then, with real information instead of a guess.
This keeps the design simple, and simple designs are the ones that are cheapest to evolve. Complexity added in anticipation of a future that doesn't arrive is pure cost with no return.
Emergent design
Under this approach, the architecture isn't decided once — it emerges from a sequence of small decisions, each responding to what the code actually needs at that moment. A pattern doesn't get introduced because the book says so; it gets introduced when duplication or rigidity starts to hurt, and the refactoring toward that pattern is the fix.
This is different from having no design. The design is very much present — it's just discovered incrementally rather than declared in advance.
When it works, and when it doesn't
Evolutionary design fits well with short feedback loops: small increments, frequent releases, continuous integration, close contact with real usage. It's the natural companion to agile development.
It fits less well in contexts where mistakes are expensive to walk back — safety-critical systems, hardware with long lead times, or situations where the cost of getting the interface wrong is enormous (a public API used by thousands of external consumers, for instance). In those cases, more upfront design is worth the cost, because the option to "fix it later" barely exists.
Most ordinary software sits somewhere in between: some upfront thinking to avoid obvious mistakes, followed by a willingness to let the details evolve as reality corrects your assumptions.