AoAD2 Practice: Incremental Design

Book cover for “The Art of Agile Development, Second Edition.”

Second Edition cover

This is a pre-release excerpt of The Art of Agile Development, Second Edition, to be published by O’Reilly in 2021. Visit the Second Edition home page for information about the open development process, additional excerpts, and more.

Your feedback is appreciated! To share your thoughts, join the AoAD2 open review mailing list.

This excerpt is copyright 2007, 2020, 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 makes a challenging demand of its programmers: every week or two, programmers should finish four to ten 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.

As a programmer, you must be able to start and finish stories, from scratch, in a single week. No advance preparation is possible. You can’t set aside several weeks for establishing technical infrastructure. 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 technical infrastructure incrementally, in small pieces, as you deliver stories.

It’s Not Just Coding

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. Design quality and development costs are joined at the hip: Code is well-designed when the costs of change are low.

Quality is highly situational, of course. The cost of change depends on the capabilities of the software, the capabilities of the programmers, and the specific changes being made.

Design is so important, we do it all the time.

The secret behind successful Delivering zone teams, therefore, is that they never stop designing. Delivering practices might seem to be about programming, at first glance, but most of them are about design. As Ron Jeffries used to say about Extreme Programming, design is so important, we do it all the time.

Allies
Pair Programming
Mob Programming
Ubiquitous Language
Test-Driven Development
Collective Code Ownership
Refactoring
Continuous Integration

Pairing and mobbing dedicate at least half of the programmers on your team to thinking about design. Ubiquitous language is about designing code to reflect domain experts’ thinking. Test-driven development encourages you to think about and improve your design at nearly every step. Collective code ownership expects people to improve the design. Refactoring makes it possible. Continuous integration allows people to make changes without stepping on each others’ toes.

Delivering teams constantly talk about design, especially when pairing and mobbing. In fact, that’s what nearly all of the conversations are about. Some of them 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.”

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

How Incremental Design Works

Allies
Simple Design
Reflective Design

Incremental design is the driving force behind evolutionary design. It works in concert with simple design and reflective design:

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

  2. When the design doesn’t do everything you need right now, incrementally add to it. (Incremental design.)

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

To be specific, 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.

This 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.

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.

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.

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.

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. [Evans 2003] (Chapter 8.) Breakthroughs happen at all levels of the design, from methods and functions to architectures.

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 “A TDD Example” on p.XX shows, your code starts out completely specific, often to the point of hard-coding the answer, but then it gradually becomes more generic as additional tests are added.

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 “Refactoring in Action” on p.XX, 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, though, transformLetter() became dramatically simpler.

Across classes and modules

When TDD is performed well, the design of individual modules and classes is beautiful: they’re simple, elegant, and convenient to use. This isn’t enough. Without attention to the interaction between modules and classes, the overall design will be muddy and confusing.

Allies
Pair Programming
Mob Programming

During TDD, navigators should also consider the wider scope. Ask yourself these questions: are there similarities between the code you’re implementing 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—bring up the issue, discuss solutions with your driver, and 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. Here, I’m referring to application architecture, by which I mean the recurring patterns in your code. Not formal patterns in the Design Patterns [Gamma et al. 1995] sense, but the repeated conventions throughout your codebase. For example, web applications are often implemented so that every endpoint has a route definition and controller class, and the controllers are often each implemented with a Transaction Script ([Fowler 2002], 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. To apply evolutionary design ideas to system architecture, see “Evolutionary Architecture” on p.XX.

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 by coming up 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 “Key Idea: The Last Responsible Moment” on p.XX.) 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 on 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 only worth doing if it’s a significant-enough improvement to be worth the cost.

Although changes to your architecture may be tedious, they aren’t usually difficult once you’ve identified the new architectural pattern. Start by trying out the new pattern in just 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 bring other classes and modules into compliance.

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 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 Refactoring

Architecture may seem too essential not to design up-front. Although some problems do appear to be too expensive to change incrementally, such as choice of programming language, I’ve found that many “architectural” concerns 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 consider them from the beginning. I disagree; I’ve dealt with them all incrementally. [Shore 2004a]

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

Your power lies in your ability to choose which refactorings to work on.

Your power lies in your ability to choose which refactorings to work on. No design is perfect. You will always have more opportunities to refactor than time to do it. And although it would be inappropriate to implement features your customers haven’t asked for, you can direct your refactoring efforts toward reducing risk. So choose refactorings that also reduce architectural risks.

To be specific, think about the problems you think you might face and refactor to improve the parts of your design that are related to those problems. For example, if your code had a lot of duplication in the way it formatted currency, localizing currencies would be expensive. You’d have to find every piece of code that dealt with currency formatting and fix it. A risk-driven solution would be to refactor your currency code so the concept had its own class, as described in “Once and Only Once” on p.XX, then move the currency formatting into that class, as shown in figure “Use Risk to Drive Refactoring”.

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 existing design. For example, you wouldn’t actually internationalize the Currency class until you were working on a story that needed it. Once you’ve eliminated duplication around a concept, changing its implementation will be just as easy later as it is now.

A team I worked with replaced an entire database connection pooling library with our own hand-built version in half a pair-day. (This was in 2001, when the library ecosystem was much less mature. The library we replaced had some obscure thread safety bugs.)

Although we didn’t anticipate this need, it was still easy because we had previously eliminated all duplication around database connection management. There was just one method in the entire system that created, opened, and closed connections, which made test-driving our own connection pool manager almost trivially easy. Most of the time was spent on figuring out how to test-drive the thread safety code.

Another way to reduce architecture risk is to ask your customers to schedule stories that will allow you to work on the risky area. For example, to address the internationalization risk, you could create a story such as “Localize application for Spain” (or any country that has different localization rules than your own). This story has real customer value, but also addresses the risk.

Your customers have final say over story priorities, however, and their sense of risk and value may not match yours. Don’t feel too bad if that’s the case; you can still use refactorings to reduce architecture risk.

Questions

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

Just the opposite, actually, in my experience. There are two reasons for this. First, because incremental design only implements enough code to satisfy the current requirements, you start delivering features 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 and eventually leads to another breakthrough—sort of like walking through a hilly forest in which the top of each hill reveals a new, higher hill you couldn’t see before. This continual series of breakthroughs substantially improves your design.

Aren’t hill-climbing algorithms vulnerable to local maxima?

Luckily, incremental design isn’t an algorithm. It’s a technique performed by humans, who can use their creativity and ingenuity to break out of local maxima. That’s what a breakthrough is: climbing to the top of a hill, seeing across the valley, and realizing that there’s a better hill within flying distance.

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

Sometimes a breakthrough will lead you to see a completely new way of approaching your design. When this happens, refactoring may seem like backtracking, especially if the refactoring simplifies your design, which it often does. It’s not really backtracking, though—if you were able to think of the simpler approach sooner, you would have. So don’t feel bad. 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 comprehensive design documentation. How can we satisfy this requirement if we don’t design everything up front?

Ask your customers to schedule documentation with a story, then estimate and deliver it as you would any other story. (See “As-Built Documentation” on p.XX.) Remind them that the design will change over time. The most effective option is to schedule documentation stories when the codebase is about to be retired or put in maintenance mode.

If your organization requires up-front documentation, the only way to provide it is to engage in up-front design. Try to keep your design efforts small and simple. If you can, use incremental design once you actually start coding.

Prerequisites

Allies
Simple Design
Reflective Design
Refactoring
Pair Programming
Mob Programming
Energized Work
Slack
Test-Driven Development
Team Room
Alignment

Incremental design depends on simple design and reflective design to keep code simple and steadily improve. It also requires a commitment to continuous daily improvement. This requires self-discipline and a desire for high-quality code, which not everybody has.

Luckily, you don’t need everyone to feel this way. In my experience, teams do well even if only one respected person on the team pushes for steady improvement. 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.

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 that 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 change the third parties’ code when you change the interface. (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 who 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 stage, 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. That’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 iteration, rather than relying on simple design and refactoring as incremental design does.

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 only works well if your plans don’t change, which is the opposite of how Agile teams normally work.

Further Reading

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

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

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

Share your feedback about this excerpt on the AoAD2 mailing list! Sign up here.

For more excerpts from the book, or to get a copy of the Early Release, 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.