Context, and Motivation for Revisiting this Topic Yet Again

While the Agile Manifesto doesn’t rule out intentional architecture/system design, or (design) documentation, there are those who advocate doing so because Agile practices produce the necessary and sufficient effect — TDD, refactoring, other good coding practices, and pair and mob programming, along with the quick and robust feedback loops of developing and continuously delivering working software, induces good emergent designs, with in-situ (expressed in code, including tests) sufficient design capture.

We return to this question perennially, and have been doing so since well before Agile. The classic of the form is Peter Naur’s "Programming as Theory Building" — written in 1985. I (jokingly) think of it like this — Naur drove right up to my house, but then drove on by. That is, I agree with his position that we build up our "theory" (or mental models) of the system and how it relates to demands on it (and strongly recommend reading that paper), but I would not leave models and other such mediums to convene and convey design reasoning behind, when we leave (as we must) behind any notions that we can make programmers "replaceable" if we just had enough of the right documentation.

In short, my view is we need design expressions to help us reason, to reason together, and to help share our reasoning with our future selves as well as new team members. To support conversation, to inform and, in effect, teach. But we need to be discerning and disciplined about what design views and descriptions help us “deliberate, deliberately” (Dawn Ahukanna) and what are excess or redundant with what is expressed in the code (by which I mean code, comments, tests), what descriptions to keep around and evolve, and so forth. What follows is how I reason myself to this position.

Is Anything Missing?

Some insist "the source is the design." Let's explore that position, for it presents a neat Socratic challenge.

When we look at our own code, or someone else's, and wonder "what the jabberwock was I/they thinking?", some of that is related to code understanding, which may be:

a matter of (re)submerging in the meaning of the abstractions and (con)sequence of operations;

related to system complexity (difficulty, size, interaction and action at a distance, wotnot) — it is in the code, but it is hard to wrap our heads around because the design thinking is distributed around the code.

And some of that is design thinking that influenced and shaped, but is not expressed in, the code. The following is generally missing from the code (though, especially at narrow/local scope, may perhaps be partially inferred from assertions and tests or comments):

assumptions (where? under what conditions? ...)

rationale (why? connecting the dots to intended outcomes/value and challenges)

