The Application Layer in Clean Architecture

The Application Layer in Clean Architecture

6 min read··

clean-architecturecqrsdotnet

The Application layer is where your use cases live: every command, every query, every orchestration step between the outside world and the domain. It's also where Clean Architecture most often goes wrong, with business logic leaking into handlers that should only coordinate. Here is what belongs in the Application layer, what doesn't, and how to keep your handlers thin.

What Is the Application Layer?

In Clean Architecture, the Application layer sits between the Domain layer (inner) and the Infrastructure layer (outer).

It has three main responsibilities:

  1. Define use cases - each use case is a single application operation (place an order, cancel a subscription, get user details)
  2. Orchestrate domain objects - it calls domain entities and services to execute business logic
  3. Define abstractions - it declares interfaces that the Infrastructure layer implements (repositories, email services, payment gateways)

The Application layer knows about the Domain layer but knows nothing about databases, HTTP, or external services.

If the Domain layer answers "what are the business rules?", the Application layer answers "when do they run, and what happens around them?".

What Belongs in the Application Layer

Use cases (Command/Query handlers):

public class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand, Guid>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IUnitOfWork _unitOfWork;

    public PlaceOrderCommandHandler(
        IOrderRepository orderRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _unitOfWork = unitOfWork;
    }

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

        _orderRepository.Add(order);

        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return order.Id;
    }
}

DTOs and response models:

public sealed record OrderResponse(
    Guid Id,
    string CustomerName,
    decimal TotalAmount,
    string Status,
    DateTime CreatedAt);

Interface definitions (ports):

public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
    void Add(Order order);
}

public interface IEmailService
{
    Task SendOrderConfirmationAsync(
        string recipientEmail,
        Guid orderId,
        CancellationToken cancellationToken = default);
}

public interface IUnitOfWork
{
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

Validators:

public class PlaceOrderCommandValidator : AbstractValidator<PlaceOrderCommand>
{
    public PlaceOrderCommandValidator()
    {
        RuleFor(x => x.CustomerId).NotEmpty();
        RuleFor(x => x.Items).NotEmpty();
        RuleFor(x => x.Items)
            .Must(items => items.All(i => i.Quantity > 0))
            .WithMessage("All items must have a positive quantity.");
    }
}

What Does NOT Belong

  • Database access code - no DbContext, no SQL, no connection strings
  • HTTP concerns - no controllers, no HttpContext, no request/response objects
  • Framework dependencies - no EF Core, no MassTransit, no Serilog
  • Third-party service implementations - only interfaces

The Application layer defines what the application does. Infrastructure defines how.

Keep Handlers Thin

The most common mistake I see in application layers is business logic leaking into handlers.

Here's what that looks like:

// Business logic in the handler - don't do this
public async Task<Result> Handle(CancelOrderCommand command, CancellationToken ct)
{
    var order = await _orderRepository.GetByIdAsync(command.OrderId, ct);

    if (order.Status == OrderStatus.Shipped ||
        order.Status == OrderStatus.Delivered)
    {
        return Result.Failure(OrderErrors.CannotCancel);
    }

    order.Status = OrderStatus.Cancelled;
    order.CancelledAt = DateTime.UtcNow;

    await _unitOfWork.SaveChangesAsync(ct);
    return Result.Success();
}

The rule "you can't cancel a shipped order" is a business rule. It belongs in the domain, where every other code path that cancels orders will also enforce it:

// Orchestration in the handler, rules in the domain
public async Task<Result> Handle(CancelOrderCommand command, CancellationToken ct)
{
    var order = await _orderRepository.GetByIdAsync(command.OrderId, ct);

    if (order is null)
    {
        return Result.Failure(OrderErrors.NotFound(command.OrderId));
    }

    var result = order.Cancel();

    if (result.IsFailure)
    {
        return result;
    }

    await _unitOfWork.SaveChangesAsync(ct);
    return Result.Success();
}

The handler loads the aggregate, calls one domain method, and saves. If your handlers read like a table of contents (load, act, save), you got it right. If they read like a business rules engine, your domain model is anemic.

Organizing Use Cases

I recommend organizing use cases by feature, not by type:

Application/
  Orders/
    PlaceOrder/
      PlaceOrderCommand.cs
      PlaceOrderCommandHandler.cs
      PlaceOrderCommandValidator.cs
    CancelOrder/
      CancelOrderCommand.cs
      CancelOrderCommandHandler.cs
    GetOrderById/
      GetOrderByIdQuery.cs
      GetOrderByIdQueryHandler.cs
      OrderResponse.cs
  Customers/
    RegisterCustomer/
      RegisterCustomerCommand.cs
      RegisterCustomerCommandHandler.cs

This follows the Screaming Architecture principle - the folder structure tells you what the application does, not what frameworks it uses.

I cover the alternatives (and when each one breaks down) in how to organize use cases in Clean Architecture, and there's a full worked example in building your first use case with Clean Architecture.

CQRS in the Application Layer

The CQRS pattern is a natural fit for the Application layer. Commands change state, queries return data.

Two paths through the application layer: a command loads an aggregate, runs domain logic, and saves via the unit of work, while a query bypasses the domain and projects directly to a DTO

Commands go through the full domain model:

// Command → Load aggregate → Execute domain logic → Save
var order = await _orderRepository.GetByIdAsync(command.OrderId, cancellationToken);
order.Cancel();
await _unitOfWork.SaveChangesAsync(cancellationToken);

Queries bypass the domain model entirely:

// Query → Project directly from database → Return DTO
return await _dbContext.Orders
    .Where(o => o.Id == query.OrderId)
    .Select(o => new OrderResponse(
        o.Id,
        o.Customer.Name,
        o.TotalAmount.Amount,
        o.Status.Name,
        o.CreatedAt))
    .FirstOrDefaultAsync(cancellationToken);

This separation lets you optimize reads independently from writes. Your write side uses the rich domain model; your read side projects directly into flat DTOs.

There's a pragmatic tension hiding in that query example: projecting "directly from the database" means the query handler needs some database access. You have three options, from purest to most pragmatic:

