System Decomposition

System Decomposition

Part 1: Functional decomposition and how not to design a software system

Software Architecture is the discipline responsible for the design and high-level structure of the system. The essence of the architecture of a system is the breakdown of the concept of the system as a whole, its components and their interaction.

While system design is quick and inexpensive compared to system construction, getting the architecture right is critical. Once the system is built, if the architecture is faulty, incorrect, or simply inadequate for your needs, it is extremely expensive to maintain or extend the system.

When we design a software system, we start with a long list of ideas littered with functional requirements from the customer. Starting from this point is difficult, so we need to take that big fuzzy list of ideas and break it down into little blocks. The act of identifying these constituent components of a system is called system decomposition.

Correct decomposition is critical. Bad decomposition means bad architecture, which in turn inflicts horrendous pain down the road, often leading to a complete system rewrite. Design flaws are a direct result of incorrect system decomposition.

Functional decomposition

Functional decomposition simply means breaking down a system into its building blocks based on the functional requirements of the system, i.e. if the system needs to perform a set of operations A, B, and C it ends up with an A block, a B block, and a B block. C. For example, a system that allows placing orders, making payment and shipping, will end with the ordering service, the payment service and the shipping service.

If your system performs a functionality-based decomposition, then your system has failed. Yes, your project failed even before writing a single line of code. You may think that what I am saying is a joke, you may say that many software projects are built in this way, but that most do something in one way does not imply that it is correct.

Let's analyze in detail the system that performs functionalities A, B and C. The problem with using 3 services, one for each functionality, is that you are not doing services A, B and C, what you are doing is A then B and then C, exactly in that order. Functional decomposition is also temporary decomposition (calling A then calling B, etc), and the problem with this is that it prevents individual reuse of services. Any other system that also uses B incorporates the notion that service B was called prior to service A and calls service C after completion.

Any system that tries to incorporate any of the services individually will fail, functional decomposition makes it impossible to reuse the components. Therefore, functional decomposition always leads to a huge duplication of functionality in the system and subsystems. The reality during system development is that everyone is reinventing the wheel all the time. Also, you could easily have hundreds of functionalities, while each block is simple, there is a huge non-linear cost to integrate those components together throughout the system.

Problems with functional decomposition

There are two opposite cases but both afflictions are often seen side by side in the same system. One way to perform functional decomposition is to have as many services as there are variations of the functionalities. This decomposition leads to an explosion of services, as a decent sized system can have hundreds of features. The explosion of services inflicts a disproportionate cost on integration and testing and increases overall complexity.

Another functional decomposition approach is to group all possible ways of performing operations into mega-services. This leads to a bloat in the size of services, making them too complex and impossible to maintain. Functional decomposition therefore tends to make services too big and too few or too small and too many.

Functional decomposition often leads to flattening of the system hierarchy. Since each service or building block is dedicated to a specific functionality, someone must combine these discrete functionalities into a required behavior. That someone is often the customer. When the client is the one who orchestrates the services, the system becomes a flat two-tier system: clients and services. Any notion of additional layers disappears.

d1.png

The client is no longer just about invoking operations on the system or presenting information to users. The client is now intimately aware of all the internal services, how to call them, how to handle their errors, etc. Calling the services is almost always synchronous because the client proceeds along the expected sequence of then , and is difficult to another way to ensure the order of calls while still responding to the outside world. Also, the client is now coupled to the required functionality. Any change in operations obliges the client to reflect that change.

What if there are multiple clients (e.g., web pages, mobile devices), each trying to invoke the same sequence of functional services? It's meant to duplicate that logic across all clients, making all those clients wasteful and expensive to maintain. As functionality changes, you are now forced to keep up with that change across multiple clients, as all of them will be affected. Once that is the case, developers often try to avoid any changes to the functionality of services due to the cascading effect it will have on clients.

Another problem with decomposition is that it requires multiple entry points to the system. The client (or clients) needs to log into the system in three places: once for the service A, then for the B , then for the C, etc. This means there are multiple places to worry about authentication, authorization, scalability, instance management, transaction propagation, identities, hosting, and so on.

As an alternative to sequencing the functional services as in the next figure, one can opt for what, at first glance, appears to be a lesser evil by having the functional services call each other, as shown in figure.

d2.png

The problem now is that the functional services are coupled to each other and to the order of the functional calls. For example, you can call the service only after the service, but before the service. In the case of previous figure, built into the service is the knowledge that it needs to call service B. The service can be called only after the service and before the service. A change in the required order of calls is likely to affect all services up and down the chain because your implementation will have to change to reflect the new required order.

But the Figure does not reveal the whole problem. Now service A must know service B and the contract must contain the parameters that the service will require to perform its functionality. These details were the responsibility of the customer. The problem is compounded by the service, which must now accommodate in its service contract the parameters required to call the and services to perform their respective business functionality. Any change in functionality is reflected in a change in the service implementation, which is now coupled to them. This type of swelling and docking is depicted in Figure.

d3.png

Unfortunately, even previous figure does not tell the whole truth. Assume that the service performed the functionality successfully, and then proceeded to call the service to perform the functionality. However, the service encountered an error and could not run properly. If it is called synchronously, then it must be intimately aware of the internal logic and state of in order to recover its error. This means that the functionality must also reside in the service. If called asynchronously, then the service must now somehow return to the service and either roll back the functionality or contain the rollback within itself.

In other words, the functionality also resides in the service. This creates a tight coupling between service and service and bloats the service with the need to compensate for the success of the service. This situation is shown in Figure

d4.png

I hope these arguments have been enough to show you the problem of functional decomposition. Having understood the problem with the functional decomposition, we can address alternatives for the decomposition

The right way

If functional decomposition is not the way to perform system decomposition, then how to perform decomposition? The answer is not at all simple, like any decision in software development, there is no right choice, there are trade-offs.

In this post we have shown that functional decomposition is a bad way because it maximizes the effort that must be made when we want to change some part of the system and the reality is that change is something inevitable in software development, if it were not the case then we would have finished the design of a perfect system and our work as developers would be finished. Furthermore, the nature of the market and free competition forces all companies to always be in constant change. Being prepared for that change is the true nature of Agile, which is embodied in one of its principles:

Welcome to changing requirements, even late developing. Agile processes harness change to the customer's competitive advantage.

Agile is not the same as speed, filling the backlog with functional requirements and having the goal of completing them in a short time, completely forgetting if it is the best for your future requirements is not Agile. Agile is about that, being flexible enough to add features in a short time and being flexible to change.

So, if in software development the only constant is change, design the system for it. Performs volatility-based system decomposition, encapsulates what changes. Doing this correctly is not easy, but it is the right way. We will talk more about volatility-based decomposition in another post.