Given some requirements for a system, how should one start designing it?
Before answering that question, let’s first think about how one understands an existing system. A typical approach is to first understand the boundary of the system. Inside the boundary is the system, outside the boundary, is the environment. The boundary separates the system from its environment and therefore in a way, defines what the system is. Like a fence with a door, the boundary has gates that allow well-defined interactions between the environment and the system. Both the boundary and the internals need to be designed well.
In an existing system, we can look at the internals of the system and tell what each part does. We can say that MySQL has been chosen because transactionality is required (or the other way round – why would they use MySQL if they didn’t need transactionality) and so on. But we cannot look at the edges of the system and explain “why” they do what they do. Understanding the design of the boundary requires us to understand the environment in which it was built. This is the difference between what Russell Ackoff calls know-how and information. I can tell how the system does what it does by looking at it in isolation, but only by looking at its environment can I tell why it does what it does. Documentation, ADRs, tribal knowledge, etc. are all tools for creating this environmental context. Unit and integration tests are not because they only verify the internals of the system and not its objectives.
Therefore, it only makes sense to establish the environment for a system and identify its role in that environment before we figure out how to make it work. In other words, start with why.
When given a system to build, I treat it like a component of a larger system and go one level higher to ask “what role does my component play in the larger system” or “who will interact with this component”. Sometimes this answer is simple because it plays a minor role in the larger system. Sometimes, it plays a major role, and defining the environment (i.e. the larger system) initially lets me understand the role of my component a little better. The clearer the environment and the other subsystems are, the more confidence I can have that I have considered all the roles my component is required to play. In practical terms, documenting this process turns out like the narrative approach to software design I have outlined elsewhere on this blog.
All this talk about edges and systems is not just philosophy. It helps in establishing the degree and dimensions of uncertainty, and this, in turn, has significant consequences on the design choices we make. If the role of the component in the environment is an experimental one, we should probably not harden the boundaries of our system too much just yet. We can go for simpler implementation and design internally. We should even be ready to throw away the component as the environment evolves. If we ignore the uncertainty in the environment, I might harden the interfaces of my component too early and get stuck with an inflexible component that is difficult to evolve. On the other hand, if the environment has well-defined expectations from my component (“Requirements are clear” in developer parlance), I choose to build well-defined interfaces from the get-go and go for a more robust internal implementation. In this case, leaving ambiguity in the interfaces would only cause confusion later. Maintainers might imagine uses for these flexible interfaces which are not needed or intended in practice.
So we identify the shape of the system first before we start colouring between the lines. We can now start breaking down the walls of the component to design its internal parts. In fact, this is the same process we will follow for building the component. We identify the purpose of the component and then create sub-parts that collectively serve this overarching purpose. The component is the environment and each internal piece is a system to be built. We should apply the same principle to the original requirement specification.
Requirements do not arise in isolation. The very fact there are “requirements” indicates that there is a purpose, and that purpose is created by something external to or “above” the requirement. Going one step higher to understand the environment will help us design a system that is much better suited to the context of the system.
Read Next: More than testing, unit tests help in system design
If you liked this, subscribe to my weekly newsletter It Depends to read about software engineering and technical leadership