Wide-and-flat software architecture

How do you design a codebase to last ten, twenty, thirty or more years into the future? How do you keep it flexible to absorb new types of change that no one could foresee? How do you best write code for long-term value?

These are all clearly open questions; software engineering is a young discipline. But, having seen a decent number of codebases by this time, I think I have come to recognize a certain shape in ones that have been the best at absorbing change, and I think this shape is in fact a big part of what’s made them better at evolving over time. I want to make software for the long term, so I want to try and put it into principles and see if this pattern is actually a thing. This is a running effort at teasing out a definition, and I hope to be able to update it with examples throughout. (For now, the epistemic status is basically a brain dump.)

The pattern

My best stab at a definition is this: a wide-and-flat codebase consists of many, many independent modules on an unchanging abstraction chosen not to be too limiting.

Many modules, hence “wide”; and sparing use of abstractions atop the main platform, hence “flat”. The modules might have great diversity in their contents and purpose. But the crucial property is that changes do not ripple: knocking down one module does not invalidate any of the others, because they do not have cross-dependencies. For that to happen, modules have to talk to each other through contracts or interfaces. A bit of duplication is allowed to keep abstractions from piling up in the business logic.

The pattern honestly sounds a bit trivial when you try to give it a positive definition. It’s easier to think about what it’s not: a codebase that’s not wide and flat would have a dependency tree with more levels between the leaf components and the platform below. Those middle levels might not span all of the leaf-level modules, and there may be many at the same depth of the tree. In real world terms, this means many abstractions that are not part of the target platform, which raises the cognitive effort needed to understand the system and debug.

So for wide-and-flat, the abstractions are not too ad-hoc. They’re well-chosen, or already well-known, and they hew closely to the platform and what it provides.

Understandability is boosted because you can rely on the components to have the same type of interface, to represent the same kind of thing, and to encapsulate any complexity.

Wide-and-flat achieves great clarity of structure through great regularity of structure.

Some familiar examples:

Growth

A big part of the pattern is its dynamic behavior: how does the system evolve in time? A wide and flat architecture is all about planning for growth and change, and then fostering the conditions that make change easy. (As such, it overlaps with a lot of familiar advice about software design, such as the single responsibility and open/closed principles of SOLID fame.)

The main thing is to make modules the unit of growth. Growth, in a wide-and-flat system, should come from adding modules rather than updating them. That is, when you can, you prefer to introduce features as new, independent modules than update existing ones. (For a contrived example, instead of adding a query parameter to change the behavior of an existing AJAX endpoint to something different but related-ish, you would add another endpoint to implement the feature.) This limits the cruft contained in any one module in the system. Nothing makes code a minefield faster than accumulating too many features in one module.

The cognitive cost of adding new modules to a system like this is very low, because limited cross-dependencies means you can count on your changes not rippling too far outwards. Adding modules adds complexity in very small, localized amounts, in which the complexity is usually contained to the new module. A long list of modules without cross-dependencies is not the kind of complexity that slows down development. Again, the flatness of wide-and-flat, means a flat dependency hierarchy. Cross-dependencies between modules make the system less flat.

Your sense of how these modules should be organized is the real connective tissue of your codebase. When do you add one, and when do you split a new one off? These are your rules for how the system evolves over time, so you had better have them, and they ought to be followed.

It’s always cleaner to add or remove a module wholesale than it is to go in surgically to one of them to do work on some feature. When we say a module should have crisp boundaries, this is partly what we mean. Replacement happens at the modular level.

Mechanism versus policy

Part of wide-and-flat is the role of abstraction in the system. There should be a reluctance to factor common parts out of the components into abstractions in ways that would unnecessarily couple things. To understand where to put the abstractions, it helps to make a distinction between two layers of your system: mechanism and policy.

How do you choose the right abstractions in wide and flat? One way to put it is that a wide-and-flat codebase seeks to keep mechanism broad and out of the policy. That is, abstractions should be for mechanisms, not policies.

You have to first be clear on the difference between the mechanism of your system (the platform you build on, the abstractions you use) and the policy (business logic, rules, specific features, etc.) Where flatness is conceived as the dependency tree of your codebase, then “flatness” enables “width”, by granting the conditions for many modules to grow.

Costs of abstraction

We want to keep abstractions as mechanism rather than policy because adding abstractions has a cost.

As new programmers, we learn to abstract things. It’s what we do. So it’s easy to wind up thinking of abstraction as this unmitigated good that ought to be applied everywhere. But like everything, it has a cost, and that’s often not apparent until later in your career. The wide and flat concept is not against abstraction, because that’s how we get anything done at all! But it is clear about where abstractions must lie and what purpose they serve.

By abstraction in this sense I am thinking of things like: frameworks, inversion-of-control containers, and deep class hierarchies. You might define it as “anything that moves the comprehensibility of the system away from the baseline of the language it’s written in”.

The first cost of abstraction is potential calcification. Every abstraction you take on compounds risk in the form of being potentially brittle in the face of future changes, because abstraction optimizes the code for known requirements, not unknown ones.

What’s more, every abstraction you import as a third-party dependency compounds risk from those dependencies rotting outside of your control.

Choosing your abstractions with care keeps you from tying your hands later. An abstraction is always going to optimize for one type of use case over another, so you should take on abstraction only after you discover what use cases you really have.

The second cost of abstraction is comprehensibility. Your in-house abstractions are not a thing compared to the technologies developers learn outside your walls. They almost certainly weren’t designed with the same care as something like a standard library. To the extent that these don’t have much conceptual integrity, programmers will have to take a lot of time to understand what they are, spelunk in the source code to see how they behave, adding friction to day-to-day development. So, wide-and-flat prefers to avoid a lot of that. You strive to build a lot of modules on a limited set of abstractions.

That means abstract, magic machinery is used judiciously. Moving parts are not introduced beyond what the underlying platform provides. The boundary between mechanism and policy is stable and clear. Mechanism is not wantonly introduced to support policy. There is a slight preference for not-invented-here.

In practice

I have tried to apply this system in practice. The biggest project I have applied it to is a frontend component system. Modules are grouped into importable namespaces, the idea being that redesigns of the websites simply add new namespaces of components rather than updating existing ones. Heavily styled one-off pages, such as for marketing campaigns, can live under their own namespace and not pollute the day-to-day components.

Modules know about other modules through interfaces, and the system has only a few of these—they indicate text, or media that can fill an area.