So far, I’ve pointed out the disparity between the short-run focus of agile development and the long-run focus of a stable design. Now we need to reconcile the two. The way to do that is in my series title: Iterate, Iterate, Iterate.
Humans are great at getting things wrong. We make mistakes all the time, fail to take circumstances into consideration, generally try not to think hard about anything, and don’t really learn anything until we fail at it. The best way of getting a good long-term design right is to do it in a way that corresponds with these human failings. Putting together a great architecture a priori is, well, tricky. For a trivial problem it isn’t impossible, but being able to extend a design to cover unforeseen circumstances requires a lot of effort and a little bit of luck. So let’s take advantage of circumstances as we learn them and apply lessons after failure. With design, that comes through constant re-design.
“But wait!” one might argue, “You’re talking about constantly re-designing in a series where you’re trying to get design right. What gives?” The answer is simple: a design is good in as much as it is useful. By “useful,” think of a combination of features: we want our design to be unobtrusive, fit with our standard development workflow, be extensible without being incomprehensible, and allow people to solve problems with a minimum of struggle. As soon as a design fails across these dimensions, it is no longer useful. For example, suppose that we have a framework which serves us well, but with recent changes, we regularly need to perform extra hours of tedious manual labor. This framework causes an undue and unnecessary struggle, and it is time for a new design. Sometimes, that new design just means extending our current design or smoothing off some rough edges; other times, we need to dig deep into the application’s foundations and start over. That second type of re-design is scary to a lot of people, especially in management. A code base with years in the making isn’t something you can re-write in a day and at best, this kind of work has long-run benefits but short-run costs.
The best solution I have to this problem is to prototype frequently, iterating on designs. You start out with the big, scary code base and try to apply some rules to it. If you have old design documents, you’re in luck—those can (hopefully) provide a decent starting point for you; otherwise, it’s going to take reverse-engineering the code and understanding at a fundamental level the type of software that can give you nightmares. But here’s the nice thing: you can build upon your prototypes in an iterative fashion. You can start with some of the easier scenarios and, as you find something which does not fit your design, throw away your design and start again from the basis that you need to incorporate this as well. To put it another way, suppose you have code which meets a set of business requirements: { x, y, z, A, B, C }, where the first three are “simple” and the next three are “complex.” After some initial research, you have been able to derive the simple cases: { x, y, z }. Once you have rules, you can build a prototype design which satisfies the simple rules. With that prototype design in place, you can continue to investigate the scary code base and dig out the remainder of the business rules. Once you derive rule A, see if A fits cleanly in your design. If it does, great: you now have a design which handles { x, y, z, A }. If it doesn’t, throw away your design and think of one which does cleanly handle { x, y, z, A }. Then repeat this for the rest of the business rules. Then, see if you can think of some extra rules like { D, E, F } or { A’, B’, C’ } and figure out what kinds of design changes would be necessary for these. A good design should be able to support some reasonable-but-non-existent rules; an ideal design would handle a broad space elegantly, leaving your development team in a good place to grow. But here’s the funny thing about ideal designs: what’s ideal today probably won’t be ideal tomorrow. Be willing to throw away your design and start over with the first business rule…but this time, at least you’ll have enumerated business rules and a better idea of how everything works.
Doing this sounds tedious and, to an extent, it really is. This is part of why design should also be relatively local: with these prototypes, you don’t intend to solve all the world’s problems; instead, focus on a specific problem space and try to cover as much of that as you can. Throw away designs when they become too difficult to understand or implement, or if they don’t solve important parts of your problem space.
Another thing you’ll find as you prototype and design is that our initial designs tend to be terrible. One common statement in Agile is that it’s backwards to try to do the one thing that requires the most information (design and understanding business rules) during the point in time in which we have the least information: before any development begins. This is a fair point and should drive design considerations. Our first design will be the design with the least known, and so it probably will have holes. As we expose these holes, we can patch a fundamentally sound design, but we should not be afraid to throw the design (and implementing code) away. The first time a developer writes a procedure, class, or function, the problems tend to be more along the lines of “How can I do this?” Developers tend to hack things together until they work and, if there’s enough time available, refactor the code to make it look marginally nicer.
Instead, imagine that, as soon as your developer has that first solution to the problem, you take all of the code that developer just wrote and delete it. Simulate a power outage—all of those changes are gone and the developer has to start over. Most developers will be livid, but the astute ones will notice something: their final code the second time around is cleaner. The reason is that the first time around, the developer was looking at the simple problem of how to solve a specific problem; the second time around, the developer knows how and is thinking about how _best_ to solve the problem. Well, the same thing holds true for design. The first time around, we’re looking at how to solve problems { x, y, z, … }, but once that prototype is done and we’ve satisfied the business rules, we can focus on how best to satisfy the rules.
In practical terms, I like to think of three separate levels of design. The first design I call “version 0.5.” This version starts out as a pen-and-paper exercise, working through basic flows in as much detail as you need. From there, you create a toy design. You understand that the toy is a model and not production-worthy. Instead, it lets you try out different scenarios and see how well the design stands up to expectations. Unfortunately, for a lot of companies, this toy model becomes production and developers spend years struggling to patch the toy or working around its flaws. Don’t let your toy models out into production or feed them after midnight.
Supposing you avoid the toy model gremlin problem above, you can start work on “version 1.0.” Version 1.0 is a total re-write of the 0.5 design. Feel free to keep as many notes on requirements, ideas of what failed, and pain points from the previous design as you can, but your goal with version 1.0 is to figure out how best to solve the problem, not just coming up with a solution. This means that you generally don’t want to re-use code and implementation artifacts from version 0.5; instead, you want to apply the lessons learned from version 0.5 to a relatively clean slate.
If version 0.5 is the toy, version 1.0 is the prototype. By the time you’ve worked through the second design, the problem should be clearer and you have something which might potentially work. This version of the design should be at least as good as the toy version and hopefully better. From here, the next step depends upon whether we’re looking at “greenfield” or “brownfield” development, where “greenfield” development means that we’re working on new code for a new project and “brownfield” development means that we’re working within an established, stable product. If you’re working on brownfield development, you can run your version 1.0 design+code side-by-side with the current production code. By this point, your product should be stable enough to compare with current production and you can simulate various loads to test weird cases that you might find in production. Depending upon the process, you might even be able to modify production so that both systems get the same set of inputs and you can track each system’s outputs, as well as metrics like failure rate and performance. Note that for brownfield development, version 1.0 is not truly production. Instead, it is somewhere between research & development and your final product.
In contrast to brownfield development, a version 1.0 design under greenfield development circumstances cannot run side-by-side with a current product because there isn’t any such product. Instead, a version 1.0 greenfield design could become an alpha program, letting end users try out a system and offer up ideas while understanding that this is not the final version and that they should not rely on this system for critical needs. This “version 1.0” design is something which a lot of companies ignore, in part because getting people to offer up good feedback is difficult. Your end users typically don’t want to be guinea pigs; they just want their applications to work correctly, and even if the development team makes it clear that this is a transitory phase, end users will find the bugs and design flaws and assume the product sucks. This is where having an established alpha/beta program can be very important for companies. By partnering with a few people or groups who try to get the most out of your product and are willing to deal with alpha pain, your developers will get important feedback early in the process, similar to what they would get with brownfield development results.
Regardless of whether this product is brownfield or greenfield development, version 1.0 is a stepping stone rather than a goal. Instead, the real goal is the version 2.0 design. Just like going from 0.5 to 1.0, going from 1.0 to 2.0 means abandoning already-written code and going back to the drawing board. This is even more painful than giving up the code in version 0.5, but just as important. In version 0.5, you collected business rules and other requirements, banged together a design, captured information on where the design fell short. In version 1.0, you took those rules, requirements, and shortcomings and came up with a better system. Running version 1.0 against production or an alpha test group gives you another list of shortcomings, as well as a critical reality check: did you miss any requirements? Did you get the business rules correct? Can you support the first wave of end user requests?
Based on this, you are finally ready to start designing version 2.0. At this point, take the code from version 1.0 and hide it, like you did for version 0.5. Go back to the drawing board for design and work through all of the problems you’ve seen thus far, extrapolating from them expectations of future problems. Once this design is in place, get back to development. By this point, development should be relatively simple, as you’ve worked through three separate designs and your development team has worked on at least one version of the product. As you work on this version, it might make sense to open the product up to beta testers to help make sure that your design is on the right track. Once you get through the beta process, you now have a production-worthy application which will be a lot easier to maintain than the version 0.5 toy that most companies push out.
This particular process feels slower than “do it all in one shot,” and it is. Design takes time and throwing away code between versions means that each version will take longer. With these costs come corresponding benefits. First, your development team will grow to love this process. They may not like throwing away code and repeating work early on, but as the number of trouble tickets decreases and maintenance becomes significantly easier, you are likely to see much less developer burnout and churn; with good developers sticking around longer, they gain more business knowledge and become even more valuable. In addition to developers loving the process, end users will generally be happier. They see the polished, working version of your application and get feature requests and bug fixes in faster than would be the case with a poor design. There is some additional up-front time expense, but end users won’t even notice it. Think of it this way: with brownfield development, end users will be using the current application until the version 2.0 design of the new application is done; by contrast, with greenfield development, end users won’t be using your app at all (outside of the alpha program). In either case, taking a bit more time at the beginning of the project does not hurt end users’ workflows.
Wrapping this post up, the most important take-away is that through a process of continuous design improvement, you can be Agile and still design good products. Design is just as much an on-going process as development, and you should be just as willing to refactor or throw away design elements as methods and classes, especially early on in the process, when your design is likely to be incorrect.
2 thoughts on “Prototype And Throw It Away: Iterate, Iterate, Iterate”