View complete guide path

.NET Clean Architecture: A Complete, Practical, Architect-Level Guide

Scope and Assumptions

This guide focuses on Clean Architecture applied to modern .NET applications, especially:

The principles are more important than any particular framework or library. Clean Architecture should survive changes to ASP.NET Core, EF Core, databases, messaging systems, and cloud providers.


1. Executive Summary

What is .NET Clean Architecture?

Clean Architecture is a way of organizing software so that the most important business rules are independent from implementation details such as:

In a .NET solution, this commonly results in layers such as:

┌──────────────────────────────────────┐
│ Presentation                         │
│ ASP.NET Core / Controllers / UI      │
├──────────────────────────────────────┤
│ Infrastructure                       │
│ EF Core / External APIs / Email      │
├──────────────────────────────────────┤
│ Application                          │
│ Use Cases / Commands / Queries       │
├──────────────────────────────────────┤
│ Domain                               │
│ Business Rules / Entities / Value    │
│ Objects / Domain Events              │
└──────────────────────────────────────┘

However, the real architecture is defined by dependency direction, not folder names.

The fundamental rule is:

Dependencies should point inward, toward business rules.

A simplified dependency graph:

flowchart TB
    Presentation[Presentation]
    Infrastructure[Infrastructure]
    Application[Application]
    Domain[Domain]

    Presentation --> Application
    Presentation --> Infrastructure
    Infrastructure --> Application
    Infrastructure --> Domain
    Application --> Domain

The Domain project should ideally know nothing about:


Why was Clean Architecture created?

Traditional applications often evolve into tightly coupled systems.

For example:

Controller
    ↓
Service
    ↓
Entity Framework DbContext
    ↓
SQL Server

Initially this is simple.

Over time, business logic becomes scattered across:

The business rules become dependent on infrastructure.

Changing infrastructure can then require changing business logic.

Clean Architecture attempts to reverse this relationship.

Instead of:

Business depends on the database.

It aims for:

The database depends on the business/application abstractions.


What problem does it solve?

Clean Architecture primarily solves coupling between business logic and implementation details.

It helps with:

1. Testability

Business rules can often be tested without:

2. Replaceability

For example:

Application
    ↓
IOrderRepository
    ↓
┌─────────────────┬──────────────────┐
│ EF Core         │ Future alternative│
│ SQL Server      │ Dapper/PostgreSQL │
└─────────────────┴──────────────────┘

The application depends on an abstraction.

Infrastructure provides the implementation.

3. Maintainability

Responsibilities are separated.

Instead of one class doing everything:

public async Task<IActionResult> CreateOrder(...)
{
    // Validation
    // Business rules
    // Database access
    // Email sending
    // Logging
}

The responsibilities are distributed according to architectural boundaries.

4. Business-rule protection

The most important logic becomes less dependent on technology.

For example:

Order cannot be shipped before payment.

This rule should remain valid whether the application uses:


What problems does it not solve?

Clean Architecture is not a solution to all software problems.

It does not automatically solve:

A poorly designed Clean Architecture solution can be more complicated than a simple layered application.

For example:

Interface
    ↓
Service
    ↓
Handler
    ↓
Repository
    ↓
Generic Repository
    ↓
Unit of Work
    ↓
ORM

This can be worse than:

Use Case
    ↓
DbContext

Architecture should reduce accidental complexity, not create it.


Who uses it, where is it used, and when should I use it?

Clean Architecture is commonly useful in:

It is especially valuable when:

It may be unnecessary when:

A simpler architecture may be better.


Quick Gist

Clean Architecture organizes a system around business capabilities rather than frameworks. The Domain contains core business concepts, the Application layer contains use cases, Infrastructure implements technical concerns, and Presentation exposes the system to users or external clients. Dependencies point inward.


2. Core Concepts

2.1 Dependency Rule

Definition

The Dependency Rule says that source-code dependencies should point toward higher-level policies and business rules.

flowchart RL
    Infrastructure --> Application
    Presentation --> Application
    Application --> Domain
    Infrastructure --> Domain

The inner layers should not depend on outer layers.

Why it matters

Suppose the Domain depends directly on EF Core:

public class Order
{
    public DbSet<OrderItem> Items { get; set; }
}

Now the business model is coupled to EF Core.

This makes the framework part of the domain model.

Instead:

public class Order
{
    private readonly List<OrderItem> _items = [];

    public IReadOnlyCollection<OrderItem> Items => _items;

    public void AddItem(ProductId productId, int quantity)
    {
        if (quantity <= 0)
        {
            throw new DomainException(
                "Quantity must be greater than zero.");
        }

        _items.Add(new OrderItem(productId, quantity));
    }
}

The domain expresses business behavior without knowing persistence technology.


2.2 Domain Layer

