Clean Architecture .NET in 2026: CQRS, MediatR and Developer Interview Questions
Master Clean Architecture in .NET with CQRS and MediatR 14. Learn layered architecture patterns, pipeline behaviors, and prepare for technical interviews with real-world examples.

Clean Architecture in .NET separates business logic from infrastructure concerns, making applications easier to test, maintain, and scale. Combined with CQRS (Command Query Responsibility Segregation) and MediatR, this architecture pattern provides a structured approach to building enterprise applications that interviewers frequently assess.
Clean Architecture questions appear in 70% of senior .NET interviews. Interviewers expect candidates to explain the dependency rule: dependencies point inward, with the domain layer having zero dependencies on external frameworks.
Clean Architecture Layers and the Dependency Rule
The Ardalis Clean Architecture template for ASP.NET Core 10 organizes code into four concentric layers. Each layer has specific responsibilities and strict dependency rules.
// Core/Domain Layer - No external dependencies
// src/Core/Domain/Entities/Order.cs
namespace Core.Domain.Entities;
public class Order
{
public Guid Id { get; private set; }
public string CustomerEmail { get; private set; }
public List<OrderLine> Lines { get; private set; } = new();
public OrderStatus Status { get; private set; }
public DateTime CreatedAt { get; private set; }
// Domain logic encapsulated in the entity
public void AddLine(Product product, int quantity)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot modify a submitted order");
var existingLine = Lines.FirstOrDefault(l => l.ProductId == product.Id);
if (existingLine is not null)
{
existingLine.IncreaseQuantity(quantity);
return;
}
Lines.Add(new OrderLine(product.Id, product.Price, quantity));
}
public void Submit()
{
if (!Lines.Any())
throw new InvalidOperationException("Cannot submit an empty order");
Status = OrderStatus.Submitted;
}
}The domain layer contains entities, value objects, and domain services. It knows nothing about databases, HTTP, or external frameworks.
CQRS Pattern: Separating Reads from Writes
CQRS splits operations into Commands (write operations that change state) and Queries (read operations that return data). This separation allows independent scaling and optimization of each path.
// UseCases Layer - Commands and Queries
// src/UseCases/Orders/Commands/CreateOrder/CreateOrderCommand.cs
namespace UseCases.Orders.Commands.CreateOrder;
public record CreateOrderCommand(
string CustomerEmail,
List<OrderLineDto> Lines
) : IRequest<Result<Guid>>;
public record OrderLineDto(Guid ProductId, int Quantity);Commands return minimal data, typically just a success indicator or a newly created ID. Queries return DTOs optimized for the consumer.
namespace UseCases.Orders.Queries.GetOrderById;
public record GetOrderByIdQuery(Guid OrderId) : IRequest<Result<OrderDetailsDto>>;
public record OrderDetailsDto(
Guid Id,
string CustomerEmail,
string Status,
decimal TotalAmount,
List<OrderLineDetailsDto> Lines
);MediatR 14: Pipeline Behaviors and Handler Implementation
MediatR 14.2 provides the messaging infrastructure for CQRS. Each command or query has exactly one handler, enforcing single responsibility.
namespace UseCases.Orders.Commands.CreateOrder;
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
private readonly IOrderRepository _orderRepository;
private readonly IProductRepository _productRepository;
private readonly IUnitOfWork _unitOfWork;
public CreateOrderHandler(
IOrderRepository orderRepository,
IProductRepository productRepository,
IUnitOfWork unitOfWork)
{
_orderRepository = orderRepository;
_productRepository = productRepository;
_unitOfWork = unitOfWork;
}
public async Task<Result<Guid>> Handle(
CreateOrderCommand request,
CancellationToken cancellationToken)
{
// Load products to validate and get prices
var productIds = request.Lines.Select(l => l.ProductId).ToList();
var products = await _productRepository
.GetByIdsAsync(productIds, cancellationToken);
if (products.Count != productIds.Count)
return Result.NotFound("One or more products not found");
// Create order using domain logic
var order = new Order(request.CustomerEmail);
foreach (var line in request.Lines)
{
var product = products.First(p => p.Id == line.ProductId);
order.AddLine(product, line.Quantity);
}
await _orderRepository.AddAsync(order, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success(order.Id);
}
}Ready to ace your .NET interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Pipeline Behaviors for Cross-Cutting Concerns
Pipeline behaviors in MediatR handle logging, validation, caching, and transaction management without polluting handlers.
namespace UseCases.Common.Behaviors;
public class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (!_validators.Any())
return await next();
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = validationResults
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return await next();
}
}Register behaviors in the order they should execute. Validation typically runs first, followed by logging, then transaction handling.
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});Infrastructure Layer: Repository Implementation
The infrastructure layer implements interfaces defined in the core layer. Entity Framework Core repositories translate domain operations to database calls.
namespace Infrastructure.Data.Repositories;
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public OrderRepository(AppDbContext context)
{
_context = context;
}
public async Task<Order?> GetByIdAsync(
Guid id,
CancellationToken cancellationToken = default)
{
return await _context.Orders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task AddAsync(
Order order,
CancellationToken cancellationToken = default)
{
await _context.Orders.AddAsync(order, cancellationToken);
}
public async Task<IReadOnlyList<Order>> GetByCustomerEmailAsync(
string email,
CancellationToken cancellationToken = default)
{
return await _context.Orders
.Where(o => o.CustomerEmail == email)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(cancellationToken);
}
}Interview Questions: What Senior Developers Should Know
Technical interviews for senior .NET positions frequently include Clean Architecture questions. Here are the patterns interviewers assess:
Q: Why does the domain layer have no dependencies on external frameworks?
The domain layer contains business rules that should remain stable regardless of infrastructure changes. If the domain depends on Entity Framework, switching to Dapper or a different database requires changing business logic. The dependency rule ensures infrastructure is a detail that can be swapped without affecting core behavior.
Q: When is CQRS overkill?
CQRS adds complexity that simple CRUD applications do not need. If read and write models are nearly identical, if there is no need for separate scaling, and if the team is small, a simpler architecture is appropriate. CQRS shines when read models differ significantly from write models, when event sourcing is required, or when read and write loads need independent scaling.
Q: How do you handle transactions across multiple aggregates?
// Transaction behavior wraps the entire handler execution
public class TransactionBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IUnitOfWork _unitOfWork;
public TransactionBehavior(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
// Commands that modify multiple aggregates use explicit transactions
if (request is not ICommand)
return await next();
await using var transaction = await _unitOfWork
.BeginTransactionAsync(cancellationToken);
try
{
var response = await next();
await transaction.CommitAsync(cancellationToken);
return response;
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
}For operations spanning multiple aggregates, use eventual consistency with domain events when possible. Synchronous transactions across aggregates indicate a potential design issue: the aggregates might belong together.
Read-Side Optimization with Separate Query Models
Queries bypass the domain layer entirely when performance matters. Direct database access with Dapper or raw SQL avoids the overhead of materializing full entity graphs.
namespace Infrastructure.Queries;
public class GetOrderByIdQueryHandler
: IRequestHandler<GetOrderByIdQuery, Result<OrderDetailsDto>>
{
private readonly IDbConnection _connection;
public GetOrderByIdQueryHandler(IDbConnection connection)
{
_connection = connection;
}
public async Task<Result<OrderDetailsDto>> Handle(
GetOrderByIdQuery request,
CancellationToken cancellationToken)
{
const string sql = """
SELECT o.Id, o.CustomerEmail, o.Status, o.CreatedAt,
SUM(ol.UnitPrice * ol.Quantity) AS TotalAmount
FROM Orders o
LEFT JOIN OrderLines ol ON ol.OrderId = o.Id
WHERE o.Id = @OrderId
GROUP BY o.Id, o.CustomerEmail, o.Status, o.CreatedAt
""";
var order = await _connection.QueryFirstOrDefaultAsync<OrderDetailsDto>(
new CommandDefinition(sql, new { request.OrderId }, cancellationToken: cancellationToken));
return order is null
? Result.NotFound($"Order {request.OrderId} not found")
: Result.Success(order);
}
}Start practicing!
Test your knowledge with our interview simulators and technical tests.
Clean Architecture Interview Preparation: Key Takeaways
- The dependency rule states that source code dependencies can only point inward. The domain layer has no knowledge of databases, frameworks, or delivery mechanisms.
- CQRS separates commands (state changes) from queries (data retrieval). Commands go through the domain model and validation. Queries can bypass domain entities for performance.
- MediatR 14 routes requests to handlers. Pipeline behaviors handle cross-cutting concerns like validation, logging, and transactions in a composable way.
- Repository interfaces live in the core layer. Implementations live in infrastructure. This inversion allows swapping EF Core for Dapper without touching business logic.
- Interview answers should include trade-offs. Clean Architecture adds initial complexity but pays off in large, long-lived applications with multiple developers.
- The ASP.NET Core Clean Architecture module on SharpSkill covers additional patterns including Specification, Result objects, and Domain Events.
Can you spot the bug in .NET?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on September 15, 2026
Tags
Share
Related articles

DbContext Lifetime in ASP.NET Core: Performance vs Thread Safety in Async Operations
Master DbContext lifetime management in ASP.NET Core. Learn when to use scoped vs transient lifetimes, how to safely handle async operations, and optimize performance with DbContext pooling.

.NET 9 Blazor: Full-Stack Development with Blazor United in 2026
.NET 9 Blazor United combines static SSR, Server, and WebAssembly render modes into one full-stack framework. A practical tutorial covering render modes, streaming rendering, constructor injection, and production-ready patterns.

ASP.NET Core Minimal APIs in 2026: Architecture, Performance and Interview Questions
Master ASP.NET Core Minimal APIs with this deep dive covering route groups, endpoint filters, Native AOT compilation, typed results, and common interview questions.