  • A query-specific abstraction (e.g. IOrderReadService) implemented in Infrastructure. Purest, but you write an interface per query group.
  • An IApplicationDbContext interface exposing DbSet<T> properties. Convenient, but it leaks EF Core types into the Application layer.
  • Dapper with an IDbConnectionFactory. Fast and explicit SQL, at the cost of a second data access approach to maintain.

All three work in practice. For most teams, the IApplicationDbContext compromise is fine for queries, as long as commands still go through repositories and the domain model.

Where Do the Interfaces Live?

Teams argue about whether repository interfaces belong in the Domain layer or the Application layer. Both are defensible:

  • Domain layer: the repository is conceptually part of the aggregate's contract ("an Order can be loaded and saved"). This is the classic DDD position.
  • Application layer: the domain stays 100% persistence-free, and repositories are just another port the application needs, like IEmailService.

I lean toward the Application layer for everything except cases where a domain service genuinely needs the abstraction. What matters far more than the choice is consistency: pick one location and enforce it with architecture tests.

Cross-Cutting Concerns

The Application layer is the right place to define cross-cutting behaviors like validation, logging, and caching - typically implemented as decorators (here using Scrutor's Decorate) or pipeline behaviors:

// Each Decorate wraps the previous registration:
// Logging runs first, then Validation, then the handler
builder.Services.Decorate(typeof(ICommandHandler<,>), typeof(ValidationCommandHandler<,>));
builder.Services.Decorate(typeof(ICommandHandler<,>), typeof(LoggingCommandHandler<,>));

This keeps your handlers focused on business logic while cross-cutting concerns are handled transparently. If you're using MediatR, pipeline behaviors give you the same result.

The Application Layer's Dependencies

The Application layer should only reference:

  • The Domain layer (entities, value objects, domain events, domain services)
  • Abstractions packages (FluentValidation contracts, MediatR contracts - but not their implementations)

It should NOT reference:

  • Infrastructure packages (EF Core, Dapper, MassTransit)
  • Presentation packages (ASP.NET Core)

You can enforce this with architecture tests:

[Fact]
public void Application_Should_Not_Reference_Infrastructure()
{
    var result = Types
        .InAssembly(typeof(PlaceOrderCommand).Assembly)
        .ShouldNot()
        .HaveDependencyOn("Infrastructure")
        .GetResult();

    result.IsSuccessful.Should().BeTrue();
}

Without a test like this, the boundary erodes one "temporary" using directive at a time.

Summary

The Application layer is where your system's use cases live. It:

  • Defines use cases as commands and queries
  • Orchestrates domain objects without knowing about infrastructure
  • Declares interfaces that the outer layers implement
  • Keeps business logic testable and framework-independent

Keep handlers thin, structure the layer by feature, use CQRS to separate reads from writes, and enforce the boundaries with architecture tests.

Thanks for reading, and stay awesome!


Frequently Asked Questions

What is the application layer responsible for in Clean Architecture?

The application layer defines use cases, orchestrates domain objects to execute business logic, and declares the interfaces that infrastructure implements. It coordinates the work but delegates business rules to the domain layer.

What is the difference between the application layer and the domain layer?

The domain layer contains business rules and entities that exist independently of any use case. The application layer contains the use cases themselves: it loads domain objects, invokes their behavior, and persists the result. Domain answers what the rules are; application answers when they run.

Should repository interfaces go in the domain layer or the application layer?

Both are valid. Placing them in the domain layer emphasizes that aggregates and their persistence contracts belong together. Placing them in the application layer keeps the domain completely free of persistence concepts. Pick one convention and enforce it consistently.

Can the application layer reference EF Core?

Ideally no. The application layer should depend on abstractions it owns, like repository or IApplicationDbContext interfaces, while infrastructure provides the EF Core implementation. Some teams pragmatically expose an interface with DbSet properties, which keeps DI intact but leaks EF Core types into the layer.

Do I need MediatR to build an application layer?

No. MediatR is one way to dispatch requests to handlers, but you can define your own ICommandHandler and IQueryHandler interfaces and register them in the DI container. The pattern matters, not the library.

Loading comments...

Whenever you're ready, there are 4 ways I can help you:

  1. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  2. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  3. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  4. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

Become a Better .NET Software Engineer

Join 66,000+ engineers who are improving their skills every Saturday morning.