Definition

The Domain layer contains the most fundamental business concepts and rules.

Typical contents:

Why it matters

The Domain should answer:

What does the business mean?

It should not answer:

How do we save this to SQL Server?

Example

public class Order
{
    public OrderId Id { get; private set; }

    public OrderStatus Status { get; private set; }

    public void Confirm()
    {
        if (Status != OrderStatus.Pending)
        {
            throw new DomainException(
                "Only pending orders can be confirmed.");
        }

        Status = OrderStatus.Confirmed;
    }
}

This is a business rule.

It belongs close to the domain concept it protects.


2.3 Entity

Definition

An Entity is an object defined primarily by its identity rather than its attributes.

Example:

Customer #123

Even if the customer’s name changes, the customer remains the same entity.

Example

public class Customer
{
    public CustomerId Id { get; private set; }

    public CustomerName Name { get; private set; }
}

Entity versus Value Object

Entity Value Object
Has identity Defined by its values
Can change over time Usually immutable
Equality based on identity Equality based on contents
Example: Order Example: Money

2.4 Value Object

Definition

A Value Object represents a meaningful concept defined by its value.

Examples:

Example

public sealed record Money(decimal Amount, string Currency)
{
    public static Money Create(decimal amount, string currency)
    {
        if (amount < 0)
        {
            throw new DomainException(
                "Amount cannot be negative.");
        }

        return new Money(amount, currency);
    }
}

Why it matters

Value objects prevent primitive obsession.

Instead of:

decimal amount;
string currency;

Use:

Money total;

This creates a stronger domain language.


2.5 Aggregate

Definition

An Aggregate is a consistency boundary containing one or more domain objects.

The main entity is the Aggregate Root.

Example:

Order Aggregate
│
├── Order
│
└── OrderItems

External code should generally interact with the aggregate through its root.

Example

public class Order
{
    private readonly List<OrderItem> _items = [];

    public IReadOnlyCollection<OrderItem> Items => _items;

    public void AddItem(ProductId productId, int quantity)
    {
        _items.Add(
            new OrderItem(productId, quantity));
    }
}

Instead of allowing arbitrary modification:

order.Items.Clear();

The aggregate root controls the invariant.

An invariant is a rule that must always remain true.


2.6 Application Layer

Definition

The Application layer coordinates use cases.

It answers:

What can the system do?

Examples:

Example

public sealed record CreateOrderCommand(
    Guid CustomerId,
    IReadOnlyCollection<CreateOrderItem> Items);

Handler:

public sealed class CreateOrderHandler
{
    private readonly IOrderRepository _orders;

    public CreateOrderHandler(
        IOrderRepository orders)
    {
        _orders = orders;
    }

    public async Task<Guid> Handle(
        CreateOrderCommand command,
        CancellationToken cancellationToken)
    {
        var order = Order.Create(
            CustomerId.From(command.CustomerId));

        foreach (var item in command.Items)
        {
            order.AddItem(
                ProductId.From(item.ProductId),
                item.Quantity);
        }

        await _orders.AddAsync(
            order,
            cancellationToken);

        return order.Id.Value;
    }
}

The handler coordinates the workflow.

The entity protects domain rules.


2.7 Use Cases

Definition

A Use Case represents an application capability.

Examples:

CreateOrder
CancelOrder
ShipOrder
GetOrderDetails

Why it matters

A common mistake is organizing Application code around technical concepts:

Services
Managers
Helpers
Utilities

A use-case-oriented structure is clearer:

Application
└── Orders
    ├── CreateOrder
    ├── CancelOrder
    └── GetOrderDetails

This maps architecture directly to business capabilities.


2.8 CQRS

Definition

CQRS, or Command Query Responsibility Segregation, separates operations into:

Example:

CreateOrderCommand
GetOrderDetailsQuery

Command

public sealed record CancelOrderCommand(
    Guid OrderId);

Query

public sealed record GetOrderDetailsQuery(
    Guid OrderId);

Why it matters

Commands and queries usually have different requirements.

Commands often need:

Queries often need:

However:

CQRS does not require separate databases.

A single database can support CQRS.


2.9 Dependency Inversion Principle

Definition

High-level policy should not depend directly on low-level implementation.

Both should depend on abstractions.

Example:

Application
    ↓
IPaymentGateway
    ↑
Infrastructure

Interface:

public interface IPaymentGateway
{
    Task<PaymentResult> ChargeAsync(
        PaymentRequest request,
        CancellationToken cancellationToken);
}

Implementation:

public sealed class StripePaymentGateway
    : IPaymentGateway
{
    public Task<PaymentResult> ChargeAsync(
        PaymentRequest request,
        CancellationToken cancellationToken)
    {
        // Stripe implementation

        throw new NotImplementedException();
    }
}

