Week of 2023-02-13
Where I try to write down some of the principles I learned while developing software. The first one is about layering and the second one layers on top of it (see what I did there?)
The Layering Principle
Preference toward layering is probably one of the more fundamental principles of software development. It pops up pretty quickly as soon as we start writing code. I wrote about layering extensively in the past, but basically, when we start connecting bits of code together, a tension arises between the bits. Some need to change faster and some need to stay put. This tension quickly forces our code to be arranged in layers, whether we want it or not. I sometimes joke that layering is either something we chose to do or something that happens to our code anyway.
Thinking of layering ahead of time is costly and usually involves discipline that is not always possible, especially when timelines are tight or the shape of the software we’re writing is not yet known. Often, our initial layering designs are wrong, and a whole different layering eventually emerges. These surprises might not be pleasant, but they are to be expected. Layers accrete. We are just here to garden them into the shape that’s most suitable for our needs.
Thus, as we engage in software development, we have to contend with two conflicting forces: one of expedience and convenience that beckons us away from layering, and one of intentionality that pulls us toward it. To resolve the conflict between these forces, here’s the layering principle: lean toward intentional layering, but give the layers room to develop.
A good rule of thumb here is to define layers early on as loosely as possible, and watch for where the layer boundaries are potentially crossed. When this crossing seems to happen, take the opportunity to clarify the layering. Watch for new layers to emerge and don’t add them without a clear need.
Here’s a concrete example of loose layer definition. Suppose we’re building a client library for a cloud service. We might define three layers, listed here in reverse order (from bottom to top):
Raw REST. At the bottom, there’s the raw REST-ful API that is literally HTTP calls to the cloud service. This is the bedrock for us – we consume it, but don’t build ourselves. Don’t forget to have a bedrock layer. There’s always something that we build on.
Core. In the middle, there’s the idiomatic layer that translates raw calls into constructs that are common for the target environment of the library. For example, if our target environment is Node, we might have something that uses http module or a new-fangled fetch to make the REST calls and return JSON.
Features. Things that make the cloud service easier to use go in the top layer. This is where we can add fun syntactic sugar that lets us write the code in three lines instead of twenty, or address a particular use case in a particularly elegant way.
This might seem counterintuitive, but start writing code without explicitly putting these layers in place. Don’t force them. Think of the process as growing a seedling. Just keep giving them a glance as more code is added. Does this particular function seem like it could be in the Core layer? How would it group with others like it? Especially at the very early stages, think of layers as aspirational, and feel free to adjust the aspiration. Be patient: they will start showing up and becoming real.
Once the layers start coming together, it helps to develop a layering hygiene: imagine that a developer chooses to engage with a layer directly, instead using the full stack. If they are making raw REST calls, are they missing anything? Can they still get the same results? If they decide to write their own specialization layer, are they missing any of the core functionality?
Finally, as we develop features, watch for what’s happening to the code. Is there a new clump of code that seems to be forming? Maybe there’s a layer of specialization that starts emerging, or perhaps the core layer is splitting into idiomatic service calls and scaling/configuration layers?
The trick to the layering principle is in recognizing that there’s no simple answer: layering is a bit of a paradox that requires flexible thinking and continuous keen observation, rather than precise solutions.
🔗 https://glazkov.com/2023/02/14/the-layering-principle/
The Wallpapering Principle
This principle builds on the layering principle, and deals with a common decision point that most software developers reach many times in the course of their work.
The situation that leads to this point unfolds something like this. There is some code at the lower layer that isn’t giving us the results we need for implementing the functionality of our layer. There’s some wart that was left there by developers of that layer, and we have to do something to minimize the exposure of our customers to this wart.
What do we do? The most intuitive action to take here is wallpapering: adding some code at our layer to reduce the gnarliness of the wart. This happens so commonly and so pervasively that many writers of code don’t even recognize they are doing it. Web development has a proud tradition of wallpapering. There are entire communities of libraries (jQuery, React, etc.) that invested a ton of time into wallpapering over the warts of the Web platforms.
Especially when we are not thinking in terms of layering, we might just presume that we are simply writing good code. However, what is really happening here is a shift in layering responsibility – or perhaps a “layer entanglement” is a more catchy term. The code we are writing to fix the wart is out of place in our layer: it actually needs to live at the lower layer. And that means that by wallpapering, we are most definitely violating our layering principle. The code we write might be astoundingly good, but it’s kind of jammed sideways between the two layers.
As a result, the wallpapering code tends to be a drag on both layers. The layer below, now constrained by the specific way in which the wallpapering code consumes it, is grumpy about the loss of agency in addressing the original wart. By wrapping itself over the wart, our code now amber-ified it, preserving it forever.
At our layer, the code is an albatross. I already pointed at the CSS Selector-parsing code in jQuery as one example. Because it belongs to a lower, more general and more slowly moving layer, every wallpapering code saps efficiency of the team that needs to maintain it.
Perhaps most importantly, the wallpapering code has the capacity to misinform the layers above of the nature of the machinery below. If the opinion of the wallpapering code deviates strongly from the lower layer’s intention, the consumers at higher layers will form inaccurate mental models of how the lower layer works. And that is where the compounding costs really get us in the long term. The story that my friend Alex Russell has been telling about the state of modern web performance is a dramatic and tragic example of that.
All in all, we are best to avoid wallpapering at all cost. However, this is easier said than done. Most of the time, our bedrock layers (the lower layers we’re building on top of) are imperfect. They will have warts. And so here we are at the primary tension that the wallpapering principle helps us resolve: the tension between the intention to avoid wallpapering and the need to deliver reasonable products to our customers.
To resolve this tension, we must first acknowledge that both of these forces have merit, and in extreme, both result in unhappy outcomes. To navigate the tension, we must lean toward minimizing wallpapering, while seeking to reduce the cost of opinion of our wallpapers when we must employ them.
The key technique here is polyfilling (and its close cousin, prollyfilling): when we choose to wallpaper, do it as closely to the spirit of the lower layer as possible. For example, if our cloud API is occasionally emitting spurious characters, we might be better off filing the “please trim those characters” bug against this API, and then trimming these characters as closely as possible to the code that receives them from the network. Then, when the bug is fixed, we just remove the trimming code.
A good polyfill is like a temporary tenant in an otherwise crowded family home: ready to move out as soon as the conditions permit. Wallpapering is usually a bad idea. But if we feel we must wallpaper, think of the code we’re about to write as a polyfill – code that really wants to live at the lower layer, but can’t yet.