Layered Architecture

Intro

Layered architecture structures an application into layers with clear responsibilities and strict dependency directions. Each layer only depends on the layer directly below it (traditional) or on inner layers (onion/clean). The goal is to isolate business rules from infrastructure details — so you can swap databases, frameworks, or delivery mechanisms without touching domain logic.

Layer Responsibilities

A typical four-layer structure:

Layer Responsibility Examples
Presentation Handle input, render output ASP.NET Core controllers, Razor views, Blazor components
Application Orchestrate use cases, coordinate domain + infrastructure Service classes, CQRS handlers, DTOs
Domain Business rules, entities, invariants Entities, value objects, domain events, domain services
Infrastructure Technical details: persistence, messaging, external APIs EF Core DbContext, HTTP clients, email senders

Dependency Rule

All dependencies point inward. The Domain knows nothing about databases, frameworks, or UI. Infrastructure implements interfaces defined by inner layers.

graph TD
    subgraph OUTER[Infrastructure and Presentation - outermost]
        UI[Controllers and Views]
        DB[EF Core and SQL Server]
        EXT[HTTP clients and Email and File system]
    end

    subgraph MIDDLE[Application Layer]
        UC[Use Cases and Services]
        IPORT[[IOrderRepository]]
        OPORT[[IEmailSender]]
    end

    subgraph CORE[Domain Layer - innermost and zero dependencies]
        ENT[Entities and Value Objects]
        RULES[Business Rules]
        DEVT[Domain Events]
    end

    UI --> UC
    UC --> ENT
    UC --> RULES

    DB -.->|implements| IPORT
    EXT -.->|implements| OPORT
    IPORT --> ENT
    OPORT --> UC

Traditional vs Onion/Clean

graph LR
    subgraph TRADITIONAL[Traditional Layered - dependencies go down]
        direction TB
        T_UI[UI] --> T_BL[Business Logic]
        T_BL --> T_DA[Data Access]
        T_DA --> T_DB[(Database)]
    end

    subgraph ONION[Onion and Clean - dependencies go inward]
        direction TB
        O_INFRA[Infrastructure] --> O_APP[Application]
        O_UI[Presentation] --> O_APP
        O_APP --> O_DOM[Domain]
    end

In traditional layered architecture, changing the database affects everything above it. In Onion/Clean Architecture, the dependency is inverted: Infrastructure depends on the Domain through interfaces, so you can swap databases without touching business rules.

.NET Example

// Domain layer — no dependencies on EF Core or ASP.NET
public class Order
{
    public int Id { get; private set; }
    public Money Total { get; private set; }

    public void AddItem(Product product, int quantity)
    {
        // Business rule: enforce invariants here
        if (quantity <= 0) throw new DomainException("Quantity must be positive");
        Total = Total.Add(product.Price.Multiply(quantity));
    }
}

// Application layer — depends on domain + abstractions
public class PlaceOrderHandler
{
    private readonly IOrderRepository _orders;
    private readonly IEmailSender _email;

    public async Task HandleAsync(PlaceOrderCommand cmd, CancellationToken ct)
    {
        var order = new Order();
        foreach (var item in cmd.Items)
            order.AddItem(item.Product, item.Quantity);

        await _orders.SaveAsync(order, ct);
        await _email.SendConfirmationAsync(cmd.CustomerEmail, order, ct);
    }
}

// Infrastructure layer — implements domain abstractions
public class EfOrderRepository : IOrderRepository
{
    private readonly AppDbContext _db;
    public async Task SaveAsync(Order order, CancellationToken ct)
        => await _db.Orders.AddAsync(order, ct);
}

Pitfalls

Anemic domain model
Business logic leaks into the Application layer (service classes do everything) while the Domain layer contains only data bags. The layers exist but the dependency rule is violated in spirit — the Domain has no behavior to protect.

Layer bypass
Controllers calling repositories directly, skipping the Application layer. Breaks the single-responsibility of each layer and makes the codebase harder to test.

Over-engineering small apps
Four layers with interfaces and DI for a 3-endpoint CRUD API adds ceremony without benefit. Apply layered architecture when the domain has real complexity worth protecting.

Questions

References


Whats next