15 min read · 9 self-checks · Updated June 2026

XP / Engineering Practices

Simple Design

An XP principle that advocates building the simplest solution that could possibly work, adding complexity only when proven necessary by actual requirements.

Senior Test Lead

What it is

Simple Design is one of Kent Beck’s four rules of XP design, ordered by priority:

  1. Passes all tests. The design must work. Untested or broken code is not simple; it is wrong.
  2. Reveals intent. The code should clearly communicate what it does. Names, structure, and flow should minimise the mental effort required to understand it.
  3. No duplication. Every piece of knowledge should have a single, unambiguous representation. Duplication breeds inconsistency and maintenance cost.
  4. Fewest classes and methods. Once the above three are satisfied, remove anything that does not earn its keep. Fewer moving parts means fewer things to break.

Simple Design fights speculative generality — the temptation to build abstractions for requirements that do not exist yet. It asks: what is the smallest, clearest thing that solves the problem today? Not tomorrow. Today.

Simple is not easy. Simple Design often requires more thought than a complex one. It is easier to add layers, frameworks, and indirection than to find the clean, minimal expression of a solution.

When to use it

Every design decision. Every code review. Every time you are tempted to introduce an interface, a base class, or a microservice “just in case.” Simple Design is a lens, not a phase.

During code review, ask four questions: Does this pass its tests? Is the intent obvious? Is there duplication? Can anything be removed without breaking the first three rules? If the answer to all four is yes, the design is simple enough.

Key benefits: faster delivery, lower cognitive load, easier onboarding, fewer bugs, and code that bends rather than breaks when requirements change.

Key concepts

The Four Rules

The rules are ordered by priority for a reason. A design with no duplication but failing tests is not simple. A design with fewest methods but obscure intent is not simple. Work down the list, never up.

Speculative Generality

Building a plugin architecture when you have one plugin. Abstracting a database layer when you have one database. Creating a factory when you instantiate one type. These are all speculative generality. The complexity tax is paid immediately; the benefit may never arrive.

YAGNI

“You Aren’t Gonna Need It.” A slogan, not a law. The point is not that you will never need the feature; it is that you do not need it now, and building it now costs more than building it later when you actually know what you need.

Emergent Design

Rather than designing the whole system up front, let the design emerge from working code. Start simple. When a real requirement forces complexity, refactor the simple code into a slightly more complex form. Repeat. The result is a design that matches the actual problem, not an imagined one.

Symptom Likely Cause Simple Design Response
Empty interfaces or base classes Speculative generality Delete them; reintroduce only when multiple implementations exist
Deep inheritance hierarchies Premature abstraction Prefer composition; flatten to what the current features require
Unused configuration options Anticipating future needs Remove; hard-code the one value you actually use

Common pitfalls

  • Confusing simple with quick and dirty. Simple Design is disciplined. Quick and dirty skips tests, ignores duplication, and produces spaghetti. They are opposites.
  • Over-engineering. Adding layers, patterns, and abstractions “for flexibility” is the enemy of Simple Design. Flexibility should be earned by demonstrated need.
  • Using simple as an excuse to skip abstraction. When a genuine need for abstraction appears (three similar features, two database types), Simple Design requires you to introduce it. It is not anti-abstraction; it is anti-premature-abstraction.
  • Applying it to genuinely complex domains. Some domains (avionics, tax law, molecular modelling) are inherently complex. Simple Design does not deny complexity; it refuses to add unnecessary complexity on top.
  • Rejecting all patterns. Patterns are tools. Simple Design uses them when they clarify intent and remove duplication, not when they impress readers.

Anti-pattern: A developer who says “we might need this later” is usually wrong. Track how often “later” actually arrives. Most teams find it is under 20% of the time.

NZ context

Fixed-price contracts are common in the New Zealand market, especially for government and SME engagements. In these contracts, every hour of unnecessary engineering is an hour that eats margin or burns budget. Simple Design protects both the vendor and the client against over-engineering that feels professional but delivers no business value.

New Zealand teams are often smaller than their international counterparts. A team of four developers cannot afford the maintenance overhead of a complex architecture designed for a team of forty. Simple Design keeps the codebase approachable so that any team member can safely modify any part of the system.

Industry Reality

