Clean Architecture is one of the most popular ways to structure a .NET application. It's also one of the easiest to get wrong. The same mistakes show up in project after project: leaky domains, anemic models, interfaces for everything. Here are the 10 anti-patterns I see most often, and how to fix each one.
What Is the Goal of Clean Architecture?
Clean Architecture exists for one reason: to keep your business logic independent of frameworks, databases, and external concerns.
When applied correctly, it gives you a testable, maintainable codebase. When applied incorrectly, it gives you unnecessary complexity with no real benefit.
Here are the anti-patterns I see most often - and how to avoid them.
1. Leaking Infrastructure Into the Domain
This is the most fundamental violation. Your Domain layer should have zero dependencies on infrastructure packages.
The anti-pattern:
// Domain layer - BAD
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore; // Infrastructure leak!
[Index(nameof(CustomerName))] // EF Core attribute in the domain
public class Order : AggregateRoot
{
[Column("order_id")] // Persistence mapping in the domain
public Guid Id { get; set; }
[Required] // Validation attribute in the domain
public string CustomerName { get; set; }
}
The fix:
// Domain layer - GOOD
public class Order : AggregateRoot
{
private Order(Guid id, string customerName) : base(id)
{
CustomerName = customerName;
}
public string CustomerName { get; private set; }
}
Move all EF Core configuration to the Infrastructure layer using IEntityTypeConfiguration<T>.
Enforce required fields through the constructor instead of validation attributes.
You can enforce this with architecture tests:
[Fact]
public void Domain_Should_Not_Reference_Infrastructure()
{
var result = Types
.InAssembly(typeof(Order).Assembly)
.ShouldNot()
.HaveDependencyOn("Microsoft.EntityFrameworkCore")
.GetResult();
result.IsSuccessful.Should().BeTrue();
}
2. Anemic Domain Model
This is when your entities are just data containers with public getters and setters, and all business logic lives in services.
The anti-pattern:
public class Order
{
public Guid Id { get; set; }
public OrderStatus Status { get; set; }
public List<OrderLineItem> Items { get; set; } = new();
public decimal TotalAmount { get; set; }
}
// Business logic dumped into a service
public class OrderService
{
public void CancelOrder(Order order)
{
if (order.Status == OrderStatus.Shipped)
throw new Exception("Cannot cancel shipped order");
order.Status = OrderStatus.Cancelled;
order.TotalAmount = 0;
}
}
The fix:
public class Order : AggregateRoot
{
public OrderStatus Status { get; private set; }
public Money TotalAmount { get; private set; }
public Result Cancel()
{
if (Status == OrderStatus.Shipped)
{
return Result.Failure(OrderErrors.AlreadyShipped);
}
Status = OrderStatus.Cancelled;
RaiseDomainEvent(new OrderCancelledDomainEvent(Id));
return Result.Success();
}
}
The entity owns its behavior. For a deeper dive, see refactoring from an anemic domain model.
3. Use Cases That Do Too Much
A use case (command/query handler) should orchestrate, not implement business logic directly.
The anti-pattern:
public class PlaceOrderHandler : ICommandHandler<PlaceOrderCommand, Guid>
{
public async Task<Result<Guid>> Handle(PlaceOrderCommand command, CancellationToken ct)
{
var customer = await _customerRepository.GetByIdAsync(command.CustomerId, ct);
// 50 lines of business logic inline
if (command.Items.Count == 0) return Result.Failure("No items");
var total = command.Items.Sum(i => i.Price * i.Quantity);
if (total > 10000) { /* apply discount logic */ }
if (customer.IsVip) { /* more logic */ }
// ... and so on
}
}
The fix:
public class PlaceOrderHandler : ICommandHandler<PlaceOrderCommand, Guid>
{
public async Task<Result<Guid>> Handle(PlaceOrderCommand command, CancellationToken ct)
{
var customer = await _customerRepository.GetByIdAsync(command.CustomerId, ct);
var order = Order.Create(customer, command.Items); // Domain logic on entity
_orderRepository.Add(order);
await _unitOfWork.SaveChangesAsync(ct);
return order.Id;
}
}
The handler orchestrates: load, call domain, save. The domain object owns the rules.
4. Over-Abstracting Everything
Not every class needs an interface. Not every method needs a command/query.
The anti-pattern:
public interface IDateTimeProvider { DateTime UtcNow { get; } }
public interface IOrderMapper { OrderResponse Map(Order order); }
public interface IGuidGenerator { Guid NewGuid(); }
public interface IStringHelper { string Trim(string input); }
The fix: Only abstract things at system boundaries - database access, external APIs, time, file system. Internal logic that has no external dependency doesn't need an interface.
IDateTimeProvider makes sense because you want to control time in tests. IGuidGenerator and IStringHelper don't.
5. Putting Everything Into One Massive Infrastructure Project
A single Infrastructure project that references every NuGet package and contains every implementation becomes a dependency magnet.
The anti-pattern:
Infrastructure/
├── EntityFramework/
├── Redis/
├── RabbitMQ/
├── Stripe/
├── SendGrid/
├── AWS/
└── Azure/
The fix: Split infrastructure by concern:
Infrastructure.Persistence/ ← EF Core, Dapper
Infrastructure.Caching/ ← Redis
Infrastructure.Messaging/ ← RabbitMQ, MassTransit
Infrastructure.Email/ ← SendGrid
Each project only references the packages it needs.
6. Circular Dependencies Between Layers
This happens when the Infrastructure layer tries to use Application services, or the Application layer directly references Presentation types.
The anti-pattern:
// Infrastructure project referencing Application project - OK
// Application project referencing Infrastructure project - BAD
// In Application layer:
using Infrastructure.Persistence; // Circular dependency!
The fix: The Dependency Rule only allows inward dependencies:
Presentation→Application→Domain✓Infrastructure→Application→Domain✓Application→Infrastructure✗Domain→ anything ✗
Define interfaces in the Application layer and implement them in Infrastructure. This is the Dependency Inversion Principle in action.
7. Mapping Mania
Creating a separate DTO at every layer boundary when the data is basically the same.
The anti-pattern:
Request → RequestDto → Command → DomainModel → PersistenceModel → ResponseDto → Response
That's six transformations for a single operation. Each one requires a mapper class.
The fix: Be pragmatic. Not every boundary needs a separate DTO. Commands and queries are your input DTOs. Domain entities map to persistence models through EF Core configuration. Responses project directly from queries.
For more on this, see mapping between layers in Clean Architecture.
8. Using Clean Architecture for Simple CRUD
Not every project needs Clean Architecture. A straightforward CRUD API with five endpoints doesn't need four projects, domain events, and a command/query pipeline.
The fix: Use Clean Architecture when:
- The domain has complex business rules
- Multiple teams work on the same codebase
- The project will evolve over years
- Testability is a hard requirement
For simpler projects, consider Vertical Slice Architecture or just a well-organized single project. I've written a full decision guide in when to use Clean Architecture.
9. Ignoring the Read Side
Sending every query through the domain model, loading full aggregates just to return a DTO.
The anti-pattern:
public class GetOrderHandler : IQueryHandler<GetOrderQuery, OrderResponse>
{
public async Task<Result<OrderResponse>> Handle(GetOrderQuery query, CancellationToken ct)
{
var order = await _orderRepository.GetByIdAsync(query.OrderId, ct); // Full aggregate load
return _mapper.Map<OrderResponse>(order); // Wasteful mapping
}
}
The fix:
public class GetOrderHandler : IQueryHandler<GetOrderQuery, OrderResponse>
{
public async Task<Result<OrderResponse>> Handle(GetOrderQuery query, CancellationToken ct)
{
return await _dbContext.Orders
.Where(o => o.Id == query.OrderId)
.Select(o => new OrderResponse(o.Id, o.CustomerName, o.TotalAmount))
.FirstOrDefaultAsync(ct);
}
}
This is the essence of CQRS - commands go through the domain model, queries bypass it.
10. Not Testing the Architecture
Without automated checks, architecture rules get violated the moment deadlines arrive.
The fix: Use architecture tests to enforce layer boundaries automatically. Run them in CI. Catch violations before they merge.
Summary
Clean Architecture is a means, not an end. The goal is maintainable, testable software - not a perfect diagram.
Avoid these anti-patterns by applying the architecture pragmatically: protect your domain, respect the Dependency Rule, keep use cases thin, and don't over-abstract.
Thanks for reading, and stay awesome!
Frequently Asked Questions
What is the most common Clean Architecture mistake?
Leaking infrastructure concerns into the domain layer, usually through EF Core attributes or framework dependencies on entities. The domain layer should have zero dependencies on infrastructure packages, enforced with architecture tests.
Is an anemic domain model always bad?
Not always. For simple CRUD applications, an anemic model with transaction scripts is fine. It becomes an anti-pattern when your domain has real business rules but they are scattered across services instead of living on the entities that own the data.
Do queries need to go through repositories in Clean Architecture?
No. Loading full aggregates just to return a DTO is wasteful. Queries can project directly from the database into response models, while commands go through repositories and the domain model. This is the core idea behind CQRS.
Does every class need an interface in Clean Architecture?
No. Abstractions belong at system boundaries: database access, external APIs, time, and the file system. Creating interfaces for internal logic with no external dependency adds indirection without any testing or flexibility benefit.
When is Clean Architecture overkill?
For simple CRUD APIs, short-lived projects, and prototypes. If the domain has no real business rules, the layering adds ceremony without payoff. Vertical Slice Architecture or a well-organized single project is often the better fit.



