AoAD2 Practice: Simple Design

This is an excerpt from The Art of Agile Development, Second Edition. Visit the Second Edition home page for additional excerpts and more!

This excerpt is copyright 2007, 2021 by James Shore and Shane Warden. Although you are welcome to share this link, do not distribute or republish the content without James Shore’s express written permission.

📖 The full text of this section is available below, courtesy of the Art of Agile Development book club! Join us on Fridays from 8-8:45am Pacific for wide-ranging discussions about Agile. Details here.

Simple Design


Our code is easy to modify and maintain.

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.

Antoine de Saint-Exupéry, author of The Little Prince

Reflective Design
Incremental Design

When writing code, Agile programmers often stop to ask themselves, “What’s the simplest thing that could possibly work?” They seem to be obsessed with simplicity. Rather than anticipating changes and providing extensibility hooks and plug-in points, they create a simple design that anticipates as little as possible, as cleanly as possible. Counterintuitively, this results in designs that are ready for any change, anticipated or not. It combines with reflective design and incremental design to allow your design to evolve in any direction.

When, not if, I need to change this decision, how hard will it be?

Simple doesn’t mean simplistic. Don’t make boneheaded design decisions in the name of reducing lines of code. A simple design is clean and elegant, not something you throw together with the least thought possible. Whenever I make a design decision, I always ask myself this question: “When, not if, I need to change this decision, how hard will it be?”

The following techniques will help you keep your code simple and change costs low.

YAGNI: You Aren’t Gonna Need It

This pithy XP saying sums up an important aspect of simple design: avoid speculative coding. Whenever you’re tempted to add something to your design, ask yourself if it’s necessary for what the software does today. If not, aren’t gonna need it. Your design could change. Your customers’ minds could change.

When you speculate, and then plans change, outdated design assumptions leave their tendrils throughout your code. You end up having to rip out and replace the speculative code. It’s better not to speculate in the first place. It’s easier to add code from a clean slate than to replace code that’s wrong.

Similarly, remove code that’s no longer in use. You’ll make your design smaller, simpler, and easier to understand. If you need the code in the future, you can always get it out of version control. For now, it’s a maintenance burden.

Once and Only Once

Once and only once is a surprisingly powerful design guideline. As Martin Fowler said:

One of the things I’ve been trying to do is look for simpler [rules] underpinning good or bad design. I think one of the most valuable rules is avoid duplication. “Once and only once” is the Extreme Programming phrase. The authors of The Pragmatic Programmer [Hunt2019] use “don’t repeat yourself,” or the DRY principle.

...Time and time again, I’ve found that by simply removing duplication I accidentally stumble onto a really nice elegant pattern. It’s quite remarkable how often that is the case. I often find that a nice design can come from just being really anal about getting rid of duplicated code. [Venners2002]

“Once and only once” isn’t just about removing duplicated code, though. It’s about giving every concept that’s important to your code a home. Think of it this way:

Express every concept once. And only once.1

1Thanks to Andrew Black for this insight.

Every piece of knowledge must have a single, unambiguous, authoritative representation.

Or, as Andy Hunt and Dave Thomas phrase their DRY Principle: “Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.” [Hunt2019] (ch. 2)

An effective way to make your code express itself once (and only once) is to be explicit about core concepts. Rather than expressing those concepts with primitive data types—an approach called “Primitive Obsession”—create a new type.

For example, an online storefront represented money with floating point numbers. Instead, they could have created a Currency class. In JavaScript, it would look like this:

class Currency {
    this._amount = amount;

  toNumber() {
    return this._amount;

  equals(currency) {
    return this._amount === currency._amount;

At first, this seems wasteful. It’s just a simple wrapper over an underlying data type, except now with added overhead. Not only that, it seems like it increases complexity by adding another class.

But this sort of simple value type turns out to be immensely effective at enabling the “once and only once” principle. Now, any code that’s related to currency has an obvious place to live: inside the Currency class. If somebody needs to implement some new code, they look there first to see if it’s already implemented. And when something about the concept needs to change—say, foreign currency conversion—there’s one obvious place to implement that change.

The alternative isn’t pretty. That online storefront? As it turned out, floating point math wasn’t a great choice. They got themselves into a situation where, when line item refunds and currency conversion was involved, they couldn’t generate a refund that matched the original invoice. (Whoops.) They had to engage in a multimonth process of finding everything that related to currency and changing it to use fixed-point math. True story.

Bet they wish they had expressed the Currency concept once. (And only once.) They could have changed the implementation of their Currency class and called it a day.

When, not if, you need to change a design decision, how hard will it be?

Coupling and Cohesion

Coupling and cohesion are ancient software design ideas that extend back to Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design. [Yourdon1975] (chs. 6-7) They’re no less powerful for their age. Both terms refer to the relationship between concepts in your code.2

2I’ve updated the definitions slightly. The original definition discusses “modules,” not “concepts.”

Parts of your code are coupled when a change to one necessitates a change to the other. To continue the Currency example, a function to convert currency to a string would be coupled to the data type used for currency.

Your code is cohesive when it’s physically close together in your source files. For example, if the function to convert currency to a string was in a Currency class along with the underlying data type, they would be highly cohesive. If the function was in a utility module in a completely different directory, they would have low cohesion.

The best code has low coupling. In other words, changing the code for one concept doesn’t require you to change code for any other concept: changing the Currency data type doesn’t require changing the authentication code, or the refund logic. At the same time, when two parts of the code are coupled, it's best if they’re highly cohesive: if you change the Currency data type, everything else you have to change is in the same file, or at least in the same directory.

When you make a design decision, step back from design patterns and architectural principles and language paradigms for a moment. Ask yourself a question: when, not if, somebody changes this code, will the other things they need to change be obvious? The answer comes down to coupling and cohesion.

Third-Party Components

Third-party components—libraries, frameworks, and services—are a common cause of design problems. They tend to extend tendrils throughout your code. When, not if, you need to replace or upgrade the component, the changes can be difficult and far-reaching.

To prevent this problem, isolate third-party components behind an interface you control. Rather than using the component directly, your code uses the interface. This is called a gateway, but I use the generic term wrapper instead.

In addition to making your code resilient to third-party changes, your wrapper can also present an interface customized for your needs, rather than imitating the third-party interface, and you can extend it with additional features as needed.

For example, when I wrote a wrapper for the Recurly billing service, I didn’t expose a method for Recurly’s subscriptions endpoint. Instead, I wrote isSubscribed(). It called the endpoint, parsed its XML, looped through the subscriptions, and converted the many possible subscription status flags into a simple Boolean result.

Create your wrappers incrementally. Instead of supporting every feature of the component you’re wrapping, support only what you need today, focusing on providing an interface that matches the needs of your code. This will make the wrapper cheaper to create and make it easier to change when (not if) you need to replace the underlying component in the future.

Some components—particularly frameworks—want to “own the world” and are difficult to hide behind wrappers. For this reason, I prefer to build my code using simple libraries, with narrowly defined interfaces, rather than one big do-everything framework. In some cases, though, a framework is the best choice.

It is possible to wrap a framework, but it’s often more trouble than it’s worth. You usually end up having to wrap a bunch of different classes. In some cases, you’ll have to wrap the framework’s base classes, which you can do by writing your own base classes that extend the framework’s base classes.

Alternatively, you can choose not to wrap a third-party component. This makes the most sense when the component is pervasive and stable, such as core language frameworks. You can make this decision on a case-by-case basis: for example, I’ll use the .NET String class directly, without a wrapper, but I’ll use a wrapper to isolate .NET’s cryptography libraries—not because I think they’ll change, but because they’re complicated, and a wrapper will allow me to hide and centralize that complexity.

Fail Fast

One of the pitfalls of simple design is that your code will have gaps. If you’re following the YAGNI principle, there will be some scenarios your code just isn’t capable of handling. For example, you could write a currency rendering method that isn’t aware of non-US currencies, because your code currently renders everything in US dollars. But later, when you add more currencies, that gap could result in a bug.

You might be tempted to prevent those bugs by handling every case you can think of. That’s slow and easy to get wrong. Instead, fail fast. Failing fast allows you to write simpler code: rather than handling all possible cases, you write your code to handle just the cases it needs to handle. For every other case, you fail fast. For example, your currency rendering method could fail fast when asked to render a non-US currency.

To fail fast, write a runtime assertion. It’s a line of code that checks a condition and throws an exception (usually; it depends on the language) when the condition isn’t met. It’s similar to a test assertion, but it’s part of your production code. For example, a JavaScript version of the currency rendering method might have this assertion at the top:

if (currency !== Currency.USD) {
  throw new Error("Currency rendering not yet implemented for " + currency);

Most languages have some sort of runtime assertions built in, but they tend to be fairly inexpressive. I like to write my own assertion module with expressive functions that generate good error messages, such as ensure.notNull(), ensure.unreachable(), ensure.impossibleException(), and so forth.

Some people worry failing fast will make their code more brittle, but the opposite is actually true. By failing fast, you make errors more obvious, which means you catch them before they go into production. As a safety net, though, to prevent your software from crashing outright, you can add a top-level exception handler that logs errors and recovers.

Fail fast code works best when combined with sociable tests (see the “Write Sociable Tests” section), because sociable tests will trigger the fail fast checks, allowing you to find gaps more easily. Isolated tests require your tests to make assumptions about the behavior of dependencies, and it’s easy to assume a dependency will work when it actually fails fast.

Self-Documenting Code

Simplicity is in the eye of the beholder. It doesn’t matter much if you think the design is simple; if the rest of your team—or future maintainers of your software—finds it too complicated, then it is.

To avoid this problem, use idioms and patterns that are common for your language and team. It’s okay to introduce new ideas, but run them past other team members first.

Names are one of your most powerful tools for writing self-documenting code. Be sure to use names that clearly reflect the intent of your variables, functions, and so forth. When a function has a lot of moving parts, use the Extract Function refactoring [Fowler2018] to name each piece. When a conditional is hard to understand, use functions or intermediate variables to name each piece of the conditional.

Note that I didn’t say anything about comments. Comments aren’t bad, exactly, but they’re a crutch. Try to find ways to make your code so simple and expressive that comments aren’t needed.

Pair Programming
Mob Programming
Collective Code Ownership

Good names and simple code are hard. Three things make them easier: first, pairing or mobbing gives you more perspectives and ideas. If you’re having trouble thinking of a good name, or you think some code your driver wrote is unclear, discuss the situation and try to find a better way to express it.

Second, you can always refactor. Give it your best shot now, and when you come back to it later, refactor it to be even better.

Third, take advantage of collective code ownership. When you see code that isn’t clear, figure out what the person who wrote it was trying to say, then refactor to make that intent obvious.

Published Interfaces

Published interfaces reduce your ability to make changes. Once an interface is published to people outside your team, changing that interface typically requires a lot of expense and effort. You have to be careful not to break anything that they’re relying upon.

Some teams approach design as if every public method was also a published interface. This approach assumes that, once defined, a public method should never change. To be blunt, this is a bad idea: it prevents you from improving your design over time. A better approach is to change nonpublished interfaces whenever needed, updating callers accordingly.

If your code is used outside your team, then you do need published interfaces. Each one is a commitment to a design decision that you may wish to change in the future, so minimize the number of interfaces you publish. For each one, consider whether the benefit is worth the cost. Sometimes it will be, but it’s a decision to be made carefully. Postpone the decision as long as possible to allow your design to improve and settle.

In some cases, as with teams creating a library for third-party use, the entire purpose of the product is to provide a published interface. In that case, design your interface carefully, up front, rather than using evolutionary design. The smaller the interface, the better—it’s much easier to add to your interface than to remove mistakes.

As Erich Gamma said in [Venners2005], “When it comes to exposing more API [in Eclipse, the open source Java IDE], we do that on demand. We expose API gradually...when we see the need, then we say, OK, we’ll make the investment of publishing this as an API, make it a commitment. So I really think about it in smaller steps; we do not want to commit to an API before its time.”

Performance Optimization

Modern computers are complex. Even reading a single line of a file from a disk requires the coordination of the CPU, multiple levels of CPU cache, the operating system kernel, a virtual filesystem, a system bus, the hard drive controller, the hard drive cache, OS buffers, system buffers, and scheduling pipelines. Every component exists to solve a problem, and each has certain tricks to squeeze out performance. Is the data in a cache? Which cache? How’s your memory aligned? Are you reading asynchronously or are you blocking?

Your intuition about performance is almost always going to be wrong.

In other words, your intuition about performance is almost always going to be wrong. Optimization tricks based on 20-line performance tests don’t cut it. The only way to optimize modern systems is to take a holistic approach. You have to measure the real-world performance of the code, find the hot spots, and optimize from there. Don’t guess. Don’t make assumptions. Just profile the code.

String concatenation, function calls, and boxing/unboxing—the things that feel expensive—are usually a nonissue. Most of the time, your performance will be dominated by the network, database, or filesystem. If not, it’s likely to be a quadratic algorithm. Rarely, it will be thread contention or nonsequential memory access inside a tight loop. But the only way to be sure is to measure real-world performance. Don’t guess. Profile, profile, profile.

In the meantime, ignore what you’ve heard about optimization tricks. When (not if) you need to change your code—whatever the reason—it will be easier to do if it’s simple and straightforward.


What if we know we’re going to need a story? Shouldn’t we put in a design hook for it?

Your plan can change at any time. Unless the story is part of your current week’s work, don’t put the hook in. The story could disappear from the plan, leaving you stuck with unnecessary complexity.

More importantly, evolutionary design actually reduces the cost of changes over time, so the longer you wait to make the change, the cheaper it will be.

What if ignoring a story will make it harder to implement in the future?

If ignoring a potential story could make it more difficult, look for ways to eliminate that risk without explicitly coding support for the story. The “Risk-Driven Architecture” section has more details.

Reflective Design
Incremental Design


Simple design requires continuous improvement through refactoring, reflective design, and incremental design. Without them, your design will fail to evolve with your requirements.

Don’t use simple design as an excuse for poor design. Simplicity requires careful thought. Don’t pretend “simple” means the code that’s fastest or easiest to implement.

Collective Code Ownership
Pair Programming
Mob Programming

Collective code ownership and pairing or mobbing, though not strictly necessary for simple design, will help your team devote the brainpower needed to create truly simple designs.


When you create simple designs:

  • Your team doesn’t write code in anticipation of future stories.

  • Your team finishes work more quickly, because they don’t build things that aren’t needed today.

  • Your design supports arbitrary changes easily.

  • Although new features might require a lot of new code, changes to existing code are localized and straightforward.

Alternatives and Experiments

Reflective Design

The classic approach to design is to anticipate future changes and build a design that preemptively supports them. I call this “predictive design,” in contrast to reflective design, which I’ll discuss next.

Although many teams have had success using predictive design, it relies on correctly predicting new requirements and stories. If your expectations are too far off, you might have to rewrite a lot of code that was based on bad assumptions. Those changes could be too far-reaching to be done economically, resulting in long-term flaws in your codebase.

Generally, I’ve found the simple design techniques described in this practice to work better than predictive design, but you can combine the two. If you do use a predictive design approach, it’s best to hire programmers who have a lot of experience in your specific industry. They’re more likely to correctly predict changes.

Further Reading

Martin Fowler has a collection of his excellent IEEE Design columns online. Many of these columns discuss core concepts that help in creating a simple design.

The Pragmatic Programmer: Your Journey to Mastery [Hunt2019] contains a wealth of design information that will help you create simple, flexible designs.

Implementation Patterns [Beck2007] gets down into the details, with chapters dedicated to subjects such as state, behavior, and methods. If you can look past its slightly dated, Java-centric examples, you’ll find a wealth of thought-provoking topics.

Share your thoughts about this excerpt on the AoAD2 mailing list or Discord server. Or come to the weekly book club!

For more excerpts from the book, see the Second Edition home page.

If you liked this entry, check out my best writing and presentations, and consider subscribing to updates by email or RSS.