Christopher Alexander approached his fifteen geometric properties from a roughly phenomenological standpoint. They are “fundamental” in the sense that they are sufficiently differentiated from one another for each to warrant its own distinct treatment. It is nevertheless possible to view commonalities among the properties, suggesting that they can be broken down to even more elementary concepts. Alexander himself hinted at this when he proposed a separate set of eleven properties for colour. Some years ago, I even considered a grouping of the properties in terms of their pertinence to information theory.
A few of Alexander's properties share a characteristic that can be articulated roughly like the space between two (or more) centers is itself a (strong) center. Boundaries is one such property. Alternating repetition and positive space, which also happen to constitute the next two chapters, have the same characteristic as well.
In my information-theoretic meta-categorization scheme, I put boundaries in a separate category (conveying a signal) from alternating repetition and positive space (compression). This demonstrates that there is more than one way to cluster the properties. We expect this, however, since we're ultimately trying to bucket continuous phenomena like space, colour, and feeling into discrete concepts and conceptual structures.
Like his other fundamental properties, Alexander approached boundaries primarily as a geometric phenomenon. He wrote that a boundary strengthens the center it bounds by “forming a field of force” while it “unites…with the world beyond the boundary”. This refers to any boundary-like structure, not just functional components like gateways and thresholds.
The great hall at Eishin is suffused with boundaries, along with plenty of the rest of the fifteen properties.
The essence of boundaries in built space can be expressed with an everyday example. Consider a room: floor, walls, ceiling. Adding waist-high wainscotting creates a boundary between the floor and the wall. Adding crown moulding creates a boundary between the wall and the ceiling. The remaining band of wall is now a boundary between the wainscotting and the crown moulding. When the boundaries are the same order of magnitude in size as the centers they bound—per Alexander's guidance—then the distinction between boundary and non-boundary becomes ambiguous, and the net effect is boundaries bounding other boundaries.
The extremely tall wainscotting in the dining room here at Astley Hall will neither give you up nor let you down. Lots of levels of scale, alternating repetition, and positive space in this room as well.
The stone and concrete banks of the Seine are a de facto recreation area in Paris.
At larger scales, a boundary becomes a place that you can occupy. In The Nature of Order, Christopher Alexander offers up the banks of the Seine as an example. Throughout the city of Paris, we see a repeating pattern of tree-lined promenades, one upper, one lower, on one side of the river or the other, or both. The boundary space is plentiful—a sizable fraction of the width of the river itself—creating space for myriad activities. Heavily arcaded buildings like those in Venice, or on the Stanford campus, or Alexander's own Eishin School, exhibit a similar affordance: these are not just thoroughfares, but meeting places, hangouts, places to sit and think.
If you know anything about Stanford, you know it has these gorgeous arcades.
The entrance street at Eishin protects the inner precinct—and the students—from the busy world outside. (If I had known Peter and Dan were doing this pilgrimage, I'd have found a way to go with them.)
These affordances are often emergent. Consider the front steps of the New York Public Library: split into parts and terraced, with folding chairs scattered about. That is, over and above the steps themselves, which make for ample seating. Nominally the steps are there to get you into the building, whose entrance is a sporty storey above ground. Assuming you can walk it—something almost every architect did until recently, and many still do—the structure not only transports you physically, but conveys you emotionally from the bustle of the street to the quiet sanctuary of the stacks. The fact that the space handily lends itself to sitting, meeting, eating lunch, is incidental.
The front steps of the New York Public Library do double duty as a hangout and meeting place.
Curiously missing from the exposition in The Nature of Order is the boundary as mythological trope. Crack open, by contrast, a Joseph Campbell book at a random page, and you are likely to find some remark or other about the role of the boundary or threshold in changing the mood of the story, if not irrevocably advancing the plot. Alexander does, however, obliquely consider the semiotics of boundaries as transition zones in A Pattern Language, with entries like Entrance Transition and Intimacy Gradient. Indeed, gateways and thresholds generate some of the strongest—and not to mention clearest—signals a building can transmit.
We are once again met with the fact that software is not quantized into discrete entities from continuous space, like the built environment is. Rather, software begins and ends as something discrete. Entities in any software system are already as separated as they can be. In other words, there are already implicit boundaries everywhere; the challenge is to make them more pronounced.
Quantizing continuous signals into discrete ones, after all, is the hardware's job.
It should hopefully be clear by now that when I refer to interfaces, I mean all kinds of interfaces, not just graphical interfaces, and that graphical interfaces can always be treated by the fifteen geometric and eleven colour properties that Alexander already established.
The interface is the natural boundary between the parts of a software system, no matter what interface we're talking about:
- instruction sets (the interface to the CPU) - operating systems (the I in POSIX is for “interface”) - ABIs like bytecode, kernel modules or binary plugins - APIs (or “interfaces” in general object-oriented parlance) - file formats - network protocols - and last but not least, user interfaces.
User interfaces may be conspicuous, but in the long term they are probably the least consequential. Every other kind of interface I cited just now is, at least proximately, machine-to-machine. The way these boundaries are strengthened is through specifications and governance structures, standards bodies, and consortia.
The idea of strengthening a boundary in terms of an interface can be demonstrated by considering file formats. The historical complement to the software application is the set of files over which it operates. A file, at the end of the day, is a sequence of bytes that is somehow meaningful to the application. Once upon a time, (at least some) developers would save a program's state by simply writing the relevant chunk of working memory to disk. Of course, naïvely ingesting a bunch of data back off the disk and straight into memory could be disastrous if the wrong parts of it were somehow modified out of band. This situation would motivate the developer to introduce integrity checks for the file contents on the way in, and eventually a proper format specification, which is implemented as a parser (for reading), a serializer (for writing), and the specification itself.
Of course, this only concerns the private matter of a given developer (person or company) organizing their own work. There is nevertheless the issue of what to do with these format specifications once you have written them. Keeping file formats secret is a tried-and-true method of facilitating a monopoly, one that was infamously employed by Microsoft for several decades, and the reason for the ubiquity of Word, Excel, and PowerPoint. When the internet—and more importantly, the Web—ballooned in popularity in the 1990s, it became more valuable to share content between heterogeneous systems, and the focus started to shift toward standards.
This is to say nothing of the institutional infrastructure, governing bodies, and the intricately intertwined politics and economics attendant to maintaining and developing said standards. So when we think of “strengthening the boundary” with respect to these kinds of interfaces, we aren't even talking about software, but rather organizations, institutions, and even cultures.
Also: pretty much anything that can be said for files can be said for protocols, because if you squint at them a certain way, they are almost the same thing.
Another place we see this is in the development of programming languages. Some languages define a boundary between the abstract language and its concrete implementation. FORTRAN, C, Common Lisp, and pretty much any serious language made in the 90s or later exhibit this characteristic. Other languages, such as PHP and Perl, grew out of their respective implementations, and are thus inseparable from them.
The last category of interface I'll consider for now is the interface of the code itself. We can understand these as the set of datatypes and operations that are said to be exposed (a condition that may or may not be enforced by the programming language in question). That is, whoever wrote them expects them to be used by other people. Strengthening the boundary here, at minimum, involves documenting the interface so others know what capabilities, constraints, and behaviours are available.
Code interfaces are (at least these days, still) made up of identifiers, alphanumeric symbols made from short strings of text. Ideally, the set of identifiers that make up the interface to a piece of software are meaningful to the people who work with them, and serve as a mnemonic for the thing being identified. One way to strengthen this boundary would be to ensure that the identifiers of a software library's interface all followed the same scheme, so that developers could infer from the structures and processes with identifiers that they happen to have memorized, that there should be other entities with related meanings and behaviours without having to go to the documentation to look them up.
An interesting secondary project would be to catalogue the terminology we have developed in the software industry to talk about concepts analogous to those addressed by Alexander and other architects and designers. This one, for example, is known as the Principle of Least Surprise. It can be seen in the language Ruby, with its convention of naming type coercions and object serializations
to_x
, where thex
is some sub-identifier (e.g.to_s
,to_i
,to_json
) that has developed a conventional meaning. Or take for example just about every language with a stack-like construct has a pair of mutually inverse operations called something likepush
andpop
. They don't have to be called that, but they often are. Entire languages likewise develop strong idiomatic styles that are enforced mainly by social norm (Python, Clojure, and of course C come to mind).
Strengthening the boundaries of code interfaces can be achieved, as we will see in subsequent chapters, through other fundamental operations, like local symmetries (albeit conceptual symmetries, like pairs of opposites), echoes (using the same construct in different places), and not-separateness (using conventions that are already present in the environment).
Software systems may start out with a simple anatomy, but as development goes on, different organs very quickly start to nucleate. This can be seen in the conceptual structure of the software (classes, methods, etc.), its concrete rendering into (e.g.) files and directories, and even in its deployment into clusters and subsystems. This is known to us as separation of concerns.
Modularizing software has the dual effect of promoting reusability, while constraining the interactions between the modules to their interfaces. This makes their behaviour more intelligible, and we can test them in isolation to ensure they work correctly, and with that assurance, treat them as opaque. This enables us to conceptualize a software system—again whether we're talking about the code itself or its deployment—at a much coarser grain.
For a discussion of modules (subsystems) and their interfaces, see Chapter 1 of Herbert Simon's Sciences of the Artificial.
Even user interfaces can be said to be made of “places”. Websites in particular lean heavily on this metaphor, as each page is a place in a space. Strengthening the boundary here would mean strengthening the signal that you are moving from one place to another (e.g. a transition animation, or a loading screen/interstitial animation in a video game), and then signalling conspicuously that you are in a different place when you arrive.
Again, boundaries as Alexander meant them are places in themselves, and so strengthening a boundary means suffusing it with more volume and differentiation. One way this can look with respect to modules is some kind of canonical layout or scheme, like an architecture diagram. An outstanding problem, as with all software documentation, is how to ensure such documents remain authoritative. At this time, such a constraint can only be achieved by culture, and so we can see once again how software is inherently living structure, to the extent that its integrity depends on sustained human attention.
My final consideration for boundaries in software pertains to its behaviour while it is running.
The state of a software system can be understood as the instantaneous configuration of every bit of information to which the system has access, whether in memory, storage, or display. The state of a running program, strictly speaking, advances billions of times a second, with every tick of the processor. A distributed system can indeed be defined as one where the state spans more than one computer.
I mention distributed systems because state always has a place, and nowadays, with multi-core processors, concurrent programming, and the tendency toward utility computing, even seemingly simple systems may be de facto (if not de jure) “distributed”. Properly distributed systems require us to take explicit care with respect to the maintenance and propagation of state, but there is general value in modelling state, and to do so, I argue, is a further demonstration of strengthening boundaries in a software system.
Properly distributed systems are subject to the CAP theorem, which states that out of being consistent, available, and partition-tolerant (that is, it still works when cut in half), you can only pick two.
Since the code is what causes the system's state to change, it is the state changes that get emphasized, whether they map perfectly to subroutines (functions, methods, whatever) in the code or not. One opportunity for strengthening boundaries in this context is precisely to ensure state transitions map one-to-one to subroutines. Another is to formally define the states themselves, as one does when designing a finite state machine. In a sense, states and state transitions are boundaries to each other.
It may be a nitpick, but I distinguish state (singular) as the total information you need to restore a system to its exact configuration at some point in time, while states (plural) are specific, identifiable points in a process, which may actually vary in terms of state-singular.
One fairly conspicuous place to start defining explicit states is errors. Software systems are notoriously bad for undifferentiated error conditions, which confuse users, make it harder to diagnose actual problems, and give software its characteristic officious and unhelpful tone. This stems from developers considering errors to be an afterthought to the so-called “happy path” of successful execution, when in fact most of the observable states will be error states: there are many more ways a given process can fail than succeed.
On a recent project of my own to make a Web authentication module that performs the extremely simple task of logging a person into a website, I ended up with fourteen error states—distinct ways the system could fail—versus only three that were part of the expected behaviour when the process was successful.
Mapping out error states entails imagining all the distinct ways a process can go wrong. Here is a rough typology of failure categories, which I do not claim to be either exhaustive or non-overlapping:
- Substrate failure: The basic conditions needed for the system to run have broken down. Includes but not limited to hardware, like a power cut, running out of RAM or storage, or unplugged network cable, but also crashes, infinite loops, etc. - Missing resource: A thing you (or rather, the computation) expected to be there, like a pointer, or a file, or database record, or server, isn't. - Input failure: Somebody or something has passed input into a piece of interface—whether system interface or user interface—that is either malformed (syntax) or otherwise invalid (semantics). - Access restriction: The caller is forbidden by policy to perform the requested action.
Each of these error categories is qualitatively different, and demands a different prevention strategy as well as a different response. Errors may moreover be compound. For example, access control may fail because a resource (say, an authentication service) isn't available, because the machine running it crashed. In this case, the person (or machine) isn't unauthorized per se, but their authorization status is indeterminate. This situation merits a different response than if they were simply not allowed.
As for their relationship to boundaries, errors can be seen as boundaries themselves. An error terminates a computation, and depending on what it is, forces the context—again, either machine or human—to either try something else, or try again.
A transaction is an operation that has been arranged to either happen completely, or, if the process is disrupted, not at all. That is, the state of the system is guaranteed to be consistent, and not ambiguous or half-done. Transactions are said to be atomic, and thus create “natural” boundaries in the execution of the code.
How you make an operation transactional, roughly, is by not destroying the state from before the transaction begins until after you get confirmation that the transaction is complete. Once completed, you perform a single “track-switching” operation to point to the new state, at which point you can reclaim the resources for the old one. If anything screws up, you roll back to that original state you saved.
To function, transactions need enough resources to store (at least) both before and after states. A real-world analogy for this is that air-lock-like entryway structure you see in jewelry stores. The store owner commits extra resources to improve the odds that people entering the store do so in a calm and orderly fashion, and that if they leave with merchandise, they do so on the condition of having paid for it. So too with transaction infrastructure in computer systems: it always takes more resources and more complexity to ensure the atomicity of operations. But, when we strengthen these boundaries, we can make certain guarantees about the system's behaviour that we would not be able to do otherwise.
In single-user, single-threaded systems, transactions guard primarily against what I called “substrate failures” above—the power being switched off and such. In a multi-user system, like a relational database, one needs to preserve the state of the affected parts of the system at the instant each connected user opened a transaction, as well as keep a queue of transactions which it has to integrate, e.g. by first-come-first-serve. This infrastructure is necessarily more complex. Federated systems—at least those that can accept write requests at more than one location—require additional infrastructure in the form of some kind of protocol that at least does the equivalent of opening transactions remotely on all the other peers, waiting until they confirm the write, confirming the write locally, and then confirming the local write back to the peers.
…and then getting confirmation that the confirmation was received? The bottom line is it's a heck of a lot more complex when your state is spread across multiple locations, but nevertheless certain scenarios demand it.
Undo is related to transactions insofar as you need to use additional resources to store past state, or at least enough information to restore a previous state.
Version control systems generally achieve this with
diff
patches: store the latest versions of a set of files and then only patches of previous revisions. Older revisions are then retrieved by successively applying the patches. This takes longer the farther back you have to go, but takes up less space than it would to store previous versions in their entirety.
Undo, unfortunately, tends to suck. This, I submit, is because developers can't get their arms around everything the user expects to be undoable. And this, I submit, is because their projects have no unified model of application state: in order to undo something, you need to be able to precisely describe what you did in the first place.
Not everything is terrible when it comes to undo. I remember remarking over 20 years ago that Maya recorded every operation done to a project into its save file. Adobe went almost that distance around the same time with its undo stack in Photoshop, but the history was not saved and thus did not persist across sessions.
At any rate, there is a lot more help nowadays for developers who want to implement a multi-step undo stack, but how well the capability covers the set of undoable operations—and not to mention whether they persist—is still up to the developer.
Establishing a unified model of application state is complicated by:
- not fully accounting for—or having application-level control over—all the undoable operations (or non-undoable ones, for that matter), - mixing undoable operations with non-undoable operations.
We can furthermore say that there are, at root, only two kinds of justifiably non-undoable operations:
- those that consume an unreasonable yet necessary quantity of resources—even on today's hardware, - those that cross administrative boundaries, e.g., you can't (at least without the help of the receiving end) un-send an e-mail.
If an operation doesn't fit into either of these categories, it can probably be undone. The challenge I see once again is in creating a model that will both express the necessary conceptual structure, as well as maintain its authority over the code.
Why I suggest that undo echoes transactions is because you arguably need transactions in place in order to implement undo. Since undo operations are usually considered in the context of user interactions (for headless or bulk operations, depending on the part you're considering, the term is something like checkpoint or rollback), this means that every “verb” a user can invoke should be atomic. This is a strong case for creating a domain-specific language, an authoritative vocabulary of user operations at the data semantics level, and declaring that only the operations on the list are allowed to change the application's state. Then, at every invocation, you can record the change in state, and the operation (plus any parameters) that caused it.
Functional programming languages are particularly well-suited to this kind of task, because they at least promote the fiction of operating without side effects. Ordinary imperative and object-oriented languages can of course be used in a pinch, but you lose that guarantee.
Enterprise systems, that typically store their state in relational databases, are notorious for only storing the current value of an entry: updating a value means deleting whatever was previously there. This is remarkable because these kinds of databases have been in service in one form or another over half a century, and such a ubiquitous problem has only been tepidly addressed (through materialized views, otherwise you're rolling your own with triggers). Even still, it requires cognizance—not to mention motivation—on the part of the software developer to use them, and still has to trade off efficient storage for ease of implementation.
Another related capability, more suited to multi-user systems, is the audit log. These record (ideally): who changed the system's state, what they did to change it, what they changed it to, and when they did it. This capability would be much easier to both implement and manage if the system's state was strongly punctuated.
Undo, along with its attendant operations, is a great example of the value of strengthening boundaries in the state of running software. Constraining the vocabulary of (necessarily atomic) operations to a set of pure functions means being able to cognitively manage, and thus say things about where, when, and how a software system's state changes. Audit logs enable you to also say who. None of this, however, can be accomplished without creating the space for it in the system.
Much of what I have discussed above depends on conceptual entities that are, at best, only weakly present in the code. Perhaps more than the other fundamental properties we have seen so far, boundaries depends on supplementary artifacts (specs, diagrams, etc.). This is troubling, because strictly speaking, code can run just fine without a single word of documentation. Under ordinary conditions, this means the documentation is always trying to keep up with the state of the code it describes.
Enter literate programming: a technique developed by Donald Knuth in the 1980s, that makes a best effort to make code subordinate to documentation. Only problem is, it's an extremely esoteric and largely neglected technique, with little to no support infrastructure available for the most popular languages. Even if there was, everybody involved on a given project would have to agree to use it. My anticipation, however, is that such a technique will almost universally be viewed as “extra work”. I nevertheless believe there is value in exploring literate programming, and doing what needs to be done to make it a viable strategy for software that more resembles Alexander's living structure.
While I believe that in principle literate programming shows promise, I have not been especially impressed with its implementations to date. Documentation is just text; diagrams are just pictures. One of the biggest promises of literate programming is that code is organized initially for the benefit of a human reader, and then is transformed as needed to meet the requirements of the machine. Aside from Knuth's initial implementation, and closely related ones, this doesn't happen very much either. This nevertheless doesn't mean the technique can't be rehabilitated.
Arguably one of the most effective methods of disciplining a codebase—one that has not only developed a sophisticated technical infrastructure but also strong cultural norms—is automated testing. Testing frameworks exist, moreover, for missions like policing coding style, or the coverage of reference documentation. Suppose you could also automatically test for boundaries. What would you need to pull that off?
As I wrote at the beginning of this chapter, the essence of boundaries (and the next two chapters) is “the space between centers is also a center”. We looked at boundaries in interfaces, modules, and changes in state. What I would be looking for is a way to formally define the conceptual entities and the relationships between them, such that they could at once be projected into things like diagrams and code skeletons, as well as automated tests.
I was surprised to learn recently that UML does not have a symbolic representation. That is, despite being a standard, in UML there are only pictures: no data format, no round-tripping, no cross-tool portability. This means the fancy software modelling ur-technique of yesteryear is not nearly as useful as advertised.
I'm not suggesting a major tooling overhaul. Just like everything Alexander, this process should be incremental. What I'm suggesting is a stronger representation of conceptual entities outside the code proper—strong enough that they eventually take over as canonical.
Interfaces, for example, can be defined in terms of their desired semantics. The result will look a lot like an API reference: name of the verb, parameters, return value. Include an index of data types. All this should be familiar enough, and a number of programming environments already generate stub code, as well as tests and documentation.
Modules are the entities probably best represented in code, since we're talking about things like classes, packages, namespaces, file and directory layouts, etc. One way something like a diagram helps is a synoptic view of the entire codebase, depicting not only the different application domains, but also, for example, inheritance and mix-ins and such. Suppose the diagram contained metadata that could generate a test case for conformance in the code, and furthermore the code could be scanned for entities that may not exist in the diagram. This would ensure that the diagram is not only in sync with the code, but also authoritative.
It further helps if the metadata is tied directly to the objects in the diagram, instead of being tacked on in a lump somewhere. I regularly create diagrams in SVG with metadata embedded this way using RDFa. Every entity, naturally, gets its own URI. I do the same with HTML documents: the markup structure echoes the data structure, which means that any change in one will be reflected in the other.
Changes in state can get a similar treatment as the prior two: explicitly identifiable states, such as those found in task flows and errors, can be both indexed and diagrammed. State-advancing operations, undoable and otherwise, can absolutely be catalogued. Again, the canonical representation should be a computable yet language-independent format, which can be projected into both prose and code.
One thing should nevertheless be clear: pronounced boundaries depend on representations outside the code for their integrity. Formal, symbolic representations will enable congruence between the code and its various forms of documentation to stay in sync. We should be looking at ways to integrate the relationship between source code and its supporting artifacts, and in my opinion, literate programming—with some modern enhancements—is an excellent point of departure.
This chapter is the first one of the fifteen properties that gave me significant trouble. I attribute it mainly to the fact that this chapter really wanted to drag the next two along with it. I had anticipated this somewhat, given what I said about there being a snowball's chance in hell that Alexander's fifteen geometric properties for buildings would map perfectly one-to-one to semiotic-topological properties in software. Anticipated, maybe, but didn't really have a contingency plan for it. That, and the last few weeks have demanded that I spread (what's left of) my attention much more thinly than I normally do.
I'm going to work to get chapters 4: alternating repetition and 5: positive space out on a bit of an accelerated schedule, and then resume the normal cadence of two to three weeks per chapter.
Thank you all for your patience and continued support.