General Approach to Breaking Down a Monolith into Components
The main topic of this discussion is "component-based decomposition." Like any form of decomposition, this is only part of the process of building application architecture, to reduce the coupling between components. In some cases, this requires changing the component structure of the application.
Monoliths are often highly coupled, component-based applications, meaning that a component itself is not necessarily an indicator of good decomposition. To reduce coupling, two approaches are commonly used: "extracting components" and "branching (duplicating) components."
In the first case, we gradually reduce coupling by selectively extracting components from the entire application. In the second case, the entire monolith is copied, and unnecessary elements are removed.
Moving away from monolithic architecture requires a well-thought-out decomposition into components, and we will explore the following algorithm:
- Codebase optimization
- Determining the level of coupling
- Establishing architectural boundaries
- Increasing the level of abstraction in interfaces
- Identifying "domains"
- Transitioning to a modular component foundation
Architecture vs. Decomposition
Often, during meetings, you hear phrases like, "Now let’s discuss the project's architecture," followed by conversations about how to split the program into classes and how to structure the project's files. In reality, this is not architecture, but decomposition. Why? Let's break it down.
Definitions:
Decomposition is the process of breaking down a whole into parts. It's a general technique used to solve problems by breaking them into smaller sub-problems. Solving these smaller problems helps in forming the solution to the original problem as a whole. (source: Wikipedia)
Software Architecture is a set of the most important decisions about the organization of a software system. Architecture includes:
- Selection of structural elements and their interfaces, and how they cooperate
- Connection of these elements to larger systems
- Architectural style, guiding the organization of elements, interfaces, and collaboration (source: Wikipedia)
While decomposition is indeed a component of building software architecture, the concept of architecture is much broader. It’s important to note that architectural decomposition is generally based on functional decomposition rather than modular or class-based decomposition.
If the discussion revolves only around dividing the program into classes (and not identifying interfaces, distributing responsibilities, or defining requirements), this can hardly be considered "architectural design." It’s more appropriate to call it "modular decomposition" or "class-based decomposition." True architecture is built from the top down, beginning with gathering and defining requirements.
Workflow for Component Decomposition of a Monolithic Application
Step 1: Codebase Optimization
When working with an existing monolithic application, the first step is to understand the extent of the technical debt and the quality of the current architecture. The simplest way to assess architectural quality is by building a "heat map" of the application.
A heat map is a schematic representation of application components as rectangles colored in a gradient from green to red. The size of each rectangle is determined by the number of lines of code and the color is based on metrics like:
- Cyclomatic complexity
- Depth of inheritance
- Number of conditional expressions
The goal is to visualize the code quality, highlighting the most and least overloaded areas, and assess how balanced the code is in terms of maintainability.
Step 2: Reducing Coupling
After optimizing the codebase, the next step is to evaluate and reduce coupling. The optimal level of coupling is 2–3 components; anything more than 7 is problematic.
Solution: To optimize coupling, the most effective methods are:
- Reducing the strength of dependencies ("composition," "usage," "knowledge")
- Distributing responsibilities
- Extracting reusable code
- Applying Dependency Injection (DI)
Step 3: Establishing Architectural Boundaries
Once the codebase is optimized, and coupling is reduced, the next step is to clearly define architectural boundaries to isolate functionality.
Solution:
- Group components into modules and establish clear interaction interfaces
- Create graphical representations with architectural boundaries (e.g., using the C4 model)
- Define namespaces (possibly along module boundaries)
Step 4: Raising the Level of Interface Abstraction
In most monolithic applications, direct access to application entities is common (e.g., direct method calls). Exceptions typically include database interactions, which use either a driver or an ORM layer.
Solution: Raising the level of interface abstraction is usually achieved by moving from interactive to reactive mechanisms, such as Pub/Sub, where there’s a source, a receiver, and a mediator (e.g., a message bus). Data formats and serialization methods (binary or text) are also standardized.
Step 5: Transitioning to a Modular Component Foundation
The next step is to organize the components into domains, where each domain is essentially a "micro-monolith" (either a service or a microservice) that handles its own business logic.
Solution:
- Granulate domains based on service boundaries
- Create isolated data sources for each domain
- Implement an Enterprise Service Bus (ESB)
- Use the Database per Service pattern
Step 6: Preparing for Load Scaling
The final step is to design a distributed monolith for scalability.
Solution:
- Implement patterns for transactional logic in distributed systems (e.g., SAGA)
- Use load balancing and message routing mechanisms
- Apply reactive principles in the application design
Comments
Post a Comment