You are here

Moving beyond MVP: 5 agile design practices you can't afford to ignore

public://pictures/Steven-Lowe-Principal-Consultant-Developer-ThoughtWorks.jpg
Steven A. Lowe, Product Technology Manager, Google

Does “agile” mean “no design”? Are we so afraid of Big Design Up Front that now we fear all modeling and design activities? Has the agile emphasis on creating working code negated the need for design completely?

Of course not, yet many agile teams have effectively abandoned modeling and design and wonder why they struggle to maintain velocity, much less go faster. Are there ways to reincorporate design into agile without drowning in documents and meetings? In other words, can design also be agile?

The answer is yes, agile design principles and practices can accelerate your team. Ready to try some?

Eric Evans says that modeling and design often create the quickest path to the actual goal; I agree. Yet many agile teams do little or no modeling or design, claiming instead that they’re focused on delivering working software. But is that always the best way to produce quality software quickly? Does software produced this way stay high quality? Or does it eventually decay, a death by a thousand cuts as mistakes, shortcuts, and big-picture ignorance force day-to-day decisions that erode the coherence of the system’s overall design?
 

[ Learn what your team needs to know to start taking advantage of test automation with TechBeacon's Guide. Plus: Get the Buyer's Guide For Software Test Automation Tools ]

Agile does not mean 'no design'

How did this design-aversion happen?

The most likely culprit is this statement from the Agile Manifesto: "...prefer working software over comprehensive documentation." Eschewing all design is, in hindsight, an understandable overreaction from the days of Big Design Up Front disasters. But does that mean we are to do no design at all?

I don’t think so, and you probably don’t either. So how do we bring design back into agile without going overboard?

[ Understand quality-driven development with best practices from QA practitioners in TechBeacon's Guide. Plus: Download the World Quality Report 2019-20 ]
 

Don’t developers design all the time?

Design happens all the time; modeling and design and coding are the same activity, at different levels of fidelity and detail. Code is highly detailed and executable. Models and designs may be highly detailed but are generally not executable. So you can reach a point of diminishing returns, where adding more detail to the model will not save you time when coding. In fact, the more detailed and further into the future you go, the more likely your design will turn out wrong. It’s hard to know how much design is enough and when to do it.

In practice, developers make small design decisions all day long. Paul Rayner points out that the sum of these micro-designs doesn’t always add up to good macro-designs (and he has some helpful suggestions to counter this using DDD). When we’re making these micro-design decisions, we rarely take the time to think about their effects on the rest of the design; we tend to work with tunnel vision, which makes for fast cycle times but sacrifices a bit of reflection.

The cumulative effect of these isolated decisions, especially on larger teams, is often suboptimal. It starts as frustrating friction when working with the existing codebase and turns into a serious drag on the team’s velocity.

What if we could give bottom-up, emergent design just the attention it needs to evolve so that it accelerates the team instead?

That’s the purpose of so-called agile design, at least in my mind.

The key principles of agile design

Reintroducing design into agile is not a new concept; Scott Ambler’s agile design principles and intro to agile modeling are at least a decade old and include far more insightful and helpful material than this short article can cover. Instead we’ll focus on three manifesto-style oversimplifications of agile design principles and look at how they suggest some practices that we can easily try.

  • Prefer group collaboration and learning over solo designs and presentations
  • Prefer incremental modeling over detailed up-front designs
  • Prefer generated, living documents over historical artifacts

These philosophical snippets apply at multiple scales, re-emphasize the desirability of group collaboration and lean processes, and support the same goal: to accelerate the pace of learning, thereby producing better-quality software more quickly.

Prefer group collaboration and learning over solo designs and presentations

Think of this as the “let’s do it together” rule. Instead of a lead or architect (or SME) lecturing the team, present the team with the problem and goals in context and let them figure it out together, with a little guidance and facilitation as necessary. Don’t get stuck on your first idea; you’re likely to get a better one. Creative group exercises such as those from Gamestorming can be helpful for teams not used to a collaboration-of-equals style. The Eventstorming practice is particularly valuable for rapidly producing group understanding via lightweight domain-driven design modeling.

A secondary implication of this rule is that knowledge "inflection points," particularly decisions and discoveries, should be actively shared with the group. Posting a message on Slack is not actively sharing. It’s fine as an anchor for a future conversation, but have that face-to-face conversation, too.

You will still be making your own micro-design decisions all day long, but consider that anything you learn is potentially valuable to the rest of the team. You should periodically look back at what macro effects these decisions may cause, if any, and inform the rest of the team, because 1) others might be affected by those decisions, and 2) others may have alternative approaches. In some cases, the odd thing that emerges in your story’s code can fit together with another odd thing that emerges in a different story’s code, and the two together might reveal a missing concept or larger-scale pattern in the system.