The Application layer does not depend on the payment provider.


2.10 Dependency Injection

Definition

Dependency Injection is a technique where dependencies are supplied from outside an object.

Instead of:

public class OrderService
{
    private readonly OrderRepository _repository =
        new OrderRepository();
}

Use:

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(
        IOrderRepository repository)
    {
        _repository = repository;
    }
}

ASP.NET Core provides the dependency at runtime.


2.11 Ports and Adapters

Clean Architecture is closely related to Hexagonal Architecture.

A Port is an abstraction representing an interaction.

An Adapter implements that interaction.

Example:

Application Port

IEmailSender
     │
     ├── SMTP Adapter
     │
     ├── SendGrid Adapter
     │
     └── Azure Communication Adapter

The Application defines what it needs.

Infrastructure decides how it is implemented.


2.12 Repository

Definition

A Repository abstracts persistence operations for domain objects.

Example:

public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(
        OrderId id,
        CancellationToken cancellationToken);

    Task AddAsync(
        Order order,
        CancellationToken cancellationToken);
}

Important distinction

A repository is not automatically required for every entity.

This is often overengineered:

IGenericRepository<T>

A generic repository may merely duplicate EF Core functionality.

Prefer repositories when they represent meaningful domain collection boundaries.


2.13 Unit of Work

Definition

A Unit of Work coordinates changes as a transaction.

EF Core’s DbContext already behaves similarly to a Unit of Work.

Therefore, adding this:

GenericRepository
+
GenericUnitOfWork
+
DbContext

may introduce unnecessary abstraction.

The question is not:

Should every Clean Architecture application have a Unit of Work?

The better question is:

Does this abstraction simplify our application’s transaction boundaries?


2.14 Domain Events

Definition

A Domain Event represents something meaningful that happened in the domain.

Examples:

OrderPlaced
PaymentReceived
OrderCancelled

Example:

public sealed record OrderPlacedDomainEvent(
    Guid OrderId,
    Guid CustomerId);

Domain events can trigger additional behavior.

For example:

Order Placed
     │
     ├── Send confirmation
     ├── Reserve inventory
     └── Notify analytics

Be careful with transaction boundaries.

A domain event is not automatically a distributed event.


2.15 Integration Events

An Integration Event communicates between independently deployable systems.

Example:

Order Service
     │
     │ OrderCreatedIntegrationEvent
     ▼
Inventory Service

Common transports:

Important distinction:

Domain Event Integration Event
Internal business event Cross-system communication
Usually internal External contract
Can be synchronous Often asynchronous
Domain-oriented Integration-oriented

Do not expose domain events directly as public integration contracts without deliberate design.


3. How It Works

Consider this use case:

A customer creates an order.

Runtime Flow

sequenceDiagram
    actor Client
    participant API as ASP.NET Core API
    participant Handler as Application Handler
    participant Domain as Domain Model
    participant Repo as Repository
    participant DB as Database
    participant Events as Event Publisher

    Client->>API: POST /orders
    API->>Handler: CreateOrderCommand
    Handler->>Domain: Order.Create()
    Domain-->>Handler: Order
    Handler->>Domain: AddItem()
    Domain-->>Handler: Updated Order
    Handler->>Repo: AddAsync(order)
    Repo->>DB: INSERT order
    DB-->>Repo: Success
    Repo-->>Handler: Success
    Handler->>Events: Publish events
    Events-->>Handler: Success
    Handler-->>API: OrderId
    API-->>Client: 201 Created

Step 1: The client calls the API

Example:

POST /orders

Payload:

{
  "customerId": "2b3d...",
  "items": [
    {
      "productId": "8e1f...",
      "quantity": 2
    }
  ]
}

The API is responsible for transport concerns.

Examples:

The API should not become the primary location for business rules.


Step 2: Presentation converts input into an application request

app.MapPost("/orders", async (
    CreateOrderRequest request,
    ISender sender,
    CancellationToken cancellationToken) =>
{
    var command = new CreateOrderCommand(
        request.CustomerId,
        request.Items.Select(x =>
            new CreateOrderItem(
                x.ProductId,
                x.Quantity))
            .ToList());

    var result = await sender.Send(
        command,
        cancellationToken);

    return Results.Created(
        $"/orders/{result.OrderId}",
        result);
});

The HTTP model and Application model may be different.

This is intentional.


Step 3: The Application layer executes the use case

The handler coordinates:

Validate
   ↓
Load required state
   ↓
Execute domain behavior
   ↓
Persist
   ↓
Trigger side effects

The Application layer should generally avoid containing rules that naturally belong inside domain objects.


Step 4: Domain objects enforce business rules

Example:

order.AddItem(productId, quantity);

The domain object validates invariants.

Example:

