Previous Clean Architecture Hexagonal-Architecture Next

Onion Architecture

πŸ§… Onion Architecture

Onion Architecture is a software design pattern that emphasizes the principle of Separation of Concerns by organizing an application into a series of concentric layers. Introduced by Jeffrey Palermo, its core principle, similar to Clean Architecture, is the Dependency Inversion Principle: dependencies flow inwards towards the core business logic.

πŸ’‘ This design ensures that the most critical part of your applicationβ€”the domain model and business rulesβ€”is independent of external concerns like databases, frameworks, or user interfaces.

🧩 Layers of Onion Architecture

The typical Onion Architecture, when applied in .NET Core, is divided into the following layers:

  • πŸ›οΈ Domain Layer: The core of the application. It contains the business entities and aggregates that represent the business concepts. This layer should have zero dependencies on any other layer.
  • βš™οΈ Application Layer: Contains the interfaces for the application's use cases and repositories. It orchestrates the flow of data to and from the domain layer. This layer depends on the Domain layer.
  • 🧱 Infrastructure Layer: The outermost layer. It contains the concrete implementations of the interfaces defined in the Application layer, including data access (Entity Framework Core), external APIs, and logging. This layer depends on the Application layer.
  • πŸ–₯️ Presentation Layer (UI/API): Also in the outermost layer, this is the application's entry point, such as an ASP.NET Core Web API or MVC project. It interacts with the Application layer to execute use cases and display results. It depends on both the Application and Infrastructure layers to resolve dependencies.

πŸ“ Layers in Onion Architecture

  • Core (Domain): Contains business entities and interfaces
  • Application Services: Contains use cases and business logic
  • Infrastructure: Contains implementations like database access, file systems
  • UI Layer: Web, desktop, or mobile interfaces

πŸ§ͺ Example: E-Commerce App

  • Domain: Product, Order, Customer entities
  • Application: PlaceOrderService, CalculateDiscountService
  • Infrastructure: SQL Server repository, Email service
  • UI: ASP.NET MVC or Blazor frontend

πŸ§ͺ Example in .NET Core: a Product Order Service

1️⃣ Project Structure

The solution is typically broken down into multiple projects to enforce the dependency rules.

  • ProductManagement.Domain (Class Library): Contains the Entities folder with the Product entity.
  • ProductManagement.Application (Class Library): Contains the Interfaces folder with the IProductRepository interface and the UseCases folder with CreateProductUseCase.
  • ProductManagement.Infrastructure (Class Library): Contains the Persistence folder with ApplicationDbContext and ProductRepository implementation.
  • ProductManagement.Api (Web API): Contains the Controllers folder with ProductsController.

2️⃣ Domain Layer: Product.cs

This class holds the core business logic and entities, independent of any framework or database.

// ProductManagement.Domain/Entities/Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; private set; }
    public decimal Price { get; private set; }

    // Domain validation and business rules are encapsulated here
    public Product(string name, decimal price)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Product name cannot be empty.", nameof(name));
        if (price <= 0)
            throw new ArgumentException("Price must be greater than zero.", nameof(price));

        Name = name;
        Price = price;
    }
}
    

3️⃣ Application Layer: IProductRepository.cs

This defines the contract for data access, which is part of the application's core logic but doesn't care about the implementation.

// ProductManagement.Application/Interfaces/IProductRepository.cs
public interface IProductRepository
{
    Task AddProductAsync(Product product);
    Task<Product> GetProductByIdAsync(int id);
}
    

4️⃣ Infrastructure Layer: ProductRepository.cs

This project, which has references to both ProductManagement.Application and ProductManagement.Domain, provides the database-specific implementation of the repository.

// ProductManagement.Infrastructure/Persistence/ProductRepository.cs
public class ProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _dbContext;

    public ProductRepository(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task AddProductAsync(Product product)
    {
        _dbContext.Products.Add(product);
        await _dbContext.SaveChangesAsync();
    }

    public async Task<Product> GetProductByIdAsync(int id)
    {
        return await _dbContext.Products.FindAsync(id);
    }
}
    

5️⃣ Presentation Layer: ProductsController.cs

The Web API controller depends on the Application layer to access the use cases, and dependency injection resolves the Infrastructure layer's implementation at runtime.

// ProductManagement.Api/Controllers/ProductsController.cs
[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository _productRepository;

    public ProductsController(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    [HttpPost]
    public async Task<IActionResult> CreateProduct([FromBody] CreateProductDto dto)
    {
        var product = new Product(dto.Name, dto.Price);
        await _productRepository.AddProductAsync(product);
        return Ok(product);
    }
}
    

βœ… Advantages

  • 🧩 Separation of concerns: Each layer has a specific, well-defined role, preventing code from becoming tightly coupled.
  • πŸ§ͺ Improved testability: The inner layers, being free of external dependencies, are easily unit-tested.
  • πŸ”— Decoupling: Enables replacing external dependencies (e.g., database, logging framework) with minimal impact on the core business logic.
  • πŸ’Ό Domain-centric: Keeps the core business rules at the heart of the application, ensuring a clearer and more robust design.
  • Framework and technology independence
  • Improved maintainability and scalability

⚠️ Disadvantages

  • πŸ“ˆ Increased complexity: Can be overkill for small, simple applications.
  • πŸŽ“ Steeper learning curve: Requires understanding architectural patterns and dependency inversion.
  • 🧾 Boilerplate code: More initial setup and abstraction may slow down development.
  • 🧠 Architectural discipline required: Strict adherence to dependency rules is necessary.
  • Slower initial development

πŸ• When to Use

  • 🏒 Complex or long-lived applications
  • πŸ“š Domain-driven design (DDD)
  • 🧩 Microservices development
  • πŸ”„ Anticipating technology changes
  • Systems with multiple interfaces (web, mobile)
  • Applications requiring high test coverage

🚫 When Not to Use

  • βš™οΈ Small, straightforward applications
  • ⏰ Strict time constraints
  • πŸ‘Ά Inexperienced teams
  • Rapid MVPs or prototypes
  • Teams unfamiliar with layered architecture

🧭 Precautions

  • Don’t over-engineer for simple apps
  • 🚫 Beware of "leaky" abstractions.
  • πŸ”’ Enforce dependency rules.
  • πŸ” Manage data mapping carefully.
  • 🧩 Handle cross-cutting concerns properly.
  • Keep domain layer free from external dependencies

πŸ—οΈ Best Practices

  • πŸ’‰ Use dependency injection (DI).
  • 🧠 Use MediatR for CQRS.
  • 🧼 Keep domain entities pure.
  • πŸ“¦ Define clear project responsibilities.
  • πŸš€ Start with a pre-built Onion Architecture template.
  • Inject dependencies from outer layers
  • Write unit tests for domain and application layers
  • Keep UI and infrastructure thin

πŸ’‘ Tips

  • Start with the domain model
  • Use a layered folder structure
  • Document layer responsibilities
  • Use code templates to reduce boilerplate
Back to Index
Previous Clean Architecture Hexagonal-Architecture Next
*