Prefer incremental modeling over detailed up-front design

Think of this as the “keep it small/fast/simple” rule. One of the agile principles states: "Simplicity—the art of maximizing the amount of work not done—is essential." Judicious application of modeling and design at a high level can eliminate a lot of work at lower levels (assuming the design is right, of course). The trick is to balance the scope, depth, fidelity, and time spent modeling with the payoff in learning and coding acceleration. A few minutes of design, with just enough detail, done just when you need it (just in time), with the right people, will maximize the payoff: faster learning, better designs, easier coding, and greater group knowledge.

This principle applies at multiple scales, from lightweight evolutionary-architecture design decisions in iteration 0 to eventstorming and modelstorming around features and stories at the beginning of an iteration to sketches and doodles when pairing.

Yes, doodling. Incremental modeling and design can be as frictionless as doodling while you work. We keep mental models in our heads while creating code; this just cultivates the habit of capturing these models as we go. In doing so, we lighten our cognitive load (and thus improve focus) and make this knowledge shareable. This is especially useful when pairing, to help ensure agreement of initial vision. I say "initial vision," because models are guesses, and the code may show us better ways.

So don't treat the model—any model—as a requirement; it's a best guess at a way to achieve the desired outcome. When the code teaches you a better path, return briefly to the model to reimagine the future. But not too much future. The biggest difficulty in agile design is resisting the temptation to do too much. Developers like detail, and they like debate. Until you get a feel for what "just enough" design looks like, time-box it. Three minutes of modeling every hour or so can let you hop over some of the learning curves.

Not that you wouldn't learn the same things by staying at the code level, eventually. It's just that periodic review and incremental design provide a higher-level point of view. Beware the quicksand of unnecessary details; focus on doing just enough, just in time. When you're done, take a vacation photo, or just scrap it, because the model should now be in the code.

Prefer generated, living documents over historical artifacts

Think of this as the “give it life” rule. If a design document, diagram, reference, dictionary,  etc. will provide sufficient value over the life of the product, then find a way to incorporate it into the build process. Either it's generated or it's understood that someone has the responsibility to update it. Let the team decide what is useful and what is not. And don’t be afraid to discard something that is no longer useful.

Traditional design documents are basically historical artifacts from the moment they’re created. Living documents such as style guides and generated diagrams can be automated and thus kept up to date—but, again, only if they provide value.

The agile principle to “prefer working code over documentation” still applies: keep no documentation that does not help create working code more efficiently and correctly.

Most of the designs and models you create will be thrown away once they’ve served their purpose. Just as you shouldn't keep things around that are no longer useful, you also shouldn't invest effort in updating things that don’t provide continuing value.

Above all, don't treat any design artifact as a requirement. The best it can be is a snapshot of our understanding at the time. If it happens to be similar to what the team currently thinks, that’s great. Just don’t assume that it is true because it is written down.

5 easy ways to practice agile design

Given the principles and intentions above, what are some quick/easy wins applying agile design? Here are five that provide high value to the team and are easy to try:

  • Name things together
  • Tell the same story in models and code
  • Notice and protect boundaries in the code
  • It’s red-green-refactor, not red-green-checkin
  • Bookend each iteration with group learning

Note: This list is far from exhaustive.

1. Name things together

The name of something matters; debating the best name for an element of the system prompts conversations about the model. A word in context should have one clear meaning; if it doesn’t, confusion and miscommunication occur, and that causes bad models and bugs. So pay attention to your words; better yet, don’t use your words, use the SME’s words. Use them everywhere: conversations, models, diagrams, stories, tests, and code—make them ubiquitous. Don’t write boring software that just talks about its own implementation; write substantial, supple software that tells the story of the solution in the domain.

Naming things together can be as simple as saying, “Hey, what about the name ‘X’ for this new service?” in stand-up and then seeing if anyone objects; if they do, call a huddle for later (please don’t derail stand-ups). Jot down the names of the major elements of the story with your pair, and note the agreed definition. Bring these terms back to the team if they can have visibility to other components. Collectively and consistently agree on the key concepts and major component/type names; you will be using these terms a lot. Rename things together when necessary. A glossary is often a useful artifact, worth maintaining and generating.

2. Tell the same story in models and code

