Coupling in software development refers to the degree of dependency between two modules/classes/objects in a software application. For instance, classes are coupled when methods in one class use methods or instance variables defined in a different class.
As you can guess, tight-coupling is when there are many inter-dependencies between two modules. Alternatively, loose-coupling is when there are few inter-dependencies between two modules.
Cohesion is not something you can create automatically. Cohesion is discovered in a particular context. That is why it is so hard for cohesion to be reliably measured. We will discuss this in detail later, stay tuned.
Cohesion and coupling
Let me show you some pictures. In each figure below, there are the very same elements with the very same dependencies. Those are further differently organized. Related domain concepts are represented with the same color:
Low cohesion, tight coupling
Elements in the first picture have no explicit boundaries; they are an example of so-called coincidental cohesion. Such architecture is known as the Big Ball of Mud or the God Object (in OOP code).
High cohesion, tight coupling
The second picture shows a system with three modules and a lot of dependencies between them. Although the modules are highly cohesive, they are cohesive by the wrong key. This happens when code is organized by other than a domain relationship. A typical example is a logical organization of code in the Layered Architecture: just imagine modules such as controllers, repositories, services, etc. Have you seen these already somewhere? Hell yeah!
High cohesion, loose coupling
The system in the third picture shows the ideal scenario: correctly organized modules leading to high cohesion and loose coupling. The right key for organization is functionality, in other words, a business domain. The domain defines abstractions with a stable purpose the cohesion is driven upon. By the way, that is the main idea of the Domain-Driven Design.
So you’ve defined boundaries based on functional cohesion, but they must have some interdependence between each other?
Having a free for all where any service can be coupled to any other service is still a hot mess of spaghetti. In other words, tight coupling. The system will still be hard to change and fragile to change.
You want to remove the tight coupling by having service boundaries be independent and not directly coupled to other services. One way of achieving this is through loose coupling provided by messaging.
Because you’re grouping by business capabilities, and the data behind those capabilities, each service should have all the data it needs. This means that you aren’t coupling services because you need to fetch data from them to perform an action.
Services together need to create workflow and exchange information. As an example from the diagram above, if the Warehouse has a “Quantity on Hand”, you might think that Sales would need that so it knows if it can sell a given product. However, Sales actually has its own concept called ATP (Available to Promise) which is a business function that is the projected amount of inventory it can sell. This consists of what’s in stock in the Warehouse, not allocated existing orders (Invoicing), as well as purchase orders and expected receipts (Purchasing).
Sales can maintain their ATP by consuming events from other services. It does not need to make directly coupled to an API to make calls at runtime to calculate ATP for a given product. It maintains and owns the ATP for all products based on events it’s consuming that are published from other services. When Invoicing publishes an OrderInvoiced event, it can subtract an amount from ATP. If the warehouse does a stock count and publishes an InventoryAdjusted event, Sales will update the ATP accordingly.
From a lower-level code perspective, the coupling can be challenging when we have to deal with many different technical concerns. Web, Authorization, Validation, Data Access, and Business Logic. But as mentioned earlier, each feature/capability can define how each of these concerns is handled.
While you can share between features, such as a domain model, this naturally starts limiting the coupling between a set of features.
One approach to handle the common concerns is by using the pipes & filters pattern. Specifically, the Russian doll model which the request passes through a filter that can call (or not) the next filter. This allows you to separate various concerns and create a pipeline for a request.
High cohesion and loose coupling are the main design drivers towards simple system architecture, which is easy to understand, change, and maintain. High cohesion and loose coupling help us reduce accidental complexity and create modules with well-defined boundaries.
- Coupling is about connections, cohesion is about belonging together.
- Cohesion cannot be created automatically; instead it is discovered in a context.
- Cohesion is defined by the clients.
- True cohesion is domain-driven.
- High cohesion results in loose coupling.
High cohesion is to die for. It enables all others, loose coupling included.