Clean Architecture is a software design pattern initially introduced by Robert C. Martin (Uncle Bob). It is based on several architectural principles, such as hexagonal and onion architecture. Clean Architecture allows us to deal with and find solutions to a wide range of problems we might encounter during software development or an application's lifecycle. We have discussed these problems in our blog post The Multifaceted Problems of Software Development.
This post will explore Clean Architecture, examining its core principles and how it changes our approach to software development. While we focus here on the design and underlying concepts of Clean Architecture, we also have a companion blog post that addresses its problem-solving aspects. If you're interested in learning how Clean Architecture can help address common challenges in software development, I recommend reading our upcoming article How Clean Architecture Solves Common Software Development Challenges. Stay tuned!
Clean Architecture was also the topic of a talk I gave in 2023 at the code.talks conference. The talk is available on YouTube; you can find by clicking on this link: https://www.youtube.com/watch?v=v3HkasWQppk
Clean Architecture
Clean Architecture is a software design approach that aims to create better software systems in terms of maintainability, testability, and adaptability. Based on other architectural guidelines, such as Hexagonal and Onion Architecture, Clean Architecture structures software systems to isolate the core business logic from external dependencies.
This approach embodies two crucial principles:
- Technology and third-party library decisions can and should be deferred until later in the project, allowing the initial focus to remain on the domain.
- The capabilities of external libraries should not constrain the domain. Instead, the domain defines the requirements, which helps find libraries that match those requirements afterward. The connection between the domain and libraries is then implemented through so-called adapters.
Clean Architecture achieves these goals by separating the domain and all other components. This separation allows developers to concentrate on core business logic first, ensuring that external factors or implementation details do not compromise the essential functionality of the system.
Clean Architecture organizes software into different layers, with the core business logic at the center, isolated from external concerns. Let's take a look at a simple illustration:
This basic diagram depicts the key concept of Clean Architecture:
- The innermost
Domain
layer contains the core business logic and entities. - The
Application
layer surrounds it, implementing use cases and coordinating data flow. - The outer layers,
Presentation
andPersistence
, handle external concerns like user interfaces and data storage.
The arrows pointing inward represent the dependency rule: outer layers depend on inner layers but not vice versa. This ensures that the core business logic remains independent of external technologies and frameworks.
In the following sections, we go into more detail to understand Clean Architecture. Let us first take a look at a more detailed representation. You will notice that while the core principles remain the same, the detailed diagram breaks down the layers further and provides more specific terminology:
[Source: Robert C. Martin (Uncle Bob) - The Clean Code Blog]
This extended diagram expands on our simplified version:
- The
Entities
in the center correspond to ourDomain
layer. Use Cases
align with ourApplication
layer.- The outer layers are further divided into specific components like Controllers, Presenters, and Gateways, which handle various aspects of the
Presentation
andPersistence
concerns.
As we look at each layer, you will see how this more detailed structure supports the core principles of Clean Architecture while providing a framework for organizing complex software systems.
Core Principles
Clean Architecture is built upon several fundamental principles that guide its structure and implementation:
- Domain-Centric Approach: At its core, Clean Architecture focuses on the business domain, placing the essential business logic and rules at the system's center.
- Isolation of Domain Logic: A key feature is separating the core business logic from external concerns such as user interfaces, databases, and third-party integrations.
- Layered Structure: As illustrated in the detailed diagram above, the architecture organizes code into distinct layers, each with a specific purpose. These layers include Entities, Use Cases, Interface Adapters, and Frameworks & Drivers.
- Dependency Rule: Dependencies only point inwards, meaning outer layers can depend on inner layers, not vice versa. This ensures the core remains independent of external changes. This rule is also called the Dependency Inversion Rule.
- Use of Abstractions: The architecture promotes the use of interfaces to define boundaries between layers, enhancing flexibility and testability.
By following Clean Architecture's principles, we can create systems that are more maintainable, testable, and adaptable to change.
The domain-centric approach and isolation of core logic enhance maintainability by centralizing business rules. The layered structure and use of abstractions improve testability by allowing components to be tested in isolation. The Dependency Inversion Rule and the use of interfaces contribute to adaptability by reducing the impact of external changes on the core system.
In the following sections, we will examine each layer to understand how these principles better.
Layer Breakdown
Clean Architecture is divided into four layers:
- Entities: This layer represents the enterprise business rules and, as such, the core business logic of the system and business data. This layer is the most abstract one and is less likely to change. The entities layer consists of domain objects and is kept separate from all other concerns in the application. This separation often results in easier maintenance and testing of the system, as many core business logic modifications can be made without affecting other parts of the system. However, it's important to note that changes to interfaces, which define the requirements of the domain layer, can still significantly impact other layers. Another significant benefit of separating the core business logic and business data in this layer is that it can be reused in different systems, even if the systems use other external technologies.
- Use Cases: This layer holds application-specific business rules that fulfill the system's requirements and orchestrate data flow to and from the entities. It contains the core application logic and describes how the system should respond to various use cases or scenarios, often involving interactions with external actors.
- Interface Adapters: This layer contains implementations of interfaces defined in the use case layer and adapters that convert data between the formats of the inner and outer layers. It includes components like controllers, presenters, and gateways that mediate between use cases and the external environment, such as user interfaces, databases, and external services. These components ensure that the use cases remain isolated from the specifics of external technologies.
- Frameworks & Drivers: This outermost layer contains the external frameworks and tools that the application interacts with, such as databases, web frameworks, UI, and other external systems. It consists of adapters and implementations that connect these external elements to the inner layers. The layer's primary function is to isolate all details, such as the infrastructure, ensuring that changes to external technologies have minimal impact on the application's core. It acts as a boundary between the external environment and the application's inner layers, as shown by the blue outermost circle in the Clean Architecture diagram.
Crossing Boundaries
When looking at the Clean Architecture diagram in this post, you may have observed the arrows pointing from the outside to the inside. These arrows represent the flow of control in Clean Architecture, moving from the outer layers to the inner layers. This means the higher-level components (inner circles) do not depend on lower-level components (outer circles). For example, the entities do not know about the use cases, but the use cases are aware of and understand the entities.
However, this raises an essential question: How can a use case interact with a presenter or a database if it doesn't know about these outer layer components? The answer lies in two key concepts:
- Interfaces: An interface defines an agreement or contract between the different layers of our system. The code in an inner layer can interact with the code in an outer layer via interfaces defined by the inner layer. This is known as the Dependency Inversion Principle, one of Clean Architecture's core principles.
- Dependency Injection: The domain and use cases don't know about specific implementations at compile time, but they must work with concrete implementations at runtime. This is where dependency injection comes in.
To understand this, let us look at the following code example:
// Interface defined by the Use Case layer
interface OrderRepository {
save(order: Order): Promise<void>;
}
// Use Case
class CreateOrderUseCase {
constructor(private orderRepository: OrderRepository) {}
async execute(order: Order): Promise<void> {
await this.orderRepository.save(order);
}
}
// Concrete implementation in the Frameworks & Drivers layer
class DatabaseOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
// Database-specific code
}
}
// Main entry point of the application
const orderRepository = new DatabaseOrderRepository();
const createOrderUseCase = new CreateOrderUseCase(orderRepository);
In the above example, the CreateOrderUseCase
depends on the OrderRepository
interface but not any specific implementation. The concrete DatabaseOrderRepository
is injected into the use case at runtime.
It's important to understand that there needs to be a part of the application that wires everything together. This part of the software might seem less "clean" since it needs to know all the layers to connect them correctly. Typically, this wiring happens in the application's main entry point or a configuration file.
By using interfaces and dependency injection, we maintain the principle that inner layers don't depend on outer layers at compile time while still allowing the necessary connections at runtime. This approach enables the flexibility and testability that Clean Architecture aims to provide, allowing us to easily change implementations or mock dependencies for testing.
A Real-World Example
A good example of the benefits of Clean Architecture is an e-commerce application. In such a system, Clean Architecture allows us to isolate the core business logic - such as product management, order processing, and customer management - from external systems like payment gateways and inventory services.
This separation provides several advantages:
- Flexibility: Developers can change external components, like a payment gateway, without affecting the core business logic.
- Adaptability: The system can easily adapt to new requirements or technologies by modifying the outer layers without impacting the core.
- Testability: Core business rules can be tested independently of external frameworks or UI.
Let us look at a simplified code example to illustrate these principles:
// Entities Layer
class Product {
constructor(public id: string, public name: string, public price: number) {}
}
class Order {
constructor(public id: string, public products: Product[], public total: number) {}
}
// Use Cases Layer
interface OrderRepository {
save(order: Order): Promise<void>;
}
class CreateOrderUseCase {
constructor(private orderRepository: OrderRepository) {}
async execute(products: Product[]): Promise<Order> {
const total = products.reduce((sum, product) => sum + product.price, 0);
const order = new Order(Date.now().toString(), products, total);
await this.orderRepository.save(order);
return order;
}
}
// Interface Adapters Layer
class OrderController {
constructor(private createOrderUseCase: CreateOrderUseCase) {}
async createOrder(productIds: string[]): Promise<Order> {
// In a real application, you would fetch products based on IDs here...
const products = productIds.map(id => new Product(id, `Product ${id}`, 10));
return this.createOrderUseCase.execute(products);
}
}
// Frameworks & Drivers Layer
class DatabaseOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
console.log(`Saving order ${order.id} to database`);
// The actual database operation would go here...
}
}
// Usage
const orderRepository = new DatabaseOrderRepository();
const createOrderUseCase = new CreateOrderUseCase(orderRepository);
const orderController = new OrderController(createOrderUseCase);
// Simulated API call
orderController.createOrder(['1', '2', '3']).then(order => {
console.log(`Order created with total: ${order.total}`);
});
The above code demonstrates how Clean Architecture separates concerns in an e-commerce system, aligning with the layers shown in the diagrams:
- Entities Layer: The
Product
andOrder
classes represent the core business objects and are part of the Entities layer. - Use Cases Layer: The
CreateOrderUseCase
class implements the application-specific business rules, matching the Use Cases layer. - Interface Adapters Layer: The
OrderController
class acts as an adapter between the external dependencies and the use cases, similar to the "Controllers" shown in the diagram. - Frameworks & Drivers Layer: The
DatabaseOrderRepository
class represents an implementation of external frameworks or tools corresponding to the outermost layer in the diagram.
This structure allows for a flexible, maintainable, and testable application:
- The core business logic (
Product
,Order
,CreateOrderUseCase
) remains isolated from external concerns. - The
OrderController
adapts external input to the use case without exposing internal details. - The
DatabaseOrderRepository
handles database interactions without affecting the core logic.
The dependency rule is maintained: outer layers depend on inner layers, not vice versa. For example, CreateOrderUseCase
depends on the Order
entity but not the DatabaseOrderRepository
.
This separation allows us to easily change components (e.g., changing the database implementation) without affecting the core business logic, demonstrating the flexibility and adaptability of Clean Architecture.
Benefits of Clean Architecture
Developers following the Clean Architecture pattern can create more maintainable and testable systems. As illustrated by the circles in the diagrams, the clear separation of concerns allows for modular and reusable code, making it easier to adapt to changing requirements or integrate new technologies. Here are some key benefits:
- Improved Maintainability: Changes to one layer are less likely to affect the other layers; thus, modifying the system without breaking it becomes easier. This is ensured by the dependency rule, shown by the arrows pointing inward in the diagrams.
- Enhanced Testability: The separation of layers allows us to test the core business logic (Entities and Use Cases) independently, making it easier to write unit and integration tests without relying on external frameworks or UI.
- Better Adaptability: The outer layers (Interface Adapters and Frameworks & Drivers) can be modified without affecting the business logic in the core, making the system more adaptable to new requirements or technologies.
- Reusability and Framework Independence: The business logic in the Entities and Use Cases layers is not dependent on external frameworks (shown in the outermost blue layer), allowing reuse in different systems and easier updates of external components.
These benefits are achieved through Clean Architecture's structured layering and dependency rule, as represented in the diagrams.
Conclusion
The Clean Architecture pattern provides an excellent framework for creating software systems that are maintainable, testable, and highly adaptive to change.
By following this pattern, we can create a solid foundation for our applications, ensuring that the core business logic remains unaffected by external changes and dependencies. The separation of concerns across the pattern's different layers, as illustrated by the circles in the diagram, makes both maintenance and testing easier. It also enhances the system's flexibility for adopting new technologies or adapting to changing requirements.
Considering the various challenges we can encounter throughout application development and maintenance, the strengths of Clean Architecture become apparent. By following Clean Architecture principles, we can effectively manage issues such as code duplication, tight coupling, scalability problems, and testing complexities. This leads to software that is easy to operate and evolve and more resilient to changing demands.
The application of Clean Architecture in overcoming software development challenges underscores the importance of thoughtful architectural design when creating software systems. It is crucial to build systems that are not only functional in the short term but also sustainable and adaptable for the future.
You can learn more about the various issues in software development in our blog post, The Multifaceted Issues of Software Development. Our future upcoming post, How Clean Architecture Solves Common Software Development Challenges, explains how Clean Architecture helps us address these issues.
By implementing Clean Architecture, as demonstrated in the layered diagrams and code examples given in this post, we can create robust, flexible, and maintainable software systems that are functional in the long term and adaptable to changing requirements.