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.