public void AddItem(
    ProductId productId,
    int quantity)
{
    if (Status != OrderStatus.Draft)
    {
        throw new DomainException(
            "Items cannot be changed after confirmation.");
    }

    if (quantity <= 0)
    {
        throw new DomainException(
            "Quantity must be greater than zero.");
    }

    _items.Add(
        new OrderItem(productId, quantity));
}

Step 5: Infrastructure persists the result

Infrastructure implements the abstraction.

public sealed class OrderRepository
    : IOrderRepository
{
    private readonly ApplicationDbContext _dbContext;

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

    public async Task AddAsync(
        Order order,
        CancellationToken cancellationToken)
    {
        await _dbContext.Orders.AddAsync(
            order,
            cancellationToken);

        await _dbContext.SaveChangesAsync(
            cancellationToken);
    }
}

The Application layer does not know:


4. Implementation

A practical starting point:

MyApp.sln

src/
├── MyApp.Domain/
│   ├── Entities/
│   ├── ValueObjects/
│   ├── Events/
│   ├── Exceptions/
│   └── Services/
│
├── MyApp.Application/
│   ├── Common/
│   │   ├── Behaviors/
│   │   ├── Interfaces/
│   │   └── Models/
│   │
│   └── Features/
│       ├── Orders/
│       │   ├── CreateOrder/
│       │   ├── CancelOrder/
│       │   └── GetOrderDetails/
│       │
│       └── Customers/
│
├── MyApp.Infrastructure/
│   ├── Persistence/
│   │   ├── Configurations/
│   │   ├── Repositories/
│   │   └── Migrations/
│   │
│   ├── Messaging/
│   ├── Identity/
│   └── ExternalServices/
│
└── MyApp.WebApi/
    ├── Endpoints/
    ├── Middleware/
    └── Program.cs

tests/
├── MyApp.Domain.Tests/
├── MyApp.Application.Tests/
├── MyApp.Infrastructure.Tests/
└── MyApp.WebApi.IntegrationTests/

Project References

A healthy dependency structure:

flowchart LR
    WebApi --> Application
    WebApi --> Infrastructure
    Infrastructure --> Application
    Infrastructure --> Domain
    Application --> Domain

The Domain should not reference:

Application
Infrastructure
WebApi

The Application should not reference:

Infrastructure
WebApi

Step 1: Domain Entity

public sealed class Order
{
    private readonly List<OrderItem> _items = [];

    private Order()
    {
    }

    public OrderId Id { get; private set; }

    public CustomerId CustomerId { get; private set; }

    public OrderStatus Status { get; private set; }

    public IReadOnlyCollection<OrderItem> Items => _items;

    public static Order Create(CustomerId customerId)
    {
        return new Order
        {
            Id = OrderId.New(),
            CustomerId = customerId,
            Status = OrderStatus.Draft
        };
    }

    public void AddItem(
        ProductId productId,
        int quantity)
    {
        if (Status != OrderStatus.Draft)
        {
            throw new DomainException(
                "Order cannot be modified.");
        }

        if (quantity <= 0)
        {
            throw new DomainException(
                "Quantity must be positive.");
        }

        _items.Add(
            new OrderItem(productId, quantity));
    }

    public void Confirm()
    {
        if (_items.Count == 0)
        {
            throw new DomainException(
                "Cannot confirm an empty order.");
        }

        Status = OrderStatus.Confirmed;
    }
}

Design reasoning

The entity controls state changes.

Avoid:

order.Status = OrderStatus.Confirmed;

Prefer:

order.Confirm();

This protects invariants.


Step 2: Application Command

public sealed record CreateOrderCommand(
    Guid CustomerId,
    IReadOnlyCollection<CreateOrderItem> Items)
    : IRequest<CreateOrderResult>;

Result:

public sealed record CreateOrderResult(
    Guid OrderId);

Step 3: Application Handler

public sealed class CreateOrderHandler
    : IRequestHandler<
        CreateOrderCommand,
        CreateOrderResult>
{
    private readonly IOrderRepository _orders;

    public CreateOrderHandler(
        IOrderRepository orders)
    {
        _orders = orders;
    }

    public async Task<CreateOrderResult> Handle(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        var order = Order.Create(
            CustomerId.From(request.CustomerId));

        foreach (var item in request.Items)
        {
            order.AddItem(
                ProductId.From(item.ProductId),
                item.Quantity);
        }

        order.Confirm();

        await _orders.AddAsync(
            order,
            cancellationToken);

        return new CreateOrderResult(
            order.Id.Value);
    }
}

The handler orchestrates.

The entity owns business behavior.


Step 4: Infrastructure Repository