🏭 What you actually encounter on the job
  • Most codebases you inherit will violate Simple Design systematically — deep inheritance hierarchies, unused configuration flags, and abstract factories with one implementation are the norm, not the exception. Your job is to make the next commit simpler, not to rewrite everything.
  • Developers often pay lip service to YAGNI while immediately adding "just-in-case" abstraction layers during the same sprint. Senior practitioners catch this in PR review, not after the fact.
  • In NZ government and enterprise projects, architecture committees frequently mandate patterns (e.g. a full repository/service/controller split) regardless of feature count. Pragmatic practitioners implement the mandated pattern at the smallest scale possible rather than fighting the policy.
  • Emergent design is rarely written into project contracts. Teams that want it typically have to protect it implicitly — keeping sprint velocity high enough that nobody asks why the codebase looks "thin" early on.
  • The YAGNI argument loses to seniority in most team debates. A principal engineer saying "we'll need this later" is treated as a requirement, not speculation. Winning these conversations requires concrete data: show the last three times "later" never arrived.

Context guide

How the right level of Simple Design effort changes based on team context.

Context Priority Why
NZ government fixed-price contract (e.g. Benefits NZ, FamiliesNZ, LandNZ) Essential Every hour of speculative engineering directly eats fixed-price margin. Policy changes driven by legislation routinely invalidate the assumptions behind premature abstractions, meaning that over-engineered features must be discarded rather than extended.
Small NZ team (2–5 developers) with high bus-factor risk Essential A complex architecture designed for a team of 20 becomes a maintenance trap for a team of four. Simpler code means any team member can safely touch any module without a week of orientation.
Greenfield sprint 1–3 on a new product (startup or internal tool) Essential Requirements are most volatile in early sprints. Abstractions introduced before the shape of the domain is understood will almost certainly need to be thrown away. Ship the concrete thing; the abstraction will reveal itself by sprint four.
Enterprise codebase with mandated architectural patterns (e.g. TeleNZ, Harbour Bank, Pacific Air) Medium Architecture committees may mandate layered patterns regardless of feature count. Apply Simple Design within the mandated structure — implement each mandated layer at the smallest scale the pattern permits. Fight complexity at the method and class level even when you cannot change the macro-architecture.
Safety-critical or compliance-constrained system (e.g. HealthNZ HealthNZ, NZ Police, NZISM-scoped infrastructure) Medium Audit trails, explicit error handling, and defensive validation may look like unnecessary complexity from a pure Simple Design standpoint, but they are required by compliance. Apply Simple Design to everything else — naming, data structures, flow — and treat compliance requirements as non-negotiable constraints, not speculative generality.
Inheriting a large legacy codebase (e.g. CoverNZ, Revenue NZ, Benefits NZ core systems) Low You cannot retroactively apply Simple Design to 15 years of accumulated complexity in a single sprint. Apply it to every new commit and every refactor ticket, but do not reject the legacy abstractions wholesale — you likely do not yet understand why they exist. Simplify incrementally and characterise before you delete.

Trade-offs

What you gain and what you give up when you adopt Simple Design.

Advantage Disadvantage Use instead when…
Lower cognitive load for new team members — a contractor joining an Pacific Air project mid-flight can read and contribute to a simple codebase within days, not weeks. Risk of under-designing when requirements are genuinely known in advance — if you have a confirmed requirement for three KiwiSaver fund types at project kickoff, building the first one without any shared abstraction creates avoidable rework. Requirements are contractually locked and multiple concrete variations are confirmed in the same sprint — introduce the abstraction early with full evidence, not speculation.
Design aligns with actual requirements — an Revenue NZ tax rule implemented as a direct function rather than a rules engine actually matches the way tax legislation is written: one rule at a time, amended by policy, not extended by plugin. Can produce short-term duplication when similar features arrive quickly — if three CoverNZ claim types are scoped across the same sprint, writing each one independently before refactoring means a brief period of near-identical code in the codebase. A sprint contains multiple concrete instances of the same pattern — introduce the shared abstraction at sprint planning rather than after the third copy lands.
Tests are cheaper to write and maintain — a Pacific Bank payment function with no abstract intermediaries is straightforward to unit-test directly; every layer of indirection added for flexibility is another seam that requires mocking. Senior engineers may perceive simple designs as unsophisticated — in organisations that equate complexity with professionalism, a clean, flat codebase can be mistaken for insufficient effort, requiring active advocacy from test leads and architects. Organisational culture strongly rewards architectural sophistication — invest time in making the business case for simplicity with concrete velocity and defect data before shipping lean designs that need defending.
Refactoring is cheaper — a simple, well-named function is easier to safely rename, split, or inline than the same logic buried inside a three-level class hierarchy. Privacy Act 2020 compliance changes that require data-flow modifications are far less risky in a simple codebase. Requires ongoing discipline — Simple Design is not a one-time decision. Without active enforcement at code review, a codebase that starts simple will accumulate complexity sprint by sprint as developers add “just-in-case” abstractions. The team lacks the review culture or time to enforce simplicity consistently — invest in automated complexity gates in CI (cyclomatic complexity thresholds, code coverage minimums) before relying on Simple Design as a cultural norm.

