Explain the difference between Domain, Application, Infrastructure, and Presentation layers in Clean Architecture.

5 minadvancedclean-architecturelayersDDD

Quick Answer

Clean (Onion/Hexagonal) Architecture organizes code into concentric layers where dependencies point inward. The Domain layer holds entities and business rules (no external dependencies); the Application layer orchestrates use cases and defines interfaces; the Infrastructure layer implements those interfaces (database, external services); and the Presentation layer (API/UI) handles I/O. The Dependency Inversion Principle keeps the core independent of frameworks and persistence.

Detailed Answer

Clean Architecture (also known as Onion Architecture or Hexagonal Architecture) organizes code into concentric layers with clear dependencies and responsibilities. Each layer has a specific purpose and follows the Dependency Inversion Principle.

Layer Structure:

┌─────────────────────────────────────┐
│           Presentation              │ ← Controllers, UI, APIs
├─────────────────────────────────────┤
│           Application               │ ← Use Cases, Services
├─────────────────────────────────────┤
│             Domain                  │ ← Business Logic, Entities
├─────────────────────────────────────┤
│          Infrastructure             │ ← Data Access, External Services
└─────────────────────────────────────┘

1. Domain Layer (Core)

The innermost layer containing business logic and rules. It has no dependencies on other layers.

// Domain/Entities/Order.cs
public class Order : AggregateRoot<OrderId>
{
    public OrderId Id { get; private set; }
    public CustomerId CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    private readonly List<OrderItem> _items = new();
    
    public void AddItem(ProductId productId, Quantity quantity, Money unitPrice)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Cannot add items to confirmed order");
            
        _items.Add(new OrderItem(productId, quantity, unitPrice));
    }
    
    public void Confirm()
    {
        if (_items.Count == 0)
            throw new InvalidOperationException("Cannot confirm empty order");
            
        Status = OrderStatus.Confirmed;
        AddDomainEvent(new OrderConfirmedEvent(Id, CustomerId));
    }
}

// Domain/ValueObjects/Money.cs
public class Money : ValueObject
{
    public decimal Amount { get; }
    public string Currency { get; }
    
    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }
}

// Domain/Interfaces/IOrderRepository.cs
public interface IOrderRepository
{
    Task<Order> GetByIdAsync(OrderId id);
    Task SaveAsync(Order order);
    Task DeleteAsync(OrderId id);
}

2. Application Layer

Contains use cases and application services. Depends only on the Domain layer.

// Application/UseCases/CreateOrder/CreateOrderCommand.cs
public record CreateOrderCommand(CustomerId CustomerId, List<OrderItemDto> Items);

// Application/UseCases/CreateOrder/CreateOrderHandler.cs
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, OrderId>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IProductRepository _productRepository;
    private readonly IUnitOfWork _unitOfWork;
    
    public async Task<OrderId> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var order = new Order(request.CustomerId);
        
        foreach (var item in request.Items)
        {
            var product = await _productRepository.GetByIdAsync(item.ProductId);
            var quantity = new Quantity(item.Quantity);
            var unitPrice = new Money(item.UnitPrice, "USD");
            
            order.AddItem(product.Id, quantity, unitPrice);
        }
        
        await _orderRepository.SaveAsync(order);
        await _unitOfWork.CommitAsync();
        
        return order.Id;
    }
}

// Application/Services/OrderApplicationService.cs
public class OrderApplicationService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IOrderPricingService _pricingService;
    
    public async Task<OrderDto> GetOrderDetailsAsync(OrderId orderId)
    {
        var order = await _orderRepository.GetByIdAsync(orderId);
        var total = _pricingService.CalculateTotal(order);
        
        return new OrderDto
        {
            Id = order.Id.Value,
            CustomerId = order.CustomerId.Value,
            Status = order.Status.ToString(),
            Total = total.Amount,
            Items = order.Items.Select(item => new OrderItemDto
            {
                ProductId = item.ProductId.Value,
                Quantity = item.Quantity.Value,
                UnitPrice = item.UnitPrice.Amount
            }).ToList()
        };
    }
}

3. Infrastructure Layer