public sealed class OrderRepository
    : IOrderRepository
{
    private readonly ApplicationDbContext _db;

    public OrderRepository(
        ApplicationDbContext db)
    {
        _db = db;
    }

    public async Task AddAsync(
        Order order,
        CancellationToken cancellationToken)
    {
        await _db.Orders.AddAsync(
            order,
            cancellationToken);

        await _db.SaveChangesAsync(
            cancellationToken);
    }

    public Task<Order?> GetByIdAsync(
        OrderId id,
        CancellationToken cancellationToken)
    {
        return _db.Orders
            .Include(x => x.Items)
            .SingleOrDefaultAsync(
                x => x.Id == id,
                cancellationToken);
    }
}

Step 5: EF Core Configuration

Prefer keeping persistence configuration outside domain entities.

public sealed class OrderConfiguration
    : IEntityTypeConfiguration<Order>
{
    public void Configure(
        EntityTypeBuilder<Order> builder)
    {
        builder.ToTable("Orders");

        builder.HasKey(x => x.Id);

        builder.Property(x => x.Status)
            .HasConversion<string>();

        builder.OwnsMany(
            x => x.Items,
            item =>
            {
                item.ToTable("OrderItems");
            });
    }
}

This prevents persistence concerns from leaking into the Domain.


Step 6: Dependency Registration

public static class DependencyInjection
{
    public static IServiceCollection
        AddInfrastructure(
            this IServiceCollection services,
            IConfiguration configuration)
    {
        services.AddDbContext<ApplicationDbContext>(
            options =>
                options.UseSqlServer(
                    configuration.GetConnectionString(
                        "DefaultConnection")));

        services.AddScoped<
            IOrderRepository,
            OrderRepository>();

        return services;
    }
}

Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddApplication();

builder.Services.AddInfrastructure(
    builder.Configuration);

builder.Services.AddAuthentication();

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();

app.UseAuthorization();

app.MapEndpoints();

app.Run();

The Web API becomes the composition root.

A composition root is the location where implementations are connected to abstractions.


Validation Strategy

Validation should exist at different levels.

Transport validation

Examples:

Application validation

Examples:

Quantity must be provided.
Request cannot contain more than 100 items.

Domain validation

Examples:

Order cannot be confirmed without items.
Quantity cannot be negative.

Do not assume one validation layer replaces another.


Testing Strategy

Domain Tests

Fast and isolated.

[Fact]
public void Confirm_ShouldThrow_WhenOrderHasNoItems()
{
    var order = Order.Create(
        CustomerId.New());

    var action = () => order.Confirm();

    action.Should()
        .Throw<DomainException>();
}

Application Tests

Mock boundaries.

var repository =
    Substitute.For<IOrderRepository>();

Test use-case orchestration.

Infrastructure Tests

Test:

Integration Tests

Test the actual system:

HTTP
  ↓
Authentication
  ↓
Endpoint
  ↓
Application
  ↓
Database

Use integration tests for critical workflows.


5. Architecture and Design

How a Solution Architect Evaluates Clean Architecture

A Solution Architect should not start with:

How many projects should the solution contain?

Start with:

What are the business boundaries?

For example:

E-Commerce Platform
│
├── Ordering
├── Catalog
├── Payments
├── Inventory
└── Shipping

These are business capabilities.

They may eventually become:

But they should not automatically become microservices.


Vertical Slice versus Horizontal Organization

Horizontal organization

Controllers/
Services/
Repositories/
Models/

A feature requires modifying many folders.

Vertical slice organization

Features/
└── Orders/
    └── CreateOrder/
        ├── Command.cs
        ├── Handler.cs
        ├── Validator.cs
        └── Response.cs

Vertical slices often work very well inside the Application layer.


Clean Architecture and DDD

Domain-Driven Design (DDD) and Clean Architecture solve different problems.

Clean Architecture DDD
Dependency boundaries Domain modeling
Framework independence Business language
Architectural organization Business complexity management

They work well together.

But Clean Architecture does not require full DDD.

A simple CRUD application should not necessarily have:

Aggregates
Domain Events
Domain Services
Specifications
Factories
Repositories
Value Objects

for every table.


Clean Architecture and Modular Monoliths

A Modular Monolith is a single deployable application with strong internal module boundaries.

Example:

Commerce Application
│
├── Ordering Module
├── Catalog Module
├── Inventory Module
└── Payments Module

Each module can internally follow Clean Architecture.

flowchart TB
    API --> OrderingApp[Ordering Application]
    API --> CatalogApp[Catalog Application]

    OrderingApp --> OrderingDomain[Ordering Domain]
    CatalogApp --> CatalogDomain[Catalog Domain]

    OrderingInfra[Ordering Infrastructure] --> OrderingApp
    CatalogInfra[Catalog Infrastructure] --> CatalogApp

This is often a better starting point than microservices.


Clean Architecture versus Traditional Layered Architecture

Traditional:

