Published November 10, 2019
by Doug Klugh
Build for the Future
Avoid one of the biggest technical debt patterns around — the monolith. Build modular systems by employing engineering practices to decouple your architecture and encapsulate functionality, while embracing modern cloud platforms to optimize reliability, resiliency, durability, scalability, and security for compute and storage systems. Software systems, especially cloud-native applications, should be built as a composite of small components and/or services that can be autonomously developed and independently deployed.
What is a Monolith?
A Monolith is a software application that is built with very little modularity and is (usually) wholly contained within a single component or executable (such as a DLL, JAR, or gem). The functionality of the application is tightly coupled and highly dependent on other functionality with very little cohesion within its code. This creates a maintenance nightmare. Change one piece of functionality in one part of the system and break five others in another part. Not only does this impede maintainability, but many other software qualities such as extensibility, testability, scalability, portability, resiliency, security, etc., etc., etc. — the list goes on and on.
Decoupling the Monolith
There are many ways to decouple a monolith. The trick is to do it in such a way that it optimizes your quality model — those software qualities that best enable you to meet your customers' needs today and well into the future.
Separation of Concerns (SoC) is a design principle that encapsulates cohesive information and/or logic in a function, class, module, or component. SoC is an effective mehtod for decoupling software systems and can be achieved by applying a variety of other engineering principles.
Write functions in such a way that if they change state, they do not return values; and if they return values, they do not change state. This is a very effective way to manage side effects within your code by controlling how and where they occur. At an absolute minimum, Getters and Setters should always adhere to this principle.
Build code that has limited knowledge of other parts of the system and interacts directly only with closely related classes. Methods should be protected from understanding the internal structure of other methods or classes. This is also known as the Principle of Least Knowledge, a proven strategy for promoting information hiding and building high quality, loosely coupled software. This can be achieved through the practice of Tell, Don't Ask.
To decouple functionality, write methods that tell objects what to do. Do not ask for their state, then perform functions on behalf of those objects. This will reduce query functions, encapsulate knowledge, and ensure compliance with The Law of Demeter.
Build different implementations of a single method or class to allow your software to dynamically choose which type of object best fulfills the current need. This may include calling a particular instance of an overloaded method based on argument types or instantiating a particular type of object to realize a specific use case.
The five design principles of SOLID, when applied appropriately, are a very effective way to decouple monolithic applications.
A class should have only one responsibility with only one reason to change.
A class should be open to extension but closed for modification.
An object should be replaceable with its subtypes without altering any system behavior.
Many specific interfaces are preferable to a single generic interface.
A class should only depend upon abstractions, not implementations.
The Component Cohesion Principles help determine which classes are best grouped together within a component and which classes we should keep apart.
Components need to be large enough to justify the cost of managing the release cycle.
Classes that change for the same reason should be grouped together in the same component.
Create components where if one class in the component is used, they’re all used.
The Component Coupling Principles help to effectively manage dependencies between components.
The dependencies between components must not form a cycle.
Components should depend upon each other in the direction of increasing stability.
The more stable a component is, the more abstract it should be — so that it can conform to the Open/Closed Principle.