Enterprise reality

How Simple Design changes when 200–300 developers across 10+ squads are touching the same codebase

  • Simplicity enforcement can no longer rely on culture alone — at this scale it must be automated. Organisations like TeleNZ and CloudBooks gate pull requests on cyclomatic complexity thresholds and enforced test-coverage minimums in CI; squads that skip the gate need an explicit architecture-lead override, creating a visible audit trail of every exception.
  • The Privacy Act 2020 and NZISM (New Zealand Information Security Manual) introduce non-negotiable structural requirements — audit logging, data-access abstraction, encryption at rest — that look like unnecessary complexity from a pure Simple Design standpoint. The discipline is to apply Simple Design to everything else while treating compliance constraints as fixed requirements, not targets for elimination. Treat them exactly as you would a failing test: non-negotiable, and never removed to make the design look leaner.
  • At 10+ squad scale, shared abstractions become a coordination problem. A utility class built by Squad A for one concrete use case gets copied by Squads B, C, and D before anyone spots the duplication — because there is no single PR review stream covering the whole codebase. The practical fix is an inner-source model: shared modules live in a versioned internal package, and squads consume rather than copy. The rule “two concrete use cases before introducing an abstraction” still applies, but the second use case is now a squad dependency request, not a file in the same repo.
  • Architecture governance bodies are a feature at this scale, not a bureaucratic overhead. When PCI DSS cardholder-data scope runs across six squads — as it does in NZ banking platforms at Harbour Bank and Coastal Bank — an architecture committee mandating a common data-handling pattern prevents ten independently designed storage layers, each carrying its own compliance risk. Accept the mandated pattern, apply Simple Design within its bounds, and document in the team wiki that the structural overhead is a compliance requirement so future engineers do not attempt to flatten it.

What I would do

Professional judgement — when to adopt Simple Design, when to adapt it, and what to watch for.

If…
I am a test lead on an Revenue NZ personal income tax feature and a developer proposes a generic “tax computation engine” with a plugin interface, citing the likelihood of future tax types being added within the year
I would…
Ask for the confirmed backlog item for the second tax type. If one does not exist in the current or next sprint, I would decline the plugin architecture and request a direct implementation of the single confirmed tax rule. I would also add a comment to the PR template that reads: “If a second tax type is added, extract the shared abstraction at that point — the shape of legislation-driven requirements is unknowable until the bill is drafted.” This keeps the decision auditable rather than subjective.
If…
I am working on a TransitNZ road-user charges portal and the architecture committee has mandated a repository/service/controller split for all API endpoints, even read-only lookups that have no business logic
I would…
Implement the mandated pattern at minimum viable scale: the repository contains a single query method, the service method delegates directly with no added logic, and the controller calls the service. I would not fight the mandate — that wastes political capital — but I would ensure each layer does only one thing and contains no dead code or forwarding logic beyond what is strictly required. Within the mandated frame, I apply Simple Design at the method level. I would also document in the team wiki that the pattern is a governance requirement, not an engineering recommendation, so future team members do not extend it unnecessarily.
If…
I inherit a HealthNZ HealthNZ patient-records module with a deep abstract base-class hierarchy and I need to add a new document type without fully understanding why the hierarchy exists
I would…
Write a characterisation test suite covering the observable behaviour of the existing hierarchy before touching a single line. Then I would add the new document type as the simplest concrete subclass that passes those tests, without collapsing the hierarchy prematurely — the existing structure may encode compliance requirements from the Health Information Privacy Code that are not obvious from the code alone. I would schedule a separate refactoring story to simplify the hierarchy only after I have enough test coverage to verify I am not breaking that compliance behaviour. Simple Design applied to inherited code means “make the next commit simpler,” not “rewrite everything today.”

The bottom line: Require at least two confirmed, distinct, in-sprint use cases before introducing any abstraction — one use case is not a pattern, it is a specific thing wearing a costume. In NZ government projects especially, “we’ll need this later” is almost always a prediction about legislation that has not been drafted yet, and those predictions are wrong far more often than they are right.

