Clean Architecture in C#: Building Maintainable and Scalable Applications
Software architecture is the backbone of any application, and a well-thought-out architecture can make the difference between a maintainable, scalable application and a fragile, difficult-to-modify one. Clean Architecture, popularized by Robert C. Martin (Uncle Bob), provides a framework for designing systems that are robust, adaptable, and independent of frameworks, databases, or user interfaces.
In this blog post, we’ll explore the principles of Clean Architecture, its core layers, and how to implement it in a C# application.
What is Clean Architecture?
Clean Architecture emphasizes separating concerns within a software system and organizing the codebase into layers with strict boundaries. Each layer is independent and responsible for specific tasks, ensuring that changes in one layer do not ripple uncontrollably through the application.
The core idea revolves around the dependency rule: dependencies should always flow inward—toward the high-level policies or core business logic. Outer layers can depend on inner layers, but inner layers should never depend on outer layers.
Core Layers of Clean Architecture
A typical Clean Architecture comprises the following layers:
Entities (Core Business Logic)
Represents the core business rules or domain logic.
Completely independent of any external systems.
Examples: domain models like
Order
,Customer
.
Use Cases (Application Logic)
Contains the application-specific business rules.
Orchestrates the interaction between entities and external systems.
Examples:
PlaceOrderUseCase
,CalculateDiscount
.
Interface Adapters (Presentation Layer)
Acts as a mediator between the core logic and external systems (e.g., UI, APIs).
Transforms data structures suitable for external systems into those used by the use case and vice versa.
Infrastructure (External Systems)
Deals with database interactions, frameworks, APIs, or third-party services.
Examples: repositories, API clients, data access code.
Advantages of Clean Architecture
Testability
- Core logic can be tested in isolation without relying on external dependencies.
Flexibility
- Makes it easy to swap frameworks, databases, or external systems without altering the core logic.
Maintainability
- Encourages separation of concerns, making the codebase easier to understand and modify.
Scalability
- With well-defined boundaries, the application can grow without becoming unmanageable.
Implementing Clean Architecture in C#
Let’s build a simple example: an Order Management System.
Step 1: Define the Entities (Core Business Logic)
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; }
public List<OrderItem> Items { get; set; } = new();
public decimal TotalAmount => Items.Sum(item => item.Price * item.Quantity);
}
public class OrderItem
{
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
Step 2: Create the Use Case (Application Logic)
public interface IOrderRepository
{
void SaveOrder(Order order);
}
public class PlaceOrderUseCase
{
private readonly IOrderRepository _orderRepository;
public PlaceOrderUseCase(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public void Execute(Order order)
{
// Business rules validation
if (order.Items == null || !order.Items.Any())
throw new InvalidOperationException("Order must have at least one item.");
// Save the order
_orderRepository.SaveOrder(order);
}
}
Step 3: Add the Interface Adapter (Presentation Layer)
public class OrderController
{
private readonly PlaceOrderUseCase _placeOrderUseCase;
public OrderController(PlaceOrderUseCase placeOrderUseCase)
{
_placeOrderUseCase = placeOrderUseCase;
}
public IActionResult PlaceOrder(OrderDto orderDto)
{
var order = MapToOrder(orderDto);
_placeOrderUseCase.Execute(order);
return new OkResult();
}
private Order MapToOrder(OrderDto dto)
{
return new Order
{
CustomerName = dto.CustomerName,
Items = dto.Items.Select(item => new OrderItem
{
ProductName = item.ProductName,
Quantity = item.Quantity,
Price = item.Price
}).ToList()
};
}
}
Step 4: Implement Infrastructure (External Systems)
public class SqlOrderRepository : IOrderRepository
{
private readonly DbContext _dbContext;
public SqlOrderRepository(DbContext dbContext)
{
_dbContext = dbContext;
}
public void SaveOrder(Order order)
{
_dbContext.Orders.Add(order);
_dbContext.SaveChanges();
}
}
Dependency Injection Configuration
In your Startup.cs
or Program.cs
:
services.AddScoped<IOrderRepository, SqlOrderRepository>();
services.AddScoped<PlaceOrderUseCase>();
services.AddScoped<OrderController>();
Best Practices
Keep the Core Layer Clean
- Avoid dependencies on frameworks or external libraries in your
Entities
orUse Cases
.
- Avoid dependencies on frameworks or external libraries in your
Use Dependency Injection
- Ensure that dependencies (like repositories) are injected, not instantiated within classes.
Stick to Single Responsibility Principle
- Each layer and class should have a clear, single responsibility.
Test Layers Independently
- Write unit tests for core business logic and integration tests for the outer layers.
Conclusion
Clean Architecture provides a robust way to design systems that are scalable, testable, and maintainable. While it requires upfront investment in organizing the codebase and adhering to principles, the benefits far outweigh the initial effort. By following Clean Architecture in C#, you can build applications that are not only functional today but also adaptable to the changing needs of tomorrow.
Happy coding!
Subscribe to my newsletter
Read articles from Joshua Akosa directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by