The right approach to modular architecture

This article is based on two simple ideas:

  • Each decision when designing a software product determines the space of possible options at later stages. This is critical to understanding how and in what sequence design decisions should be made.

  • Modules can be used to preserve uncertainty at key nodes to allow more freedom later.

I'll try to explain how this happens, what the consequences are, and what mistakes to watch out for. But first, let's visualize how decisions affect each other.

Design decision tree

We can think of the design space as a tree.

Decisions at the “leaves” of the tree deal with specifics that do not affect the rest of the project:

  • What input encoding do we use?

  • What color will the button be?

  • What is the duration of the timeout?

On the other hand, branches located near the trunk indicate important questions, the answers to which greatly narrow the choice space in the future. These are important questions, the answers to which inevitably make many potential implementations impossible:

  • How much memory can we use?

  • What problems will we not solve?

  • How much control will we give users?

The path we take along the tree, that is, the design decisions we make, lead to the creation of a specific product.

In this example, we created a product defined by our answers to the questions achmt. It is noteworthy that the decision taken in the matter adirected us along the path to cwhich means that it also eliminated potential solutions that require an answer to the question b.

This is why it's useful to start with high-level questions: it gets rid of parts of the design tree that we consider irrelevant and allows us to focus on what actually matters and can be implemented. This way we can deliver the product faster and quickly find out which nodes of the “tree” we made the wrong choice.

Different configurations = different products

In software development, the result of work, as a rule, is not just one product. We create products that are configurable, which equates to a collection of products that work slightly differently; in this case, the specific product needed by the user in each specific case is selected using the settings.

This angle of view allows us, during the design process, to see the range of products that we can create within the framework of the implementation under consideration and compare it with the technical specifications. For ease of further analysis, let's imagine that the final “leaf” node represents not one product, but all the products that can be created by making configuration changes.

Focus on leaves

When we first start developing a product, we often dive straight into the specifics, i.e. We make decisions at the leaf level before moving on to higher level decisions. Halfway through development (that is, when a significant amount of code has already been written), our project tree may well look like this:

What product can come out of this? None. These solutions are on completely different branches, that is, they are mutually exclusive and there is no way to create a product that satisfies them all. We may not notice this until someone forces us to make a decision, for example, on the issue band then we will understand that whatever we choose, we exclude for ourselves either kor g.

This is usually discovered quite late in the development process, so at this point stakeholders may agree to discard some of the functionality previously agreed upon in order to complete the project faster. But this is little consolation.

A more positive scenario is also possible, in which we accidentally (or not quite consciously) make coordinated decisions at the leaf level, for example, like this:

But when we come to discuss the issue awe understand that we would prefer to make a choice that opens up an alternative c. Unfortunately, this is inconsistent with all the decisions we have made so far.

This unfortunate situation is surprisingly common: our leaf-level decisions imply a set of higher-level decisions that don't suit us. Again, we can easily not notice this until we try to integrate all the pieces and bring the product to market. The result: a lot of expensive rework or a suboptimal solution.

This is another reason to focus on high-level solutions first: we reduce the risk of wasting time on solutions that then have to be reworked because they are inconsistent with the vision of the final product.

It's easier to start with leaves

I think we tend to start with leaves because:

  • these issues are easier for a wide range of participants to understand, which means it is easy to distribute responsibility for the decision;

  • they quickly give us the feeling of moving forward;

  • they are simpler;

  • we are more likely to have a ready answer to them based on past experience.

When we focus on the details and ignore the big picture, we can close tasks one by one and demonstrate high speed at the beginning of a project, which has a positive effect on our confidence and promotes career growth.

But this also means that we postpone mistakes until the moment when there is little time left, and there are already a lot of people involved in the project. But in such conditions the cost of rework increases manyfold! We can definitely do smarter things.

The earliest stages of a project are not the time to make low-level decisions. Save the details for later.

Requirements change, branches remain

Let's imagine that we have overcome the difficulties described above (either by developing a coherent product from the very beginning, or later reconsidering some of the early solutions and abandoning them) and we have a working product. Guess what happens next?

Requirements are changing!

Suddenly it turns out that users actually wanted the product that results from decisions achnq. We go back, throw out some code, write new code, refactor, and end up with a new set of design decisions.

It's only now that we're learning what users might actually want. achmp!

Some businesses are lucky and at this point they can say: “Here. This is your finished product. Do with it what you want.” In other businesses this process never ends; we keep finding new requirements and – hypothetically – forever wandering through the branches of the design tree.