Best Practices

✓ What experienced practitioners do
  • ✓ Apply the four rules in priority order during every code review — tests first, then intent, then duplication, then size. Never trade a failing test for cleaner code.
  • ✓ When introducing a new abstraction, require at least two concrete use cases before creating the generalisation. One use case is not a pattern; it is a specific thing wearing a costume.
  • ✓ Delete speculative code on sight — unused configuration options, empty interfaces, abstract base classes with one subclass. These accrue cognitive debt faster than they save future effort.
  • ✓ Use inline methods, flat functions, and named variables aggressively before reaching for class hierarchies. Readability via naming is cheaper than readability via structure.
  • ✓ Track cyclomatic complexity on changed files in CI. A rising average over a sprint is a signal that speculative generality is creeping in.
  • ✓ Run a "read-aloud" check: can a junior explain what a method does in one sentence without reading its dependencies? If not, simplify the naming or split the method before adding more code around it.
  • ✓ When stakeholders request future-proofing, negotiate: implement the simplest version now and book a refinement story for when the second use case actually arrives. This keeps scope honest.
  • ✓ Pair refactoring toward simplicity with test coverage — never simplify untested code without adding a characterisation test first, so you can verify the behaviour is preserved.

Common Misconceptions

❌ Myth: Simple Design means writing the quickest, most naive code possible — no patterns, no abstractions, just brute-force logic.

Reality: Simple Design is disciplined, not lazy. It demands that code passes tests, reveals intent, and avoids duplication before reducing size. A well-named class with a single responsibility is simpler than a 300-line function, even though the class involves more files. "Simple" means easy to understand and change, not easy to write.

❌ Myth: YAGNI means you should never plan ahead — just write whatever works right now and fix the mess later.

Reality: YAGNI targets speculative features and abstractions, not considered design. You absolutely should think ahead when naming things, structuring a module boundary, or choosing a data format — because those decisions are hard to reverse. The line is: don't build the feature until you need it, but do name and structure the code you're building as if it might need to grow.

❌ Myth: Simple Design is only relevant for XP teams. If you're using Scrum or Kanban, it doesn't apply.

Reality: Simple Design is a code-level discipline that applies regardless of delivery framework. Scrum boards and sprint ceremonies don't prevent over-engineering; only code-level habits do. NZ teams using Scrum or Kanban benefit from Simple Design principles just as much as XP teams — often more, because they lack XP's explicit pair programming and TDD safety nets.

Career level guidance

Level Focus Milestones
Senior Apply the four rules in daily work; challenge over-engineering in review; refactor toward simplicity Can defend a simple design against demands for premature abstraction; consistently produces code that juniors can read and modify
Test Lead Ensure testability does not drive unnecessary complexity; coach the team on YAGNI; review designs for simplicity Identifies speculative generality during design review; measures and reports on code complexity metrics; secures stakeholder trust in lean designs

Tip for test leads: Cyclomatic complexity and lines of code are useful proxies, but the best test of Simple Design is a junior developer explaining the code back to you. If they can, it reveals intent. If they cannot, it is not simple yet.

Senior engineer insight

Teams that do Simple Design well treat testability as a first-class design criterion — if a unit is hard to test in isolation, that is a signal the design is too coupled, not a reason to skip the test. The practitioners who struggle are those who confuse the four rules as a checklist applied once, rather than a continuous discipline applied at every PR. The single pattern that reliably separates the two groups: require at least two distinct, real use cases before introducing any abstraction. One use case is not a pattern; it is a specific thing wearing a costume.

The most common mistake: treating “simple” as a synonym for “minimal effort” — writing quick, undisciplined code and calling it Simple Design. Real Simple Design is harder to write than complex code because it demands that every name, boundary, and structure earns its place.

From the field

A Wellington-based XP team building a Ministry of Education reporting portal came in with a solid YAGNI mindset — or so they thought. In week two they introduced a generic “report pipeline” abstraction because the lead assumed a second report type was coming in quarter three. Quarter three arrived, the second report type changed scope, and the pipeline had to be scrapped and rewritten around the actual requirement, burning three days of re-work. When they switched to writing the first report as a direct, concrete implementation and letting a second real requirement force the abstraction, their velocity stabilised and the codebase became legible enough for a contractor joining mid-project to contribute safely from day one. The lesson that generalised: in government projects especially, the shape of “future requirements” is determined by policy changes that no developer can predict — so build what is funded, cover it with tests, and refactor when the policy lands.