Handles external concerns like data persistence, external APIs, and frameworks. Implements interfaces defined in the Domain layer.

// Infrastructure/Data/Repositories/OrderRepository.cs
public class OrderRepository : IOrderRepository
{
    private readonly ApplicationDbContext _context;
    
    public async Task<Order> GetByIdAsync(OrderId id)
    {
        var orderEntity = await _context.Orders
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == id.Value);
            
        if (orderEntity == null)
            return null;
            
        return MapToDomain(orderEntity);
    }
    
    public async Task SaveAsync(Order order)
    {
        var orderEntity = MapToEntity(order);
        
        if (_context.Entry(orderEntity).State == EntityState.Detached)
            _context.Orders.Add(orderEntity);
        else
            _context.Orders.Update(orderEntity);
            
        await _context.SaveChangesAsync();
    }
    
    private Order MapToDomain(OrderEntity entity)
    {
        // Mapping logic from entity to domain object
        return new Order(new OrderId(entity.Id))
        {
            CustomerId = new CustomerId(entity.CustomerId),
            Status = Enum.Parse<OrderStatus>(entity.Status)
        };
    }
}

// Infrastructure/Data/ApplicationDbContext.cs
public class ApplicationDbContext : DbContext
{
    public DbSet<OrderEntity> Orders { get; set; }
    public DbSet<OrderItemEntity> OrderItems { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
    }
}

// Infrastructure/ExternalServices/EmailService.cs
public class EmailService : IEmailService
{
    private readonly SmtpClient _smtpClient;
    
    public async Task SendOrderConfirmationAsync(CustomerId customerId, OrderId orderId)
    {
        // Implementation for sending email
        var message = new MailMessage();
        // ... email logic
        await _smtpClient.SendMailAsync(message);
    }
}

4. Presentation Layer

Handles user interface and external API concerns. Depends on the Application layer.

// Presentation/Controllers/OrdersController.cs
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IMediator _mediator;
    private readonly OrderApplicationService _orderService;
    
    [HttpPost]
    public async Task<ActionResult<OrderId>> CreateOrder([FromBody] CreateOrderRequest request)
    {
        var command = new CreateOrderCommand(
            new CustomerId(request.CustomerId),
            request.Items.Select(item => new OrderItemDto
            {
                ProductId = new ProductId(item.ProductId),
                Quantity = item.Quantity,
                UnitPrice = item.UnitPrice
            }).ToList()
        );
        
        var orderId = await _mediator.Send(command);
        return Ok(orderId);
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<OrderDto>> GetOrder(int id)
    {
        var order = await _orderService.GetOrderDetailsAsync(new OrderId(id));
        return Ok(order);
    }
}

// Presentation/Models/CreateOrderRequest.cs
public class CreateOrderRequest
{
    public int CustomerId { get; set; }
    public List<OrderItemRequest> Items { get; set; }
}

public class OrderItemRequest
{
    public int ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

Dependency Flow:

// ✅ Correct: Dependencies point inward
Presentation → Application → Domain
Infrastructure → Domain

// ❌ Wrong: Dependencies point outward
Domain → Application → Presentation
Domain → Infrastructure

Key Principles:

  1. Dependency Inversion: High-level modules don't depend on low-level modules
  2. Single Responsibility: Each layer has one reason to change
  3. Interface Segregation: Depend on abstractions, not concretions
  4. Open/Closed: Open for extension, closed for modification

Benefits:

  • Testability: Easy to unit test business logic
  • Maintainability: Clear separation of concerns
  • Flexibility: Easy to change external dependencies
  • Independence: Business logic independent of frameworks
  • Reusability: Domain logic can be reused across applications

Project Structure:

src/
├── Domain/
│   ├── Entities/
│   ├── ValueObjects/
│   ├── Interfaces/
│   └── Events/
├── Application/
│   ├── UseCases/
│   ├── Services/
│   └── DTOs/
├── Infrastructure/
│   ├── Data/
│   ├── ExternalServices/
│   └── Configuration/
└── Presentation/
    ├── Controllers/
    ├── Models/
    └── Middleware/

This architecture ensures that business logic remains pure and independent of external concerns, making the system more maintainable and testable.