martinevald.net

On the Principle of Composability

(Published on 2023-02-06, corrected and amended on 2023-02-07.)

Visualisation can be a helpful tool to understand any given software architecture. It can be used to understand what components a system consists of, how they interoperate, and the manner in which they depend on each other.

It can also be helpful in understanding the complexity of an architecture; a complex architectural solution will result in a complex architectural diagram. But, that must also come with a caveat emptor: A diagram is only a model of the real thing, and will always hide information. That is the point of it, after all: If you wanted to visualise absolutely everything in an architecture, the efficient way of achieving that would be to just print all the code. We use diagrams because we don’t want to visualize things on that scale.

I was recently involved in a discussion regarding an architectural proposal, where it was suggested that two separate components in the proposal could be combined into a single one, and it would result in a simpler (and therefore better) architecture. This was based on reducing the complexity of the architectural diagram that came with the proposal. The argument struck me as coming across as obvious “common sense”, but yet it also intuitively felt wrong, not least because it seemingly violates the Unix philosophy: “The Unix philosophy favors composability as opposed to monolithic design”. Why is it that the Unix philosophy favours composability, and what does it mean exactly?

What Is Composability?

Composability can be broadly defined as the ability to form composites from components, or metaphorically as the ability to build houses from bricks. Each component (or brick) is a simple part, but can be combined into something more complex, without the architect having to care about exactly how each component (or brick) was made.

The main argument for making small and simple components, is that it is much easier than making larger components. It is a kind of divide-and-conquer approach to programming: We begin by dividing a complex problem into smaller sub-problems, and then solve each sub-problem indepentently, by writing a small program. Then once we are confident that our resulting programs are working, we can confidently string them together to solve the original complex problem. As an added bonus, the sub-problems we identified may turn out to also be sub-problems for other complex problems, immediately aiding us in solving those problems as well. Because the programs we wrote work independently, we can combine them in many different ways, just like we can lay bricks in many different ways depending on what we are building.

This is a decent facsimile of how Unix commands work: They are small and simple programs that each exists to solve one problem, and to solve that problem well. Unix allows us to connect the output from one program to the input of another, providing us a degree of composability using simple, standard commands. If a command to solve a specific, simple problem is missing, we can usually write it ourselves without too much effort.

Unix commands are just one example of composable components, though; any piece of code with a well defined interface, that can be executed independently of other code, fulfills the requirements for composability.

Composability And Testing

My experience is that unit testing is a well established practice in several software development domains, while in a some others it’s more rarely used. In short, it is a development technique where we write tests for individual parts of our code, to verify that they produce the expected results given different sets of inputs. It is meant to be fairly exhaustive; an ideal test suite should cover all possible combinations of inputs and outputs in the piece of code that it’s testing. In addition to helping us test our code as we’re writing it, the idea is also that when something in our program changes, we will immediately spot unexpected bugs and errors, because the tests for those parts of our code will fail. When done properly, it is a bit like pre-emptive automatic debugging, where our tests are figuring out where the bug is happening for us.

For a pure function, which only takes its inputs and produces an output with no additional side effects, writing tests like this is easy. For non-pure functions that do have side effects, that go writing things to disk, that rely on the system being in a certain state prior to calling, writing good and reliable unit tests can be very hard, if not downright impossible. Fortunately, functional composability affords us a mechanism to turn any non-pure function into a pure function. The mechanism is called dependency injection.

In a slightly simplified model, a function will only become non-pure if it calls another non-pure function. Dependency injection works by having a function expect to receive references to any potentially non-pure functions it calls as inputs, meaning the caller is now in control of whether that function is a pure function or not — the caller is in control of the callee’s dependencies, which is why this is also referred to as the architectural principle of dependency inversion.

This concept can be generalised from functions to more generic components, and so we can see how composability helps us write good unit tests for our code.

Composability And Complexity

A frequent argument I am getting against composability is that it increases architectural complexity, by introducing concepts like explicit interfaces and dependency inversion, where it would be simpler to just hardwire components together. This argument is entirely valid, but it’s only so based on the assumption that the architecture will never change, or that it can be safely and easily changed without having unit tests in place. Failing that, the argument is only a vote for taking a shortcut now, that will result in a much longer journey at some point down the road.

This complexity-based argument against composability is sometimes presented as YAGNI, but it’s important to remember that YAGNI was originally formulated as an extreme programming (XP) principle. Regardless of what your stance is on adhering to XP principles, one of the other principles it proscribes is that you should be unit testing your code vigorously — so designing for composability can definitely be YAGNI-adherant, even if you don’t need it for any other purposes than for writing better unit tests.

Architectural Diagrams And Composability

Let’s go back to the beginning of the article, and analyse the situation with the diagram. We will see that when designing software using composition, there are a few things to keep in mind regarding complexity, but also regarding apparent complexity.

Based purely on a diagram, the simplest, most elegant software architecture will always be a solitary monolith: Then you have reduced what would have been an entire, complex system of interconnecting lines and nodes into a single box with no interactions or dependencies. This is undeniably a very simple diagram. But is it a simple architecture?

When looking at an architectural diagram, and before making design decisions based solely on that, it’s important to recall that it hides information. What we do, when we combine architectural components in a diagram, is that we are hiding complexity: By drawing a box around a set of interacting components, and saying that they should be combined into a single big component, what we in reality have done is not simplified the architecture, but hidden its inherent complexity by simply reducing the granularity of that part of the diagram. We have simplified the diagram, not the architecture.

This can be done deliberately, of course, to illustrate how individual components can be combined to form larger logical components, but when doing so it’s important to understand the abstraction layer we’re talking about; on what topological tier of the architecture we are reasoning. An architectural diagram can be a useful design tool, as long as there is full awareness of what complexity it hides, as well as what information it visualises. Combining separate components in an architecture is always just a way of pushing that complexity down to a lower architectural tier, and is never an actual architectural simplification.

Conclusion

As I have hopefully illustrated, composability can be a highly valuable software design principle. Exactly what a software component is, and how much it should do, is something I purposely do not attempt to specify in this article: It’s about a defining a principle; a way of thinking about architecture, and not about mechanical rules for how to write code. I hope it’s been of some value to you!