Judging by the traffic on my previous post, it seems that many of you like Christopher Alexander. I do too! So you’re going to keep hearing about him for a few more posts.
In the previous post, I introduced Christopher Alexander’s concepts of life and centers, his 15 properties of living structure, and the idea that living structure can only arise through adaptation. In these next few posts, I want to clarify exactly what it means for software to evince these 15 properties.
I’ve accompanied each property with an illustration. Sorry they’re so crude—I drew them with a mouse because that was all I had. I hope they get the point across anyway.
1. Levels of Scale
In every modern high-level programming language, source code is organized into centers that span a vast range of scales. Ordered roughly from smallest to largest:
characters
tokens (variable names, operators, etc.)
expressions
lines
statements
paragraphs (chunks of code separated by blank lines)
routines (functions, methods, procedures)
classes and types
modules
architectural layers (UI, controllers, service objects, data access)
programs
services
applications
application suites (e.g. Microsoft Office)
Christopher Alexander stressed that the mere presence of centers of different sizes was not sufficient to constitute Levels of Scale. In order for the whole to feel human-scaled and not alienating, the arrangement of centers must meet a few criteria:
The jumps in size between adjacent levels of scale must not be too large. Ratios between 2:1 and 5:1 work well. When the jumps are larger than that, it becomes difficult to perceive a relationship between centers at adjacent scales. Instead of a cohesive whole, we see a jumble of unrelated pieces.
Small-scale centers must be present. That is, the levels of scale property must be applied recursively, all the way down to the smallest scale we can perceive or manipulate.
The frequency distribution of centers must roughly follow a power law, where there are more smaller centers than larger ones.
These criteria echo the received wisdom that “a function should have 5 lines or fewer.” Obviously, this is not viable as a hard-and-fast rule, but it makes sense as a general guideline, and Levels of Scale tells us why. Each center should be simple enough to “fit in your head.” You should be able to see how each center relates to, and is made from, the centers one level smaller. Thus, a line should have only a handful of tokens, a method a handful of lines, and an object a handful of methods.
What would code look like without levels of scale? Well, imagine an assembly-language program where instead of procedures and calls, you just have jumps (i.e. GOTOs) within an undifferentiated mass of instructions.
A program written in this extreme style might have only two levels of scale — instruction and program. The instruction:program ratio might be several thousand to one. Even if you’ve never written or tried to read a program in this style, you can probably imagine the difficulty it would cause!
Levels of scale also exist in people-space. From smallest to largest:
Individuals
Pairs (if you do pair programming)
Teams
Departments
Offices
Geographic regions
Companies
Note that the centers on this list don’t form a strict hierarchy, but a DAG: e.g. the people on a cross-functional team come from different departments. In today’s distributed world, a team probably spans offices, too.
2. Strong Centers
One subtle but powerful way to promote compactness in a design is to organize it around a strong core algorithm addressing a clear formal definition of the problem, avoiding heuristics and fudging.
—Eric S. Raymond, The Art of Unix Programming, "Compactness and the Strong Single Center"
In Alexander’s terms, a Strong Center is one that plays a “primary” role in a design. It is the core, the heart, the kernel of the thing: its reason for being. A Strong Center tends to have support from many other centers, which are subordinate to it and less central. This “support” can come in the form of another of the 15 properties, but it can also simply be a “pointing” relationship, where the subordinate center gets its purpose from the strong center. The key point is that the subordinate centers are organized around the central strong center. They come into being, and are shaped the way they are, because of the strong center.
Examples of strong centers in architecture include:
The stage of a theater
The altar of a church
A town square or main street
A university quadrangle
The main room of a house (in my house it’s the kitchen)
But what does a strong center look like in a computer program?
Imagine it’s your first day on a new team. The tech lead sits you down in front of a whiteboard and says “okay, the first thing you need to know about this system is X.” X is likely to be the strongest center in the system.
Examples:
“We use CRDTs to sync data between peers”
“There’s a rules engine that runs all the business logic”
“This program is structured as a pipeline that processes data via a sequence of transformations”
“Our domain model is written in a functional style, with algebraic types”
“It’s a CRUD app on top of a Postgres database”
“It’s a Minecraft mod”
“We use Redux for all our clientside state”
Having a strong center, a “first thing to know,” on a project is wonderfully reassuring. When you get lost, you can orient yourself relative to the strong center. As long as you can relate the bit of code you’re looking at to that center, you know at least one important thing about it. You can see how it fits into the big picture.
As Eric Raymond points out in the quote above, strong centers should be precision-engineered and rock solid. A bit of roughness outside the strong center is fine, if it helps accommodate supporting centers to the strong center. But the strong center itself must be exact: it’s the foundation for the rest of the system, so it pays to get it right.
A strong center need not be a formal algorithm or model, though—it can simply be the “central” part of a design, the part toward which the other parts “point.” Whenever you hear software engineers talking about the “edge” of a system, you know that there is a strong center somewhere nearby. Without a center, there could not be an edge!
For example, in a typical Ruby on Rails app, the center is the database. We have the sense that web requests come in at the “edge” of the system, and travel inward through “layers”—controller, services, and model—finally arriving at the central database. Then the response data goes back out, passes through the outermost “view” layer, and finally is returned to the client.
By contrast, in applications that use functional domain-driven design, the domain model is the strongest center. Database interactions happen near the edge of the system, and it is the in-memory domain model that forms the kernel, the solid core of the program.
Extending the idea of strong centers into people-space, we get the concept of a team lead — the role that, at Pivotal Labs, we called “anchor.” The history of the anchor role is instructive. Early on in the history of Labs (which was a software consulting company) engineering teams had a completely flat structure and fluid membership. Labs engineers might rotate between client projects on a weekly basis. Because engineers were pair-programming full time, this worked on a technical level—knowledge-sharing happened fast enough that someone could contribute meaningfully to a project in a one-week rotation. But clients weren’t totally happy with it. They wanted the stability of having one person they could talk to about the technical aspects of the project. Thus, the anchor role was born. An anchor was simply an engineer who would be assigned to the project for its entire duration, and serve as the primary point of contact with the client.
It’s worth considering this history from the perspective of the 15 properties. In essence, Pivotal Labs
started out without strong centers on its engineering teams
identified that this structure was mismatched with their clients’ needs
fixed the problem by strengthening a center.
This is how adaptation works. It’s a great example of how, in a complex system, small changes can have a big impact.
3. Thick Boundaries
How do we connect the strong centers of our programs to the messy outside world? We need some code that serves as the intermediary, the boundary, between the two. Boundary code does all the unglamorous yet essential work that’s needed to keep the strong center pristine. Boundaries can parse inputs, serialize outputs, perform effects, and handle errors.
Boundaries don’t have to be associated with strong centers, though. Any time you draw a line between two pieces of code and say “this part is responsible for this, and that part is responsible for that,” you’ve created a boundary. APIs and function signatures are boundaries. Anything with an inside and an outside has a boundary.
Alexander noted that boundaries in living buildings tend to be thick—often far thicker than you might at first think to make them. Thick walls, deep porches, expansive gardens—these are all important boundaries that deserve space.
In many programs that I’ve seen, the boundaries are not given sufficient space or attention. Programmers often balk at creating comprehensive parsers and serializers for their input and output data because “all that code” takes up more space than they think ought to be allotted to the problem. The issues that arise from this are manifold. One issue is that parsing and error handling get smeared throughout the program and often end up contaminating the strong centers. Alexis King has pointed out the problems with this:
[A] program that does not parse all of its input up front runs the risk of acting upon a valid portion of the input, discovering a different portion is invalid, and suddenly needing to roll back whatever modifications it already executed in order to maintain consistency. [. . .] The entire program must assume that raising an exception anywhere is not only possible, it’s regularly necessary.
Parsing avoids this problem by stratifying the program into two phases—parsing and execution—where failure due to invalid input can only happen in the first phase. The set of remaining failure modes during execution is minimal by comparison, and they can be handled with the tender care they require.
Another, related problem stemming from insufficient parsing is that invalid or undesirable states, which ideally should not be representable in the core domain model, end up being representable. Code that works with the domain model then has to handle these bad states. It’s much tidier to reject the bad states upfront, in a parsing layer.
Alan Kay once likened OOP objects to biological cells — noting that cells spend an enormous amount of energy just maintaining their boundaries, keeping the bad stuff out and the good stuff in. Objects in programs are similar: they can and must reject bad input at their boundary, so it doesn’t propagate further into the program.
Thick boundaries also have their place in team organization. At Pivotal Labs, we did something that Todd Sedano calls dual-track software development. Product managers and designers worked in one “thread,” defining how the product should look and work from the user’s perspective. Engineers worked in a parallel thread, writing the code to make the PM’s dreams come true. The boundary between the two threads was a priority queue, the backlog. The PM would insert work items into this queue — choosing their position according to priority — and engineers would pop the top item off the queue whenever they needed a new thing to do.
This boundary — the explicit structure of it, with exactly one “highest priority” work item at any one time, and the explicit rules about how it was to be used — made communication about the project much smoother. It left less room for anxiety and clashing egos. But it also made room for joy: the satisfaction of getting things done, knowing that they were important to someone.
4. Alternating Repetition
Often in software development, we find that two kinds of centers repeat throughout a structure and are dual to each other. Alternating repetition is about noticing these dualities and taking equal care with both halves, so that both are made coherent.
For example, in Unix pipelines, the filter programs repeat, and so do the streams of data between them. Both form positive space (see below). One way of seeing the pipeline is to see the filters as primary. Another is to see the streams of data as primary.
More generally, routines and data alternate throughout the structure of a program. Data types form the space "between" routines, since data are passed from routine to routine. I've seen too many programs where programmers only thought about the routines, and the data was neglected. In a well-designed program, the data is as coherent as the code that processes it. This isn’t a zero-sum tradeoff. Coherence of data enables the code to be more coherent.
Other dualities include:
Test code and production code
Structure and behavior
Refactoring and features
In test-driven development, the rhythm of writing a test and then writing the code to make it pass repeats and alternates. Either the tests or the code can be viewed as primary. Both are coherent, intelligible entities in their own right.
Applying alternating repetition to people-space, we get work-life balance. One of the things I really appreciated about working at Pivotal Labs was that the workday ended, unambiguously, at 6 pm sharp, every day. Engineers didn’t have personal laptops (client confidentiality prevented us from taking code offsite) so it was physically impossible to take work home. The flip side of this was that for the 8 hours we were at work, we were on. Pairing 40 hours a week is no joke. The schedule demanded a lot of us — sometimes too much.1 But it also, undeniably, brought out our life.
I sometimes wonder what Labs would have been like if we’d been able to sustain ourselves on 30- or 35-hour workweeks. I’m not sure I’d be able to convince myself to go back to pairing 9-to-6. But 9-to-4? Heck yeah!