DEV Community

Cover image for Building Your First Use Case With Clean Architecture
Muhammad Salem
Muhammad Salem

Posted on

Building Your First Use Case With Clean Architecture

​Clean Architecture has emerged as a guiding principle for crafting maintainable, scalable, and testable applications. At its core, Clean Architecture emphasizes the separation of concerns and the dependency rule. The dependency rule dictates that dependencies should point inward toward higher-level modules. By following this rule, you create a system where the core business logic of your application is decoupled from external dependencies. This makes it more adaptable to changes and easier to test.

The Domain layer encapsulates enterprise-wide business rules. It contains domain entities, where an entity is typically an object with methods.

The Application layer contains application-specific business rules and encapsulates all of the system's use cases. A use case orchestrates the flow of data to and from the domain entities and calls the methods exposed by the entities to achieve its goals.

The Infrastructure and Presentation layers deal with external concerns. Here, you will implement any abstractions defined in the inner layers.

The statement "A use case orchestrates the flow of data to and from the domain entities and calls the methods exposed by the entities to achieve its goals" is a key concept in Clean Architecture. Let's break this down and explore it further.
Image description

Understanding Use Cases in Clean Architecture:

In Clean Architecture, a use case represents a specific action or scenario that the system can perform. It's part of the application layer and acts as an intermediary between the outer layers (like UI or external systems) and the inner domain layer.

The use case is responsible for:

  1. Accepting input from the outer layers
  2. Manipulating domain entities
  3. Coordinating the flow of data
  4. Returning results to the outer layers

Data Flow in Clean Architecture:

The flow of data in Clean Architecture typically follows this pattern:

  1. External request comes in (e.g., from UI or API)
  2. The request is passed to a use case
  3. The use case interacts with domain entities
  4. Results are passed back through the layers

Let's illustrate this with a real-world example in C#. Consider an e-commerce application where we want to implement a "Place Order" use case.

First, let's define our domain entities:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public List<OrderItem> Items { get; set; } = new List<OrderItem>();
    public decimal TotalAmount { get; private set; }

    public void AddItem(Product product, int quantity)
    {
        Items.Add(new OrderItem { Product = product, Quantity = quantity });
        CalculateTotalAmount();
    }

    private void CalculateTotalAmount()
    {
        TotalAmount = Items.Sum(item => item.Product.Price * item.Quantity);
    }
}

public class OrderItem
{
    public Product Product { get; set; }
    public int Quantity { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's create a use case for placing an order:

public class PlaceOrderUseCase
{
    private readonly IProductRepository _productRepository;
    private readonly IOrderRepository _orderRepository;

    public PlaceOrderUseCase(IProductRepository productRepository, IOrderRepository orderRepository)
    {
        _productRepository = productRepository;
        _orderRepository = orderRepository;
    }

    public async Task<int> Execute(PlaceOrderRequest request)
    {
        // Create a new order
        var order = new Order();

        // For each item in the request, add it to the order
        foreach (var item in request.Items)
        {
            var product = await _productRepository.GetByIdAsync(item.ProductId);
            if (product == null)
            {
                throw new ProductNotFoundException(item.ProductId);
            }
            order.AddItem(product, item.Quantity);
        }

        // Save the order
        await _orderRepository.SaveAsync(order);

        // Return the order ID
        return order.Id;
    }
}

public class PlaceOrderRequest
{
    public List<OrderItemRequest> Items { get; set; }
}

public class OrderItemRequest
{
    public int ProductId { get; set; }
    public int Quantity { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the PlaceOrderUseCase orchestrates the flow of data:

  1. It accepts input from the outer layers (the PlaceOrderRequest).
  2. It interacts with the domain entities (Order and Product).
  3. It uses infrastructure services (IProductRepository and IOrderRepository) to retrieve and persist data.
  4. It returns a result (the order ID) to the outer layers.

The use case doesn't contain business logic itself. Instead, it delegates to the domain entities (like the Order.AddItem method) and coordinates the overall process.

This approach provides several benefits:

  1. Separation of Concerns: The business logic is encapsulated in the domain entities, while the use case handles the coordination.

  2. Testability: The use case can be easily unit tested by mocking the repositories.

  3. Flexibility: The implementation of the outer layers (like how the order is presented or stored) can change without affecting the core business logic.

  4. Maintainability: Each component has a single responsibility, making the system easier to understand and modify.

In the broader context of software architecture, this pattern of data flow - where requests flow inward to the domain layer and responses flow back outward - is common in many architectural styles beyond Clean Architecture. It's seen in hexagonal architecture, onion architecture, and others. The key principle is to keep the core business logic (the domain layer) isolated and independent of external concerns, allowing the system to be more flexible and adaptable to change.

Top comments (0)