Back to Blog
.NET CoreArchitectureC#DDD

Clean Architecture in .NET 8: Layers, Dependencies & Real-World Patterns

18 May 20259 min read·Venkatraman Nagarajan

Why Clean Architecture?

After working on monolithic codebases that became impossible to test or extend, I adopted Clean Architecture as the standard starting point for every .NET project. The core idea is simple: dependencies always point inward. Business logic knows nothing about databases, HTTP, or UI frameworks.

Project Structure

src/
├── Domain/           # Entities, value objects, domain events
├── Application/      # Use cases, interfaces, DTOs
├── Infrastructure/   # EF Core, email, external APIs
└── WebApi/           # Controllers, middleware, DI setup

The Domain layer has zero dependencies. The Application layer defines interfaces — IOrderRepository, IEmailService — but never implements them. Infrastructure provides concrete implementations. WebApi wires everything together via dependency injection.

Dependency Inversion in Practice

// Application layer — defines the contract
public interface IWarehouseRepository
{
    Task<WarehouseItem> GetByIdAsync(Guid id, CancellationToken ct);
    Task SaveAsync(WarehouseItem item, CancellationToken ct);
}

// Infrastructure layer — implements it
public class WarehouseRepository : IWarehouseRepository
{
    private readonly AppDbContext _db;
    public WarehouseRepository(AppDbContext db) => _db = db;

    public async Task<WarehouseItem> GetByIdAsync(Guid id, CancellationToken ct)
        => await _db.WarehouseItems.FindAsync(new object[] { id }, ct)
           ?? throw new NotFoundException(id);
}

Use Case Pattern (CQRS-lite)

Rather than fat controllers, every operation becomes a command or query handled by a dedicated class. I use MediatR for this:

// Command
public record ReceiveStockCommand(Guid ItemId, int Quantity) : IRequest<Unit>;

// Handler lives in Application layer
public class ReceiveStockHandler : IRequestHandler<ReceiveStockCommand, Unit>
{
    private readonly IWarehouseRepository _repo;
    public ReceiveStockHandler(IWarehouseRepository repo) => _repo = repo;

    public async Task<Unit> Handle(ReceiveStockCommand cmd, CancellationToken ct)
    {
        var item = await _repo.GetByIdAsync(cmd.ItemId, ct);
        item.ReceiveStock(cmd.Quantity); // domain method
        await _repo.SaveAsync(item, ct);
        return Unit.Value;
    }
}

Key Takeaways

  • Your domain and application layers should compile and unit-test with no database installed.
  • Controllers are thin — they translate HTTP into commands and return results.
  • Never let EF Core entities leak into your domain — use mapping.
  • Domain events are the clean way to trigger side effects without coupling layers.

I've applied this pattern across WMS platforms at UPS Healthcare and financial APIs at LTIMindtree. The payoff is real: adding a new storage backend or changing email providers becomes a one-file change.

Found this useful? Share it:

© 2026 VENKATRAMAN NAGARAJAN. All rights reserved.

Senior Full Stack Engineer · Chennai, India