theory of operation (how? explaining how key mechanisms are intended to deliver outcomes with an emphasis on qualities or cross-cutting concerns which by nature are non-local and emergent, identifying challenges faced and how they're addressed)

alternatives considered, but ruled out (what we're not doing and why not; what we tried but didn't pan out)

We might protest that points like these merely highlight what is missing in terms of retrieving the design thinking that went into the design. The design consequences are now entirely captured in the code. Or are they?

What is the point of considering "Why was it done this way — what forces and goals and constraints made writing the code that way make sense, at the time?" We can play devil's advocate and say the design is what it is — a POSIWID kind of point. It works as implemented. We only need to figure out what to change and how, not what we were weighing and resolving, when it got the way it is. But if we don't understand what system integrity means for our system, how do we maintain it? Further, in evolving the system, we want to assess whether the founding assumptions and shaping forces still hold — does the design still make sense? What do we retain? What design philosophy and principles, structures and mechanisms do we preserve to preserve structural and design integrity. and what can we make accommodations to and shift?

Likewise, we might protest that what we decided not to do is merely superfluous cognitive load, now that the system is the way it is. Except that we argue so much about better ways to do things, that for those that are make-or-break shapingly important, it is just as well to persist our reasoning so that we can recall and share it, defuse arguments perhaps, but also inform and persuade and convey the wisdom that went into the choices that were made. Not everyone will read it, but those seeking to understand the choices, and (if the design is good) to grow their technical acuity and wisdom, will. And that seeds more informed conversations. Further, we can increase the likelihood, with better, more tuned and targeted, documentation, but that warrants its own discussion.

Backing out further, as we begin to navigate towards an understanding of an overwhelmingly large code base, it is useful to have "big picture" map-like views of the system, that help us create a mental model of the structures and their interrelationships, and what they are supposed to do — to channel our attention more efficiently, but also to have a level of understanding of the larger system, and how it works, so we have context for the pieces of the system we're going to focus on.

Design is the act and the outcome. We design a system. The system has a design.

The system has a design because implicit and explicit design intuition and reasoning shaped the system, steering it, or attempting to steer it, towards more the outcomes we want. Still more, the better we are at design.

Code as Realization

The code is not simply synonymous with the design. It is a realization of the design. It is shaped and constrained by the design. From the code, we can but partially infer the design intent that shaped it. We can reflect aspects of the design that is embedded in the code, and this can be supported with code visualization tools (visualizing static dependencies among structures in the code, for example), but these reflections of what can be garnered just from the code are incomplete descriptions of the design.

Code can be used as a design medium (Jack Reeves' essays) — that is, it is a medium for design thinking and design expression. Indeed, code is the dominant design medium for software systems. But that is not to say it is the only medium, nor for all purposes the best.

Code realizes the design that may be enacted in our heads as thoughts about what the system needs to do and how to accomplish it. And, as the code takes form, we interact with the design expressed in code and our shaping, reshaping, elaborating mental model of the design.

Exploring Code as the Medium for System Design

It is just as well to ask:

When is code poor or inadequate as a design medium (medium for considering and making design choices, collaborating on them and communicating them, preserving/recording them, adapting and evolving them)?

What is missing from the code that is critical to the design (the act: surfacing and playing with ideas, assessing and comparing alternative approaches; the outcome: understanding, sharing or communicating and improving it)?

Systems are not "parts flying in formation, trying to be an airplane" (Wim Roelandts) but parts that interact and give rise to (emergent) capabilities and properties. Design considers and expresses how to achieve these capabilities with more the properties various stakeholders care about. Within the constraints we face. And weighing and contending with forces that impinge on the system, arising from various (and (co-)evolving) contexts of use, operation and development, and necessitating trade-offs as we try to ensure the system's properties fall within the design acceptance envelope. Properties that include structural integrity and design integrity (including consistency and coherence or "wholeness" with that "quality without a name"-ness that comes from coherent design and not just a heap of parts duct tape and bailing wired together).

We have more options than just code for thinking, reasoning, analysis, trade-off and alternatives assessment to enable us to come up with better mechanism and system designs. These include models and discussions (conversations and written), mockups and sketch-prototypes, simulations, and code prototypes — as well as an incremental and evolutionary approach to systems design-building-evolving that we associate with Agile development.

Design happens at different scales and scopes — it is fractal, if you like. And to see the design at larger scope — the form of the system, its contours and surfaces, interactions and major flows, it can be helpful to express the design in a design language other than code. Code draws us to the details; requires a focus of comprehension on particulars that are dense and meaningful in their very precision. With code, every detail matters. That is not to say that no details matter to design at other scopes, but it is part of the art of design to decide, given a design element and its expression, which details matter. Any conceptualization takes in and leaves off — we just can't hold all actively in mind at once. Code tends to pull us to a local view, implicitly leaving the system and even mechanism views out of the frame of focus. As we zoom out to wider frames, taking in more of the system and what it does where and how and why (because we must make trade-offs), we begin to describe our mental model to ourselves, to make meaning and explain the system to ourselves, in a different language than the language of this or that operation, as powerfully transformative as any operation may be.

System design is about dynamics (flows and transformations) within and across (interrelating) structures and consequences of dynamics (behavior, under forces and constraints) giving rise to (ultimately, system) capabilities with more the properties we (users, operators, devs,..) want — and with fewer side-effects and externalities that we don’t want. And system design is about what capabilities and properties we want, taking into account that our system impacts and interacts within larger systems-of-systems. That is, as much as much of design is local (to an element), much is non-local. And this is where we need to focus design attention — too. But it is this non-local design that we must foremost or largely conduct in our minds, if our only means of externalized design expression is code (including comments and tests). We create mental models and internal narratives about what the system needs to do and be and how it does that; and when we talk this through with others, we have only clues (in the code) to assess whether we’re “on the same page” and share understanding and intent. This remains true with diagrams or models (of facets of the system), but with more views and expressions that explicitly highlight and explore non-local, across element interactions, flows and transformations, responses to events, and so forth, the more of the design we externalize to expose assumptions, play out and explore and explain our thinking, and build mental models that are closer and more actively probed and tested and validated for fit to context and to need/purpose.

Life is way better than our ideas about it. — Michael Feathers

Complex systems, too. Our ideas are such small fragmented, partial conceptualizations; and even if we work hard to develop not just knowledge but wisdom, and apply not just wisdom but a determined process of finding things out through questioning-reflecting and experiment, we're fallible. The notion that we can get away with designing a complex system merely with code as the medium of design thinking, reasoning, probing, alternative exploring, on the one hand, and expressing, communicating, holding for future reference and evolution, and so forth, on the other... is... just silly when one puts it like that? We need to take different vantage points and use different frames-of-reference, and while TDD, for example, is a way to get us to shift our vantage point, other vantage points also help. Some are ad hoc, and chosen or designed to fit the moment. Others we step into as a matter of discipline, because we have found that discipline helpful across a range of contexts and systems.

Documentation, by Another Name

So, about documentation. We typically call these non-code expressions of the design, "documentation." One of the arguments to not create documentation (again, context setting and design expressions), is that the documentation gets out of date. But think about it! If we haven't worked on a piece of the system for a few months, our mental models are out of date. Our mental models interact with that part of design externalized and expressed in code, so that's something. But it is work to reload an accurate mental model. As we do so, we're making sense of the code and the "requirements" (also design that exists in mental models, some documentation, some expectations we and other stakeholders have, and so forth). Recreating the design reasoning that relates what the code needs to do, with what target properties, in what contexts of use and operation.

Now play that out over larger systems and many people, all making sense of the system from incomplete data — not just the code, but why the code made sense to write that way. Out-of-date design expressions can mislead and obfuscate. But is that an argument to abandon "documentation" or to put more effort into describing critical aspects of the design — evolving the design reasoning and the expressions we use to support and collaborate on that reasoning, as we evolve the system? One additional argument for more of the latter: these expressions of architecturally significant facets of the design serve not only to help "load" the design thinking into our mental models, to give them that cognitive assist, but they help share the thinking. So we can collaborate. And in those interactions not just improve the design, but surface where our individual mental models are out of sync/inconsistent. But further, it's, in effect, an educating function in an arena where designs are so often protected IP, and the thinking is otherwise only passed along in conversations. Those are extremely important. but writing and modeling organizes and clarifies the thinking.

Extended Cognition and Mental Models

Anyway, it's a blast to think we're creating these mental models of various contexts (use, operation, development), the system and all the design choices that effect the "requirements," and the design choices that guide and constrain and effect how we address the challenges inherent in building the system. For a small and relatively simple system, we can hold it all in mind. For a complex system, it's a lot to load our minds up with; for really complex systems, too much for any one mind. We can give ourselves cognitive assist by putting some of that in a form outside our heads, where we can see it, collaborate on it, come back to it. Code, yes. And models and descriptions. Too.

Just Enough, Just in Time Design

Again, judgment factors, and we need to consider what's enough design, for where we're at, to get more what we want. But when someone is arguing to ditch documentation, remember, they're asking to limit the (externalized and visible) design expressions to code. That is great for simple systems we can build and evolve reliable mental models for. But use judgment about how to use, support and amplify design judgment when it comes to evolving more complex systems, that place more demands organizationally. Design doesn't mean (all) "upfront." We seek to do just enough design, just in time. In the best medium for the design at hand, which remains predominantly code.

See also: