AoAD2 Practice: Incremental 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.

Incremental Design

Audience
Programmers

We design while we deliver.

Agile teams make a challenging demand of their programmers: every week or two, the team is expected to finish 4–10 customer-centric stories. Every week or two, customers may revise the current plan and introduce entirely new stories, with no advance notice. This regimen starts on the very first week.

For programmers, this means you must be able to implement stories, from scratch, in a single week. Because the plan can change at nearly any time, you can’t set aside several weeks for establishing design infrastructure—that work might be wasted when plans change. You’re expected to focus on delivering customer-valued stories instead.

This sounds like a recipe for disaster. Fortunately, incremental design allows you to build your designs incrementally, in small pieces, as you deliver stories.

Never Stop Designing

Code is well-designed when the costs of change are low.

Computers don’t care what your code looks like. If the code compiles and runs, the computer is happy. Design is for humans: specifically, to allow programmers to easily understand and change the code. Code is well-designed when the costs of change are low.

Allies
Pair Programming
Mob Programming
Test-Driven Development

The secret behind successful Delivering zone teams, therefore, is that they never stop designing. As Ron Jeffries used to say about Extreme Programming, design is so important, we do it all the time. With pairing or mobbing, at least half the programmers on your team are dedicated to thinking about design, and test-driven development encourages you to improve your design at nearly every step.

Delivering teams constantly talk about design, especially when pairing and mobbing. Some conversations are very detailed and nitpicky, such as, “What should we name this method?” Others are much higher-level, such as, “These two modules share some responsibilities. We should split them apart and make a third module.” They constantly switch back and forth between details and the big picture.

Design discussions don’t have to be restricted to whomever you’re currently working with. Have larger group discussions as often as you think is necessary, and use whatever modeling techniques you find helpful. (See the “Drop in and Drop Out” section.) Try to keep them informal and collaborative. Simple whiteboard sketches work well.

How Incremental Design Works

Allies
Simple Design
Reflective Design

Incremental design works in concert with simple design and reflective design:

  1. Simple design: Start with the simplest design that could possibly work.

  2. Incremental design: When the design doesn’t do everything you need, incrementally add to it.

  3. Reflective design: Every time you make a change, improve the design by reflecting on its strengths and weaknesses.

In other words, when you first create a design element, whether it’s a new method, a new class, or even a new architecture, be completely specific. Create a simple design that solves exactly the problem you face at the moment and nothing else, no matter how easy it may seem to solve more general problems.

For example, when I implemented the networked mouse pointer shown in the “Real-World Evolutionary Design” figure, I created a networking class with a method to send the pointer location to the server. All it did was call my networking library:

sendPointerLocation(x, y) {
  this._socket.emit("mouse", { x, y });
}

Being so specific is difficult! Experienced programmers think in abstractions. In fact, the ability to think in abstractions is often the sign of a good programmer. Avoiding abstractions and coding for one specific scenario will seem strange, even unprofessional.

Do it anyway. Waiting to introduce abstractions will allow you to create designs that are simpler and more powerful. You won’t have to wait long.

The second time you add to a design element, modify the design to make it more general—but only general enough to solve the two problems it needs to solve. Next, review the design and make improvements. Simplify and clarify the code.

To continue the example, after sending pointer events, I had to receive them from the server. This led me to introduce ClientPointerEvent and ServerPointerEvent classes, rather than hardcoding the event object. The code became:

sendPointerLocation(x, y) {
  this._socket.emit(
    ClientPointerEvent.EVENT_NAME,
    new ClientPointerEvent(x, y).toSerializableObject()
  );
}

A little more complicated, but a little more flexible.

The third time you add to a design element, generalize it further—but again, just enough to solve the three problems at hand. A small tweak to the design is usually enough. It will be pretty general at this point. Again, review the design, simplify, and clarify.

My next step in the example was to network draw events. I started by making a sendDrawEvent(event) method. It was an experiment in moving responsibility for creating events into the application-level code. That worked well, so I generalized sendPointerLocation(x, y) and sendDrawEvent(event) down to sendEvent(event):

sendEvent(event) {
  this._socket.emit(event.name(), event.toSerializableObject());
}

Continue this pattern. By the fourth or fifth time you work with a design element—be it a method, a module, or something bigger—you’ll typically find that its abstraction is perfect for your needs. Best of all, because your design was the result of combining practical needs with continuous improvement, the design will be elegant and powerful.

Levels of Design

Incremental design happens at all levels of the design, from within a class or module, to across classes and modules, and even at the level of the application architecture.