Does the flow of your code tell the narrative of the underlying stories? Or does it talk about computer-y things instead? Can you have a conversation with the SME using only the names of your classes and methods? If you read through the code at a high level, do you encounter the expected domain terms and command/event names? In other words, is the behavioral description of the business solution reflected in the names used for classes and methods in the code? If not, the code does not reflect the domain model; the model in the code is anemic. The risk from anemic code is that it is even more likely to drift away from the domain and toward the implementation vocabulary over time, diluting the model in the code and increasing the drag/friction on enhancement and modification.

A periodic practice to prevent this kind of decay is “campfire stories.” Take a class and read it aloud, starting with “Once upon a time” and then describing the (user) journey from entry point to end. Were any of the paths unexpected? Were any of the conditions too technical, or possibly vague?

A habit to remediate this risk is to consider everything from the domain perspective first when writing code. In other words, write the solution at a high level using domain terms, preferably with the same phrasing as used by the SME. Then make the code work without obscuring this domain narrative. If the SME says that a Customer subscribes to a SaaS Product, then the code should say the same thing. For example,

            Customer.subscribe(SaasProduct)

does not mean the same thing as

            SubscriptionManager.add(Customer,SaasProduct)

The former expresses a direct behavior in domain terms, while the latter expresses a computer-mechanical construction operation. Under the hood, it may be wholly necessary and right that this operation be implemented as a subscription-manager service, but having only that phrasing warps the model in the code from the business domain to the computer domain.

3. Notice and protect boundaries in the code

Most bugs are in the interfaces, which in object-oriented systems tend to be methods. Notice where things go in and out of your classes and modules. Pay attention to trust and translation boundaries: be especially wary of interface methods masquerading as domain methods (you can recognize them by their use as callbacks or entry points). They may also have “foreign” parameters, i.e. input parameters that use structures, types, and terminology from outside the domain of this class. For more details, see Domain-Driven Design: are your boundaries leaking?

4. It’s red-green-refactor, not red-green-checkin

If you’re following test-driven development, you first write a failing test, then you make it work, then you refactor the code until it is clean, simple, and clear. Many devs skip refactoring, which is a form of micro-design decision by omission. It’s also an opportunity lost—an opportunity to learn from the code.

I like clean code, 'cause it cannot lie.

Refactor until there are no code “smells” and everything has meaningful names. This kind of common refactoring—for mechanical elegance—not only prevents some inadvertent technical debt, but also makes it possible to see past the code and learn more deeply about the domain. In other words, once the code is clean, it is easier to see the model that has emerged. Clean code—code without mechanical tension—reveals the actual semantics of the model, which might differ from what you thought they were. There may still be semantic tension/confusion in the code; often the concepts, relationships, and behaviors we actually implemented are more profound than the ones we originally imagined or intended.

Lessons learned while refactoring are prime candidates for sharing with the team. How much detail to share depends on how relevant what you learned is to the rest of the system. Periodic team refactoring (a.k.a. “mob refactoring,” a form of “mob programming”) can be useful for spreading knowledge of the system around, and collecting multiple observations about the location and solution for any remaining tension between components. This team-level reflection provides a safety net to keep the overall design from emerging or evolving in undesirable ways.

5. Bookend each iteration with group learning

At a minimum, prepare the team to learn at the beginning of an iteration, and reflect on what you’ve learned at the end. Agile teams often start projects or even iterations with kickoffs and end them with retrospectives. Apply the same pattern to modeling and design. If you spread the knowledge and vision around the team at start, you’re more likely to get better alignment but also more and better ideas for solutions and alternatives.

Every model, every design, is a hypothesis, and should be validated by code. In the kickoff, a sketch or sticky-note model of how the current iteration’s stories fit into the domain and affect component interactions should be sufficient for common understanding and uncovering potential flaws and risks. A team with a common understanding, a common context, strategically aligned, is more likely to produce better code faster. A ‘model retro’ session at the end of a project or iteration extends the power of continuous improvement to the emerging model/design—precisely the counterbalance needed to learn from and/or realign the bottom-up emergent designs and models. Neither of these sessions need be long or produce artifacts; the point is to share and spread the knowledge among the team, strengthen the domain model, reinforce and adapt the ubiquitous language, and generally understand what the working code has to teach.

This is just the beginning

We model to learn. So model periodically, but also incrementally when you need to. Choose the appropriate balance between model/design fidelity, creation speed, and system/class scope. Intentionally model the domain. Take vacation photos and share them, but don’t create historical artifacts. Emphasize group learning and collaboration to prevent knowledge silos and model fragmentation.

Author’s note: I will be talking about this in more depth at ScaledConf 2016 in Chicago, November 7-8.
 

[ Understand the issues and risks that come with SAP modernization with TechBeacon's Guide. Download: Ensure SAP Modernization Success with DevOps ]