Presentation
    ↓
Business Logic
    ↓
Data Access

Clean Architecture focuses more strongly on dependency direction.

The Data Access layer should not be architecturally “below” the business layer if the business layer depends directly on its implementation.

Instead:

Business
    ↓ abstraction
Data Access
    ↑ implementation

Alternatives and Trade-Offs

Option 1: Simple ASP.NET Core CRUD

Endpoint
   ↓
DbContext

Best for:

Advantages:

Disadvantages:


Option 2: Clean Architecture

Best for:

Advantages:

Disadvantages:


Option 3: Full DDD + Clean Architecture

Best for:

Advantages:

Disadvantages:


6. Production Readiness

Security

Clean Architecture does not automatically make an application secure.

Security should include:


Authentication

Authentication answers:

Who are you?

Examples:

Authentication should generally remain near the application boundary.

The Domain should not depend on JWT claims.

Avoid:

public class Order
{
    public void Confirm(JwtSecurityToken token)
    {
    }
}

The Domain should receive business-relevant information.


Authorization

Authorization answers:

Are you allowed to do this?

Application-level authorization can use abstractions such as:

public interface ICurrentUser
{
    Guid? UserId { get; }

    bool IsInRole(string role);
}

For complex authorization, prefer business-oriented policies.

Instead of:

Role == Manager

Sometimes the real rule is:

CanApproveOrder

This decouples business capability from organizational role names.


Data Protection

Protect:

Consider:

Do not log:

Passwords
Access tokens
Payment details
Sensitive personal data

Scalability

Clean Architecture helps isolate scaling decisions but does not automatically provide scalability.

Potential strategies:

Horizontal scaling

Load Balancer
    │
    ├── API Instance 1
    ├── API Instance 2
    └── API Instance 3

Caching

Use caching for:

Do not introduce caching before understanding consistency requirements.


Performance

Common problems:

N+1 database queries
Large object graphs
Unnecessary abstraction
Excessive allocations
Serialization overhead

For read-heavy queries, it may be appropriate for Application query handlers to use optimized projections.

Example:

var orders = await _db.Orders
    .AsNoTracking()
    .Where(x => x.CustomerId == customerId)
    .Select(x => new OrderSummaryDto(
        x.Id,
        x.Status))
    .ToListAsync(cancellationToken);

A query does not always need to reconstruct a complete aggregate.


Reliability

Important techniques:

Avoid blind retries.

Retrying a non-idempotent payment request can create duplicate charges.


Idempotency

An operation is idempotent if repeating it produces the same effective result.

For example:

POST /payments
Idempotency-Key: abc123

If the client retries because of a timeout:

First request
    ↓
Payment processed
    ↓
Response lost

Client retries

The server must avoid charging twice.

Store and validate the idempotency key.


Observability

A production system should provide:

Logs

Structured logging:

logger.LogInformation(
    "Order {OrderId} created for customer {CustomerId}",
    orderId,
    customerId);

Metrics

Examples:

Tracing

Distributed tracing follows a request across:

API
 ↓
Application
 ↓
Database
 ↓
Message Broker
 ↓
Worker

Failure Recovery

Critical workflows should consider:

What happens if the database succeeds but publishing fails?

Example:

Database transaction
      │
      ├── Order saved
      │
      └── Event publish fails

A common solution is the Transactional Outbox Pattern.

sequenceDiagram
    participant App
    participant DB
    participant Worker
    participant Broker

    App->>DB: Save Order
    App->>DB: Save Outbox Event
    DB-->>App: Commit

    Worker->>DB: Read Outbox Event
    Worker->>Broker: Publish Event
    Broker-->>Worker: Success
    Worker->>DB: Mark Processed

The business transaction and event record are persisted together.

Publishing happens separately.


7. Real-World Usage

Use Case 1: E-Commerce

Business rules:

Orders
Payments
Inventory
Discounts
Shipping

Clean Architecture is useful because business rules are likely to evolve independently from:


Use Case 2: Banking or Financial Operations

Example rules:

Transfers
Account limits
Approval workflows
Transaction rules
Audit requirements

A strong Domain layer helps protect critical invariants.

Example:

A transfer cannot exceed the approved daily limit.

Use Case 3: Enterprise Workflow System

Examples:

Purchase Requests
Approvals
Escalations
Notifications

The Application layer coordinates workflows.

The Domain layer represents business rules.

Infrastructure handles:


When Clean Architecture Is a Good Fit

Use it when:


When Another Approach Is Better

A simple CRUD application may benefit from:

Minimal API
   ↓
DbContext

Example:

app.MapGet(
    "/products",
    async (ApplicationDbContext db) =>
        await db.Products
            .AsNoTracking()
            .ToListAsync());

Adding five architectural layers to this may not create value.