In practice, this process cannot last forever. Remnants of our past mistakes (dotted nodes) complicate the codebase and make it increasingly difficult for us to move between branches. In the end, new competitors will be able to do it faster and take away our users.

Some decisions cannot be changed

At some point, we may encounter a product requirement that is impossible with the current implementation of one of the high-level nodes and requires, for example, to go to another branch in the question c. Such changes are very expensive because all subsequent decisions depend on them. If we redo cwe will also have to redo hmp. You will have to start a large and expensive project to rewrite the entire product, and this is almost always a bad idea.

Again, remodeling means we accumulate even more leftovers. Although we need to change our decision on the issue c, at the moment there is no longer a practical way to do this, all that remains is to create a new product from scratch. Therefore, it is very important that high-level decisions are correct from the beginning.

The Paradox of High-Level Decisions

The problems described above lead us to what can be called the design paradox.

  • We need at first focus on high-level issues because otherwise we risk making incompatible decisions at a lower level.

  • We can only make informed, high-level decisions by postponing them for as long as possible, when the real requirements for the product become clearer.

One thing worth noting here is that not all high-level decisions will change. In any field it is easy to find constants, or at least reliable connections. Sometimes they are called fundamental laws of the area, and a big part of developing innovative products is discovering them. Here are some simple examples:

  • Passwords must be kept secure. If we use password-based authentication, we need resources to store them securely.

  • There will be bugs in our software, so we need to provide a way to fix them.

  • Network I/O is slower than local memory access – although the next fastest thing after “my memory” may well be “someone else's memory on the same local network”.

  • People don't understand tables with a lot of numbers. They need visual representations of quantitative data.

  • Meteorologists must be able to present fronts in their weather visualization programs.

It's okay to make decisions based on these “fundamental laws” and write code accordingly, because they are unlikely to change. But there are also high-level solutions that can subsequently become unsuitable due to changing requirements. How to deal with this?

Program family

Let's take a step back. The key realization is that we Not We develop one program, which we then develop.

Instead, we immediately assume that we will create several very similar programs. We will create family of programs. We build this fact into our project from the very beginning. We take into account that we won't have the right requirements (we never have them) and plan to create multiple versions of the product. This changes the way we design.

Of course, we still only build one product at a time: the one that meets our best current understanding of the requirements. But we're building it in such a way that other products we might need in the future are easy to create.

Having realized this, we begin to think in modules.

Modules allow you to defer decisions

When solving questions for which the answer may change in the future, we do not write code that reflects our solution immediately. Instead, we find an interface that matches all possible solutions to a given issue, and write code for that interface. It turns out that we hide our solution is inside a module, and the rest of the code works with the interface of this module and does not require a clear answer from us here and now.

If we hide the decision made in the question hin our original product ac-(h)-mtthen first we will get the same product.

But then, when we realize that we might need a product instead ac-(h)-nq, we won’t have to go back, throw away old solutions and rewrite the code. This thread will still work with the generic interface we made to hide the solution h. We can complete the missing parts without changing what already exists.

This is even more important for high-level decisions. If we hid the solution c into a module, we could create a program a-(c)-gowithout changing anything on the other branch, because none of the solutions g, h or any subsequent ones will not lose relevance, because the module c can work with any of the branches.

Which decisions to hide and how?

There's an obvious reason we don't hide every solution in a module: modules are more expensive than assumptions. Compared to specific assumptions, modules:

  • take longer to create because they need a universal interface. And if the interface is not universal enough, they can become a bottleneck, which is even worse;

  • increase the entropy of the code, making it difficult to understand how it actually works.

It is worth hiding only some decisions – those that are likely to change and the cost of redoing which would be unacceptably high.

Now that we know what modules are for, we also know what they do Not intended:

  • We don't create a module just because we found a noun in the specification.

  • We don't split our software into two modules because there will be two teams working on it.

  • We don't create 100 modules because we recently read about the convenience of microservices.

  • We are not creating a new module because the source code for the current module is growing too large.

We are creating a module, to hide the design solutionwhich we are forced to accept until we are sure that it will not changemaking it difficult to accept the following.


Freely translated by the Russian Hacker News project. Original article

I'm leading telegram channel with translations of interesting articles from Hacker News and more. Posts are published no more than once a week and only on business, no advertising. Subscribeso as not to miss out on fresh stuff!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *