Deconstructing patterns
Imagine a dungeon with dragons. It is made of halls connected by tunnels. Each hall is cohesive. Tunnels are narrow interfaces that decouple them. A hall is amorphous – it can have any shape but it cannot open to another hall except through a tunnel – such are the rules of the game. The tunnels both restrict the freedom of the halls and interconnect them.
SOLID principles#
If cohesion and decoupling dictate software architecture, they should surface in its principles. Let’s take a look at SOLID:
- The single responsibility principle, also known as do one thing and do it well, is a general advice for keeping unrelated functionality decoupled.
- The open-closed principle and Liskov substitution principle decouple the logic of the parent class or the code that uses it, respectively, from the functionality of its subclasses.
- The interface segregation principle decouples independent parts of an object’s interface.
- The dependency inversion principle decouples an object’s users from its implementation.
Please beware that each of those principles in and of themselves invokes decoupling which is not free – your software may end up having too many moving parts and strict rules to remain easy to read and support.
Gang of Four patterns#
Let’s now discuss something more practical, namely the [GoF] patterns which seem to be ingenious, yet hacky, ways for rearranging the roles in your code. They override ordinary OOP rules, which is useful when you need extra flexibility. For example, the creational patterns interfere with the normally cohesive select type – create – initialize – use sequence of operating an object.
Some patterns provide a basic decoupling:
- Adapter translates between two interacting components so that they may evolve independently.
- Observer decouples an event from the reactions which it causes by registering the event handlers at runtime.
- Chain of Responsibility separates the method invocation from the method execution. A client’s calling a method of an object runs the corresponding method of another object.
Others break the functionality or data of a class into two or more parts, juggling them at runtime:
- Proxy separates an object’s representation from its implementation, enabling lazy loading or remote access.
- Flyweight extracts an immutable data member of a class and merges multiple instances of that identical data to save memory.
- Strategy and Decorator decouple a dimension of an object’s functionality to allow runtime changes in or composition of the object’s behavior, respectively.
- State separates an object’s behavior into multiple classes based on the object’s current state.
- Template Method decouples several aspects of a class’s behavior from its main algorithm and envelops the variations of those aspects into subclasses.
- Bridge separates a high-level hierarchy of classes from their low-level implementation details which may comprise an orthogonal hierarchy.
- Memento decouples the lifetime of an object’s state from the object itself.
On the other hand, several patterns gather separate components together:
- Command collects all the data required to call a method.
- Mediator is a cohesive implementation of multi-object use cases.
- Composite and Facade represent multiple objects as a cohesive entity. A Composite broadcasts a call made to its interface to every object which it contains, while a Facade orchestrates the subsystem which it wraps.
- Abstract Factory and Builder encapsulate type selection and initialization for several related hierarchies, so that the client code gets objects from a consistent set of types. On top of that, a Builder cross-links the objects it creates into a cohesive subsystem, which is then returned to the builder’s client as a whole.
The remaining patterns pick an aspect or two of an object’s behavior and move them elsewhere:
- Iterator moves the code for traversal of a container’s elements from the container’s clients into the container’s implementation, decoupling the clients from the iteration algorithm.
- Visitor aggregates the actions that a client needs to perform on each kind of object in a hierarchy, decoupling them from the classes that constitute the hierarchy.
- Interpreter decouples client scenarios from the rest of the system by having them written in a dedicated language and run in a protected environment.
- Prototype binds the type selection and initialization together and decouples them from the object creation.
- Singleton binds the creation and initialization of a global object to every call of its methods.
- Factory Method decouples the initialization from type selection and hides both from the class’s users.
As we see, every [GoF] pattern boils down to binding (making cohesive) and/or separating (decoupling) some kind of functionality or responsibilities.
Architectural metapatterns#
Finally, let’s close the book by iterating over the metapatterns and looking into their roots through the lens of unification and separation.

- Monolith keeps everything together for quick and dirty projects:
- Total cohesiveness results in low latency, cost-efficient performance, and easy debugging.
- Shards slice a large-scale application into multiple instances:
- Decoupling the instances enables scaling but sacrifices the consistency of shared data.
- Layers separate the high-level code from the low-level implementation:
- Cohesion within a layer makes it easy to implement and debug.
- Decoupled layers may vary among themselves in technologies and properties, but are somewhat slower and hard to debug in-depth.
- Services divide a complex system into subdomains:
- Cohesiveness within a service keeps it simple and efficient when it does not need to consult with other services.
- Decoupling enables the development of larger codebases by multiple specialized teams but any global use cases become complicated.
- Pipeline segregates data processing into self-contained steps:
- Decoupling simplifies reassembling or expanding the system but increases its latency.

Grouping related functionality:
- Middleware separates the implementation of communication and/or instance management from the business logic:
- The cohesive communication layer is not only reliable, but also uniform, which makes it easy to learn.
- Decoupling the communication concerns from the business logic simplifies the latter.
- Shared Repository dissociates data from code, enabling data-centric programming:
- Cohesive data is consistent and easy to handle.
- Decoupled business logic can be scaled or subdivided independently of the data.
- Proxy mediates between a system and its clients, taking care of some aspects of their communication:
- A cohesive edge component is easier to manage and secure.
- Decoupling generic aspects simplifies the business logic but usually increases latency.
- Orchestrator collects a multitude of complex use cases into a dedicated layer:
- Cohesive use cases are easy to comprehend and debug.
- Decoupling the use cases from the domain logic allows for variation in technologies but increases latency and complicates in-depth debugging.
- Sandwich distantiates both control and data from the domain rules, which become segmented:
- Cohesive use cases and data integrate the system.
- Decoupling the subdomain components from each other and from the system-wide layers keeps every part of the system reasonably small and independent.

- Layered Services first decouple the subdomains, and then the layers within each subdomain:
- Decoupled subdomains allow for multi-team development and large codebases but complicate global use cases. Decoupled layers enable variation in technologies within a subdomain and limit interdependencies between subdomains to a single layer.
- Polyglot Persistence divides data among multiple data stores:
- Decoupling improves performance through data store specialization at the cost of consistency.
- Backends for Frontends dedicate one or two components (a Proxy and/or Orchestrator) to each kind of client.
- Decoupling allows for customization on a per-client-type basis but makes it hard to share functionality among the clients.
- Service-Oriented Architecture first segregates a large system into layers, then subdivides each layer into services:
- Decoupling layers strangely enables reuse as any component of an upper layer can access every component below it. Decoupling services within the layers allows for multi-team development. Drawbacks include high latency, system complexity, and interdependencies.
- Hierarchy recursively separates general and specialized logic, tackling complexity:
- Cohesive general and subdomain-specific business logic helps readability and debugging.
- Decoupled layers and subdomains allow for modification and expansion of local functionality at the cost of performance.

- Plugins separate customizable aspects of a system’s behavior:
- Decoupling several aspects of a system allows for it to be fine-tuned but requires careful design and may lower performance.
- Hexagonal Architecture isolates the business logic from its external dependencies:
- Decoupling protects from vendor lock-in and supports automatic testing at the cost of lost optimization opportunities.
- Microkernel mediates between resource consumers and resource producers:
- Cohesive resource management optimizes resource usage.
- Decoupling allows for seamless replacement of resource providers.
- Mesh aggregates distributed components into a virtual layer:
- Virtual cohesion hides the complexity of distributed communication from the client code.
- Actual decoupling (distribution) of the nodes enables scaling and fault tolerance.