8. Common Mistakes

Mistake 1: Treating folders as architecture

Warning sign:

Domain/
Application/
Infrastructure/

but:

Domain → Infrastructure

The project names are correct, but the dependency rule is violated.


Mistake 2: Creating interfaces for everything

Bad:

IOrderService
OrderService

when there is only one implementation and no architectural reason for the abstraction.

Interfaces should represent meaningful boundaries.


Mistake 3: Generic Repository Overuse

Example:

IGenericRepository<T>

Problems:

Use repositories intentionally.


Mistake 4: Anemic Domain Models

An anemic domain model stores data but contains almost no behavior.

public class Order
{
    public OrderStatus Status { get; set; }
}

Then business logic becomes scattered:

OrderService
OrderManager
OrderHelper
OrderProcessor

When domain complexity justifies it, behavior should live with the domain concepts.


Mistake 5: Putting All Logic in the Domain

Not every rule belongs in an entity.

Example:

Send an email after order confirmation.

This is usually an application or infrastructure concern.

The Domain should not become a dumping ground.


Mistake 6: Treating CQRS as a Requirement

You can implement Clean Architecture without:

CQRS is a tool, not a requirement.


Mistake 7: Overusing Domain Events

Do not create events for every property change.

Bad:

CustomerNameChanged
CustomerEmailChanged
CustomerPhoneChanged

unless these events have meaningful business significance.


Mistake 8: Infrastructure Leakage

Example:

public class Order
{
    public virtual ICollection<OrderItem> Items { get; set; }
}

This may introduce ORM concerns into the Domain.

Leakage is sometimes acceptable pragmatically, but it should be a conscious trade-off.


Mistake 9: Too Many Projects

Avoid architecture like:

MyApp.Core
MyApp.Core.Interfaces
MyApp.Core.Models
MyApp.Common
MyApp.Shared
MyApp.Shared.Common
MyApp.Utilities

Project boundaries should represent meaningful architectural boundaries.


9. End-to-End Project

Project: Order Management System

Requirements

The system must:

  1. Create orders.
  2. Add order items.
  3. Confirm orders.
  4. Cancel orders.
  5. Retrieve order details.
  6. Publish an event when an order is confirmed.

Architecture

flowchart TB
    Client[Web / Mobile Client]

    API[ASP.NET Core API]

    App[Application Layer]

    Domain[Domain Layer]

    Infra[Infrastructure Layer]

    DB[(SQL Database)]

    Broker[Message Broker]

    Client --> API
    API --> App
    App --> Domain

    Infra --> App
    Infra --> Domain

    API --> Infra

    Infra --> DB
    Infra --> Broker

Domain Model

Order
│
├── OrderId
├── CustomerId
├── Status
│
└── OrderItems
     ├── ProductId
     └── Quantity

Create Order Flow

HTTP Request
    ↓
CreateOrderCommand
    ↓
Validation
    ↓
Create Aggregate
    ↓
Apply Domain Rules
    ↓
Persist
    ↓
Return Order ID

Query Flow

For reads:

HTTP GET
    ↓
GetOrderDetailsQuery
    ↓
Optimized SQL Projection
    ↓
DTO
    ↓
JSON Response

Do not force a query to load a full aggregate if a DTO projection is sufficient.


Key Tests

Domain

✓ Cannot confirm empty order
✓ Cannot modify confirmed order
✓ Quantity must be positive

Application

✓ CreateOrder persists aggregate
✓ CancelOrder returns not found appropriately
✓ Command validation prevents invalid request

Integration

✓ POST /orders creates order
✓ GET /orders/{id} returns details
✓ Authentication is enforced

Evolution Path

Stage 1

Single database
Single API
Modular code

Stage 2

Add:

Background processing
Caching
Transactional outbox

Stage 3

If business and operational boundaries justify it:

Ordering
Inventory
Payments

may communicate asynchronously.

Do not split merely because the original architecture diagram looked like microservices.


10. Final Review

Quick Gist

The essential model is:

Domain
    Business meaning and rules

Application
    Use cases and orchestration

Infrastructure
    Technology implementations

Presentation
    HTTP/UI and external interaction

The critical dependency direction is:

Outer layers → Inner layers

The Domain should be protected from unnecessary knowledge of:

Use abstractions at meaningful boundaries.

Do not abstract everything.


Practical Example

A compact order-confirmation example:

public sealed class Order
{
    private readonly List<OrderItem> _items = [];

    public OrderStatus Status { get; private set; }

    public void AddItem(ProductId productId, int quantity)
    {
        if (Status != OrderStatus.Draft)
        {
            throw new DomainException(
                "Order cannot be modified.");
        }

        if (quantity <= 0)
        {
            throw new DomainException(
                "Quantity must be positive.");
        }

        _items.Add(
            new OrderItem(productId, quantity));
    }