At each level, quality tends to improve in bursts. Typically, you’ll incrementally grow a design for several cycles, making minor changes as you go. Then something will give you an idea for a new design approach, which will require a series of more substantial refactorings to support it. Eric Evans calls this a breakthrough. [Evans2003] (ch. 8)

Within a class or module
Allies
Test-Driven Development
Refactoring

If you’ve practiced test-driven development, you’ve practiced incremental design, at least at the level of a single module or class. You start with nothing and build a complete solution, layer by layer, making improvements as you go. As the “A TDD Example” section shows, your code starts out completely specific, often to the point of hardcoding the answer, but then it gradually becomes more generic as additional tests are added.

Within a class or module, refactorings occur every few minutes, during the “Refactoring” step of the TDD cycle. Breakthroughs can happen several times per hour, and often take a matter of minutes to complete. For example, there’s a breakthrough at the end of the “Refactoring in Action” section, when I realized that the regular expression allowed me to simplify the transformLetter() function. Notice how, up to that point, the refactorings resulted in small, steady improvements. After the breakthrough, transformLetter() became dramatically simpler.

Across classes and modules

When using TDD, it’s easy to create beautifully designed modules and classes. That isn’t enough. You also need to pay attention to the interaction between modules and classes. If you don’t, the overall design will be muddy and confusing.

Consider the wider scope as you work. Ask yourself these questions: are there similarities between this code and other parts of the system? Are responsibilities clearly defined and concepts clearly represented? How well does the module or class you’re currently working on interact with other modules and classes?

When you see a problem, add it to your notes. During one of the refactoring steps of TDD—usually, when you’ve come to a good stopping place—take a closer look at solutions, then refactor. If you think your design change will significantly affect other members of the team, take a quick break to discuss it around a whiteboard.

Don’t let design discussions turn into long, drawn-out disagreements. Follow the ten-minute rule: if you disagree on a design direction for 10 minutes, try one and see how it works in practice. If you have a particularly strong disagreement, split up and try both as spike solutions. Nothing clarifies a design decision like working code.

Ally
Slack

Cross-module and cross-class refactorings happen several times per day. Depending on your design, breakthroughs may happen a few times per week and can take several hours to complete. (Nonetheless, remember to proceed in small steps.) Use your slack to complete breakthrough refactorings. In some cases, you won’t have time to finish all the refactorings you identify. That’s okay. As long as the design is better at the end of the week than it was at the beginning, you’re doing enough.

For example, when working on a small content management engine, I started by implementing a single Server class that served static files. When I added support for translating Jade templates to HTML, I started out by putting the code to do so in Server, because that was the simplest approach. It got ugly after I added support for dynamic endpoints, so I factored the template responsibilities into a JadeProcessor module.

That led to the breakthrough that static files and dynamic endpoints could similarly be factored into StaticProcessor and JavaScriptProcessor modules, and that they could all depend on the same underlying SiteFile class. That cleanly separated my networking, HTML generation, and file-handling code.

Application architecture

“Architecture” is an overloaded word. In this case, I’m referring to the recurring patterns in your team’s code. Not formal patterns in the Design Patterns [Gamma1995] sense, but the repeated conventions throughout your codebase. For example, web applications are often implemented so every endpoint has a route definition and controller class, and the controllers are often each implemented with a Transaction Script.1

1To learn about Transaction Script and Domain Model architecture, see [Fowler2002] (ch. 9).

Those recurring patterns embody your application architecture. Although they lead to consistent code, they’re also a form of duplication, which makes changes to your architecture more difficult. For example, changing a web application from using a Transaction Script approach to a Domain Model approach requires updating every single endpoint’s controller.

I’m focusing on application architecture here, which is specifically about the code your team controls. There’s also system architecture, which involves all the components of your deployed software, such as third-party services, network gateways, routers, and so forth. To apply evolutionary design ideas to system architecture, see the “Evolutionary System Architecture” practice.

Be conservative in introducing new architectural patterns. Introduce just what you need for the amount of code you have and the features you support at the moment. Before introducing a new convention, ask yourself if you really need the duplication. Maybe there’s a way to isolate the duplication to a single file, or to allow different parts of the system to use different approaches.

For example, in the content management engine I described previously, I could have started out with a grand strategy for supporting different templating and markup languages. That was meant to be one of its distinguishing features, after all. But instead, I started by implementing a single Server class, and let the code grow into its architecture over time.

Even after I introduced classes for each type of markup, I didn’t try to make them follow a consistent pattern. Instead, I allowed them to each take their own unique approach—whichever was simplest in each case. Over time, some of those approaches worked better than others, and I gradually standardized my approach. Eventually, the standard was so stable, I converted it into a plug-in architecture. Now I can support a new markup language or template just by dropping a file in a directory.

Because architectural decisions are hard to change, it’s important to delay those commitments. (See the “Key Idea: The Last Responsible Moment” sidebar.) The plug-in architecture I mentioned happened years after the content management engine was first created. If necessary, I could have added plug-in support sooner, but I didn’t need to, so I took it slow. That allowed me to standardize an approach that had a lot of experience and wisdom baked into it, and as a result, it hasn’t needed additional changes.

In my experience, breakthroughs in architecture happen every few months, although I expect this to vary widely by team. Refactoring to support the breakthrough can take several weeks or longer because of the amount of duplication involved. As with all breakthroughs, it’s worth doing only if it’s a significant enough improvement to be worth the cost.

Changes to your architecture can be tedious, but they aren’t usually difficult, once you’ve identified the new architectural pattern. Start by trying out the new pattern in one part of your code. Let it sit for a while—a week or two—to make sure the change works well in practice. When you’re sure it does, bring the rest of the system into compliance with the new approach. Refactor each class or module you touch as you perform your everyday work, and use some of your slack to update other classes and modules.

It’s easier to expand an architecture than to simplify one that’s too ambitious.

Keep delivering stories while you refactor. Although you could take a break from new development to refactor all at once, that would disenfranchise your on-site customers. Balance technical excellence with delivering value. Neither can take precedence over the other. This may lead to inconsistencies within the code during the changeover, but fortunately, that’s mostly an aesthetic problem—more annoying than truly problematic.

Introducing architectural patterns gradually, only as needed, helps reduce the need for architectural refactorings. It’s easier to expand an architecture than to simplify one that’s too ambitious.

Risk-Driven Architecture

Architecture is too essential to design up front.

Architecture may seem too essential not to design up front. I would argue the opposite: it’s so essential, it should be designed as late as possible, when you have the most information and can make the best decisions.

Although some problems do appear to be too expensive to change incrementally, such as choice of programming language, I’ve found that many “architectural” decisions are actually easy to change if you eliminate duplication and embrace simplicity. Distributed processing, persistence, internationalization, security, and transaction structure are commonly considered so complex that you must design them from the beginning. I disagree; I’ve dealt with them all incrementally. [Shore2004a]

What do you do when you see a hard problem coming? For example, what if your stakeholders insist you not spend any time on internationalization, but you know that it’s coming and it’s only going to get more expensive to support?

The difficulty of architectural additions depends on the quality of your design. For example, internationalizing currency formatting is difficult when the formatting code is duplicated throughout your application. But if the formatting code is centralized, internationalization it is easy—or, at least, no more difficult than doing so from the beginning.

Allies
Reflective Design
Slack

This is where risk-driven architecture comes in. In any given week, you’ll have enough slack for a certain amount of refactoring. When you decide how to use your slack, give priority to architectural risks. For example, if your code has a lot of duplication in the way it formats currency, internationalization is at risk. Prioritize refactorings that eliminate the duplication, as shown in the “Use Risk to Drive Refactoring” figure.

Two UML class diagrams. The first is labelled “Risk. Every class duplicates the currency rendering algorithm. If it is internationalized, changing it will be difficult and expensive.” It shows three UI classes, each with a “renderCurrency” method. A large arrow transitions to the second diagram, which is labelled “No Risk. The currency rendering algorithm is only implemented in the Currency class. If it is internationalized, only one method needs changing.” It shows the three UI classes depending on a Currency class, which has a single “render” method.

Figure 1. Use risk to drive refactoring

Limit your efforts to improving your design. Don’t add new features. For example, while it’s okay to refactor the Currency class to make it easier to internationalize in the future, don’t actually internationalize it until you’re working on an internationalization story. Once it’s refactored, it’ll be just as easy to internationalize later as it is now.

Questions

Isn’t incremental design more expensive than up-front design?

Just the opposite, in my experience. There are two reasons for this. First, because incremental design implements only enough code to satisfy your current story, you start delivering much more quickly with incremental design. Second, when a future story changes, you haven’t coded anything to support it, so you haven’t wasted any effort.

Even if requirements never changed, incremental design would still be more effective, because it leads to design breakthroughs on a regular basis. Each breakthrough allows you to see new possibilities, which eventually leads to another breakthrough. This continual series of breakthroughs substantially improves your design.

Don’t breakthroughs result in wasted effort as you backtrack?