Self-Check

Q: Your team is building a new Benefits NZ benefit entitlement calculator. A developer proposes a plugin-based rule engine so future benefit types can be added without touching core code. There is currently one benefit type in scope. Should you support this design, and why?

A: No — this is speculative generality. The plugin architecture pays a complexity tax now (more interfaces, indirection, configuration) for a benefit that may never arrive in the form imagined. The Simple Design response is to implement the single benefit type directly and clearly. When a second genuine benefit type is required, refactor to the abstraction then — at that point you will know exactly what shape the abstraction needs to take, rather than guessing.

Q: You are reviewing a pull request for an TransitNZ vehicle registration portal. The PR introduces a base class called AbstractVehicleProcessor with three subclasses, but only one subclass is currently used, and no second use case is planned this quarter. Applying Simple Design principles, what feedback do you give?

A: Flag the base class as premature abstraction. Simple Design's fourth rule — fewest classes and methods — means you remove structure that does not earn its keep. Request that the unused base class be deleted and the single concrete implementation used directly. Leave a comment noting that when a real second vehicle type appears, extracting the abstraction at that point will produce a far more accurate design than the one invented today without evidence.

Q: What is the key difference between Simple Design and Refactoring, and why do experienced practitioners treat them as complementary rather than the same practice?

A: Simple Design is a design philosophy applied at the point of creation — a set of rules that guides what to build in the first place. Refactoring is a technique for improving existing code without changing its observable behaviour. They are complementary: Simple Design reduces the amount of refactoring needed by preventing unnecessary complexity from entering the codebase; refactoring is the mechanism used to move existing code toward a simpler design when it has drifted. On NZ government projects where legacy code accumulates quickly, both are needed — Simple Design on new work, refactoring to incrementally clean what already exists.

Q: A senior developer on your Revenue NZ tax calculation project says: "Simple Design is fine for startups, but in enterprise software you always need to future-proof the architecture — clients expect it and it saves rework later." What is wrong with this reasoning and how do you respond?

A: The claim confuses professional-sounding architecture with proven value. Future-proofing costs are certain and immediate; the benefit is speculative. Research consistently shows that under 20% of "we might need this" abstractions are ever used as intended. In Revenue NZ contexts, requirements are driven by legislation and Revenue NZ policy changes — the shape of future requirements is often unknowable until the legislation is drafted. The correct response is: build the simplest correct system today, cover it with tests, and refactor to the more complex form when the requirement actually materialises. Simple Design is not a startup shortcut; it is the only approach that reliably produces a system that can actually be changed when the real requirements arrive.

Why teams fail here

  • Architecture by anxiety: developers introduce abstractions not because the design requires them but because they feel exposed shipping “thin” code. The complexity is emotional insurance, not engineering response.
  • YAGNI lip service: the team agrees on YAGNI in the retro, then adds a “just-in-case” configuration flag in the very next PR. Simple Design requires active enforcement at code review, not passive agreement in principle.
  • Testability treated as a testing problem: when a class is hard to unit test, teams add mocking layers around it instead of simplifying the design. Hard-to-test code is a design smell — the fix is to simplify the dependency structure, not paper over it with test infrastructure.
  • Seniority beats evidence: a senior engineer says “we’ll need a plugin system eventually” and the team builds it immediately. Simple Design requires the discipline to ask for data — how many times has “eventually” actually arrived? Track it and show the number.

Key takeaway

Simple Design is not about writing less code — it is about writing only the code whose need has already been proven, so that when requirements actually change, there is no speculative scaffolding in the way.

How this has changed

The field moved. Here is how Simple Design evolved from its origins to current practice.

1996

Kent Beck articulates the four rules of simple design: passes all tests, reveals intention, no duplication, fewest elements. Originally part of XP, these rules provide a design philosophy that prioritises working software over clever code.

2001

"Simplicity — the art of maximising the amount of work not done — is essential" in the Agile Manifesto aligns with simple design thinking.

2008

YAGNI and KISS become widely-known engineering principles. Simple design thinking spreads to architecture decisions — REST over SOAP, not because of cleverness but because of simplicity.

2015

The pendulum swings — over-simplified "simple" systems become unmaintainable as requirements grow. Simple design requires active refactoring, not just avoiding complexity upfront.

Now

AI coding tools tend to generate complex, pattern-heavy code unless prompted toward simplicity. Simple design principles are applied to AI-generated code review — flagging unnecessary abstraction and premature generalisation.

← Back to Agile Techniques