    public void Confirm()
    {
        if (_items.Count == 0)
        {
            throw new DomainException(
                "Cannot confirm an empty order.");
        }

        Status = OrderStatus.Confirmed;
    }
}

Application orchestration:

public async Task<Guid> Handle(
    ConfirmOrderCommand command,
    CancellationToken cancellationToken)
{
    var order = await _repository.GetByIdAsync(
        OrderId.From(command.OrderId),
        cancellationToken);

    if (order is null)
    {
        throw new NotFoundException();
    }

    order.Confirm();

    await _repository.SaveAsync(
        order,
        cancellationToken);

    return order.Id.Value;
}

The use case coordinates.

The entity protects the business rule.

Infrastructure persists the result.


Best Practices

Architecture

Domain

Infrastructure

Testing

Production


Expert-Level Interview Questions & Answers

1. Why is dependency direction more important than the number of layers?

Answer:

Layers are organizational constructs. Dependency direction determines coupling.

A system can have:

Domain
Application
Infrastructure

and still violate Clean Architecture if:

Domain → EF Core
Application → Infrastructure

The important question is:

Can the business and application logic exist without knowing the implementation details?

If yes, the architecture is closer to Clean Architecture.


2. Should EF Core repositories always be hidden behind interfaces?

Answer:

No.

EF Core’s DbContext already provides many persistence abstractions.

Adding:

Repository
+
Generic Repository
+
Unit of Work

may only wrap existing functionality.

Repository abstractions are useful when:

For simple queries, direct optimized data access in the Application query side can be a better trade-off.


3. Where should validation live?

Answer:

Validation has multiple layers.

Validation Example
Transport Invalid JSON
Application Required command fields
Domain Cannot confirm empty order
Database Unique constraint

Do not expect one layer to replace all others.

The Domain should protect critical invariants because callers cannot always be trusted.


4. How do you prevent a Clean Architecture application from becoming overengineered?

Answer:

Apply abstractions when they protect a meaningful boundary.

Avoid creating:

Interfaces for every class
Repositories for every entity
Factories for every object
Domain events for every state change

Architecture should match complexity.

Start with the simplest design that preserves important boundaries.


5. Should every system use CQRS?

Answer:

No.

CQRS is useful when read and write concerns have meaningfully different models.

For simple applications, a conventional application service can be easier to understand.

Use CQRS when it simplifies complexity, not because it is fashionable.


6. How would you handle database success but message publishing failure?

Answer:

Use a transactional outbox when reliability requirements justify it.

Within one database transaction:

Save business change
+
Save event record

A background process later publishes the event.

This avoids relying on a distributed transaction between the database and message broker.

Consumers should also be idempotent because messages may be delivered more than once.


7. How would you evolve a Clean Architecture modular monolith into microservices?

Answer:

First establish strong module boundaries.

Avoid direct access to another module’s internal data.

Define explicit integration contracts.

Then evaluate:

Only extract services when there is a meaningful organizational or technical boundary.


8. Where should authorization rules live?

Answer:

It depends on the rule.

Technical authorization:

Is authenticated?
Has required claim?

usually belongs near application boundaries.

Business authorization:

Can this employee approve an order of this value?

may belong in the Application or Domain model depending on how deeply it participates in business rules.


9. What is the difference between Clean Architecture and Onion Architecture?

Answer:

They are closely related.

Both emphasize:

The terminology and diagrams differ more than the fundamental principle.

The most important practical rule remains:

Implementation details should depend on business policy, not the reverse.


Further Study

After mastering the basics, study these topics in roughly this order:

.NET Architecture

  1. ASP.NET Core dependency injection lifetimes
  2. EF Core tracking and change tracking
  3. EF Core transaction behavior
  4. Minimal APIs and endpoint organization
  5. Modular monolith architecture

Application Architecture

  1. CQRS
  2. Vertical Slice Architecture
  3. Pipeline behaviors
  4. Result and error handling patterns
  5. Application-level authorization

Domain Design

  1. Domain-Driven Design
  2. Aggregates and consistency boundaries
  3. Value objects
  4. Domain events
  5. Bounded contexts

Distributed Systems

  1. Transactional Outbox Pattern
  2. Idempotency
  3. Event-driven architecture
  4. Saga patterns
  5. Eventual consistency

Production Engineering

  1. Structured logging
  2. Distributed tracing
  3. Metrics and service-level objectives
  4. Resilience patterns
  5. Containerized deployment and cloud architecture

The ultimate goal is not to produce a solution with the maximum number of projects, interfaces, patterns, or layers.

The goal is to build a system where:

Business complexity is explicit, dependencies are controlled, infrastructure remains replaceable where valuable, and the architecture makes future change safer rather than more expensive.