You don’t really backtrack. Sometimes a breakthrough will lead you to a radically simpler design, which can feel like backtracking, but it’s not really. If you were able to think of the simpler approach sooner, you would have. Simplicity is hard, and you’ll have to iterate your design to get there. The nature of breakthroughs, especially at the class and architectural level, is that you usually don’t see them until you’ve lived with your current design for a while.

Our organization (or customer) requires design documentation. How can we satisfy this requirement?

If you can convince your organization to wait, you can provide “as-built” documentation. (See the “As-Built Documentation” section.) It’s cheaper to produce and more accurate than up-front documentation. And, because it’s created after you release, you can release sooner. Cheaper, better, and faster? That could be compelling.

If not, the only way to provide up-front documentation is to engage in up-front design, and probably up-front requirements analysis, too. However, you may not need to design everything. Your stakeholders may just need you to commit to a particular part of the design for the purpose of coordinating with another group, or for governance purposes. Work with them to identify the smallest subset that needs up-front design, then use incremental design for everything else.

Prerequisites

Allies
Pair Programming
Mob Programming
Collective Code Ownership
Energized Work
Slack

Incremental design requires self-discipline, a commitment to continuous daily improvement, and a desire for high-quality code. And, of course, the skill to apply it at the right times. These traits aren’t shared by everyone.

Luckily, you don’t need everyone to share these traits. In my experience, teams do well even when one person coaches the rest of the team in using incremental design. However, you do need pairing or mobbing, collective code ownership, energized work, and slack as support mechanisms. They help with self-discipline and allow people who are passionate about code quality to influence all parts of the code.

Allies
Simple Design
Reflective Design
Test-Driven Development
Team Room
Alignment

Incremental design depends on simple design and reflective design. Test-driven development is also important. Its explicit refactoring step, repeated every few minutes, gives people continual opportunities to stop and make design improvements. Pairing and mobbing help in this area, too, by making sure that at least half the team’s programmers, as navigators, always have an opportunity to consider design improvements.

Be sure your team communicates well via a shared team room, either physical or virtual, if you’re using incremental design. Without constant communication about cross-module, cross-class, and architectural refactorings, your design will fragment and diverge. Agree on coding standards during your alignment discussion so everyone follows the same patterns.

Anything that makes continuous improvement difficult will make incremental design difficult. Published interfaces are an example; because they are difficult to change after publication, incremental design usually isn’t appropriate for interfaces used by third parties, unless you have the ability to change the third parties’ code. (But you can still use incremental design for the implementation of those interfaces.) Similarly, any language or platform that makes refactoring difficult will also inhibit your use of incremental design.

Finally, some organizations constrain teams’ ability to use incremental design, such as organizations that require up-front design documentation or that have rigidly controlled database schema. Incremental design may not be appropriate in these situations.

Indicators

When you use incremental design well:

  • Every week advances the software’s capabilities and design in equal measure.

  • You have no need to skip stories for a week or more to focus on refactoring or design.

  • Every week, the quality of the software is better than it was the week before.

  • As time goes on, the software becomes increasingly easy to maintain and extend.

Alternatives and Experiments

If you’re uncomfortable with the idea of incremental design, you can hedge your bets by combining it with up-front design. Start with an up-front design phase, then commit completely to incremental design. Although this will delay the start of your first story, and may require some up-front requirements work, this approach has the advantage of providing a safety net without incurring too much risk.

That’s not to say that incremental design doesn’t work—it does! But if you’re not comfortable with it, you can hedge your bets by starting with up-front design. It’s how I first learned to trust incremental design.

Other alternatives to incremental design are less successful. One common approach is to treat Agile as a series of mini-waterfalls, performing a bit of up-front design at the beginning of each iteration. Unfortunately, these design sessions are too short and small to create a cohesive design on their own. Code quality will steadily degrade. It’s better to embrace incremental design.

Another alternative is to use up-front design without also using incremental design. This works well only if your plans don’t change, which is the opposite of how Agile teams normally work.

Wait until you’re comfortable with incremental design before experimenting. Once you are, see how far you can push it. Don’t just reduce your up-front design; reduce the amount of design speculation you perform in your head, too. What’s the least amount of advance design thinking you can get away with? Find the limits of incremental design.

Further Reading

“Is Design Dead?” [Fowler2004] discusses incremental design from a slightly skeptical perspective.

“Continuous Design” [Shore2004a] discusses my experiences with difficult challenges in incremental design, such as internationalization and security.

“Evolutionary Design Animated” [Shore2020a] discusses my real-world experience with incremental design by visualizing the changes in a small production system.

Share your thoughts about this excerpt on the AoAD2 mailing list or Discord server. For videos and interviews regarding the book, see the book club archive.

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.