Clean Architecture and Co.: Structuring Software Architecture with Patterns
Suitable architectural pattern ensures well-maintainable systems; an unsuitable one leads directly to chaos. Clean Architecture combines a series of advantages.
(Image: Franck Boston / Shutterstock.com)
- Matthias Eschhold
Structured software is based on a plan that considers the specific requirements of a system and translates them into loosely coupled components. In collaborative software development, development teams need such common plans to create a harmonious and unified architecture without having to coordinate every detail beforehand. If the plans prove effective, they evolve into patterns and principles at different architectural levels.
When fundamentally structuring a system, one must distinguish between architectural styles and architectural patterns, although they do not always clearly demarcate. An architectural style is a means that gives the system a fundamental structure. With the Event-driven Architecture style, for example, the application is based on asynchronous communication, and events influence the architecture and code in many places. The same applies to REST, which dictates a resource-oriented structure.
If a development team opts for microservices as an architectural style, it chooses a distributed system architecture; with the Modular Monolith style, the opposite is true. In complex systems, architects typically combine multiple styles. Some architectural styles complement each other, such as REST and microservices, while others are mutually exclusive, like microservices and the Modular Monolith.
Whether microservices or modular monolith – both say little about the design of internal structures. At this inner architectural level, the application architecture, patterns are used that combine design principles and rules, shaping the basic structure of the application. Architectural patterns of application architecture use areas of responsibility and relationship rules as structuring elements. In the Clean Architecture pattern, for example, these are concentric rings, with the direction of relationships always leading towards the inner core of the ring model. The layered architecture, on the other hand, divides areas of responsibility into hierarchical layers, where each layer may only communicate with the one below it (see Figure 1).
Pattern Language as a Foundation
A pattern language complements architectural patterns for a holistic design plan – from modules and packages to class design. It forms the foundation for a consistent and understandable implementation of patterns and describes a set of design patterns for programming at the class level.
The classes of the pattern language represent business objects, domain logic, and technical components. They are implemented in a class composite, adhering to the defined relationship rules. These rules determine how classes interact with each other, how they depend on each other, and what tasks they have. A business object is characterized by its properties and behavior, while a service implements business logic and domain process control. Such precise differentiation makes architecture clear and comprehensible.
Videos by heise
An important aspect of a pattern language is the organization of code into an easily understandable hierarchy. This promotes the distribution of responsibilities across different classes. In principle, any project can define its pattern language or use an existing one as a basis and expand it with individual requirements. A pattern language also ensures that all team members use the same terms and principles.
This article uses the DDD Building Blocks as the basis for a pattern language, as shown in the following table and Figure 2.
| Element of the pattern language | Description |
| Value Object | A Value Object represents an immutable domain value without its own identity. The Value Object is responsible for validating the domain value and should only be creatable in a valid state. Furthermore, a Value Object implements associated domain logic. |
| Entity | An Entity is an object with a unique identity and a lifecycle. The Entity is described by Value Objects and is responsible for validating business rules that span multiple domain values and for implementing associated domain logic. |
| Aggregate | An Aggregate is a collection of Entities and Value Objects held together by a Root Entity (or Aggregate Root, or simply Aggregate). The Root Entity defines a business consistency boundary, clearly separated from other Root Entities (or Aggregates). |
| Domain Service | A Domain Service implements business logic that does not belong to an Entity or a Value Object. Furthermore, the Domain Service controls the flow of a use case. A Domain Service should be implemented stateless. |
| Factory | A Factory is responsible for the creation of Aggregates, Entities, or Value Objects. The Factory encapsulates the creation logic of complex domain objects. |
| Repository | A Repository is responsible for storing and retrieving Aggregates and Entities from a data source. The Repository encapsulates access to a database or other technical components. |
An example illustrates the difference between a Value Object and an Entity: An Entity could be a specific electric vehicle. Entities are therefore unique and unmistakable. In the real world, this is evident from the globally unique Vehicle Identification Number (VIN). The current state of an electric vehicle at a given time, for example, is described by its charge level, a value that changes over the course of the vehicle's use. The charge level corresponds to a Value Object. It has no identity of its own but is defined solely by its value.
Extension of the Pattern Language Based on Styles and Patterns
The pattern language of the building blocks is incomplete. It requires additional elements that depend on the architectural styles and patterns used. REST as an architectural style, for example, introduces two elements into the pattern language: controller and resource. When integrating REST as a provider, the focus is on the resource, which is provided as a Data Transfer Object (DTO) via the API endpoint. The controller acts as the interface between the consumer's request and the system's domain logic. This means the controller uses the already introduced domain service and delegates the execution of domain logic to it.
When integrating REST as a consumer, the pattern language gains the element Service Client, which is used for retrieving data or executing functions via an external API endpoint. The Domain Service triggers this as part of the domain logic via the Service Client.
The Event-driven Architecture style extends the pattern language with the elements Event Listener, Event Publisher, and the Event itself. An Event Listener listens for events and calls the corresponding Domain Service to trigger the execution of business logic. The Event Publisher publishes a change in domain state via an Event. The Domain Service triggers the event publication as part of its domain logic, using the Event Publisher for this purpose.
The terms listed in these examples are not defined in the literature compared to the DDD Building Blocks and originate from practice. Figure 3 shows the classes of the extended pattern language.
Architectural patterns combine rules, design patterns, and principles. Patterns like Clean Architecture, which are particularly suitable for complex systems with high lifecycle requirements, bundle multiple concepts and therefore influence the pattern language more strongly than other patterns. An example is the Use Case concept in Clean Architecture, which is a central element and extends the pattern language with the elements Use Case Input Port, Use Case Output Port, and Use Case Interactor. Another example is the application of the Dependency Inversion Principle (DIP) in Clean Architecture, which leads to the pattern element Mapper.
After the excursion into pattern languages, this article presents various architectural patterns, which can be divided into layer-based and domain-based.
Layer-Based Architectural Patterns
Layer-based architectural patterns are structured in a data-centric manner. Depending on the pattern, this aspect is more or less pronounced. The layering differs in technical (horizontally cut) and domain (vertically cut) respects. For further description, Simon Brown's terminology of "Package by …" is suitable.
Package by Layer: This pattern organizes the application according to technical aspects, for example, by Controller, Service, and Repository (Figure 4). However, it quickly reaches its limits: Medium and large systems with complicated domain logic require vertical layering based on domain aspects; otherwise, projects tend to end up as complicated monoliths with many architectural violations.
Advantages:
- Familiar and widespread
- Easy to understand and apply
- Practical in small projects
Disadvantages:
- Tight coupling between layers, with the risk of chaotic dependencies as the system grows
- Domain-related functionalities are distributed across many packages
- Difficult to maintain and extend for medium to large applications
Package by Feature: The code is organized vertically based on domain aspects. A cutting heuristic, how exactly the feature is to be derived from the domain requirements, is not defined by the architectural pattern. It only defines that this domain cut must be made. If tactical DDD is applied, the cut is made along the Aggregates (see Figure 5).
Advantages:
- Domain-cohesive code is grouped locally, leading to high maintainability and extensibility.
- Modularization enables independent development of domain modules.
- Domain end-to-end components are loosely coupled.
- Dependencies between domain modules must be explicitly managed, increasing the architecture's robustness against unwanted dependencies.
- Domain-complex, medium to large applications are better managed with vertical layers than with Package by Layer and Package by Component.
Disadvantages:
- Dependencies between domain modules require advanced communication patterns (e.g., events), increasing architectural complexity.
- Vertical modularization must be well thought out to avoid tight coupling between modules.
Package by Component: The pattern structures the application both domain-wise (vertically) and technically (horizontally), with a domain feature being divided into an inbound component and a domain component (see Figure 6). The domain component encapsulates business logic and the associated persistence layer. This subdivision into domain modules is a crucial difference from Package by Layer.
Advantages:
- Good modularization through domain boundaries between components
- High reusability of domain components through different inbound components
- Easier testability due to increased modularization compared to Package by Layer
Disadvantages:
- Tight coupling between inbound and domain layers, with the risk of indirect dependencies and side effects upon changes, especially as the application grows
- Component communication is difficult to manage with increased domain complexity
- Harder to extend for medium to large applications with higher domain complexity