There is a file in almost every .NET project that everyone on the team is quietly afraid of.
It does not have a scary name. It is usually called something harmless like ProductService.cs or OrderService.cs.
But when you open it you will find five hundred lines of mixed logic where reading data and writing data and validating data and sending emails all live in the same class pretending to be friends.
Changing one thing in that file risks breaking three other things. Testing it requires mocking half the universe. Every new feature adds another method that nobody is proud of.
Nobody built that file intending for it to become a problem. Nobody built that file intending for it to become a problem. It grew that way. One feature at a time.
This article is about why that happens and how Clean Architecture combined with CQRS fixes it permanently not just theoretically with actual .NET code with real folder structure which you can use starting today.
Why Most .NET Projects Slowly Fall Apart
When you start a new .NET project everything feels clean. Controllers are thin, services are focused, the database does what the it is supposed to do.
Once requirements change business logic creeps into controllers after queries get written inside the same service that also handles updates and deletions and sends notifications.
Six months later someone opens OrderService.cs and it has forty methods, seven injected dependencies and a comment that says “do not touch this method nobody knows how it works.”
The real problem is not bad developers instead it’s about missing architectural guidance because they are working without it and end up creating random boundaries between objects and classes by writing inconsistently which is often worse than code written with no structure at all.
The problem is that there was no clear answer to the question: where does this code belong?
Clean Architecture is the answer to that question.
What Clean Architecture Actually Is ?
Most developers think Clean Architecture is about folders. They see the famous diagram with concentric circles, create four projects in their solution and call it done. But their code is still a mess just organised into more folders.
Clean Architecture is not about folders. It is about one rule: dependencies only point inward. The outer layers know about the inner layers. The inner layers know nothing about the outer layers.
Think of it like the layers of an onion. At the very center is your Domain. Your entities, your business rules, the things that make your application what it is.
Domain has zero knowledge of databases, HTTP, Entity Framework or anything external. It just describes what your system does in pure business terms.
Surrounding Domain is Application. This is where your use cases live. It knows about Domain but nothing about infrastructure. It defines what operations your system can perform.
Outside Application is Infrastructure. Entity Framework, email services, file storage, external APIs. All the technical details that the rest of your application should not care about.
On the outside is the API layer. Controllers, minimal API endpoints, anything that receives a request from the outside world and hands it to your application layer.
Here is what that looks like as a real solution structure for a visitor management system similar to an ERP project:
VisitorManagement.sln
│
├── VisitorManagement.Domain
│ ├── Entities/
│ │ └── Visitor.cs
│ └── Enums/
│ └── VisitorStatus.cs
│
├── VisitorManagement.Application
│ ├── Features/
│ │ └── Visitors/
│ │ ├── Commands/
│ │ │ ├── RegisterVisitor/
│ │ │ │ ├── RegisterVisitorCommand.cs
│ │ │ │ └── RegisterVisitorCommandHandler.cs
│ │ └── Queries/
│ │ ├── GetVisitorById/
│ │ │ ├── GetVisitorByIdQuery.cs
│ │ │ └── GetVisitorByIdQueryHandler.cs
│ ├── Interfaces/
│ │ └── IVisitorRepository.cs
│ └── DTOs/
│ └── VisitorDto.cs
│
├── VisitorManagement.Infrastructure
│ ├── Persistence/
│ │ ├── AppDbContext.cs
│ │ └── Repositories/
│ │ └── VisitorRepository.cs
│ └── Services/
│ └── EmailService.cs
│
└── VisitorManagement.API
└── Controllers/
└── VisitorsController.cs
Notice something about this structure. There is no OrderService.cs or any other class files with forty methods because every feature has its own focused folder and inside each folder the operations are split into two types: Commands and Queries and that split is CQRS.
What CQRS Actually Is and Why It Matters ?
CQRS stands for Command Query Responsibility Segregation. The name sounds like something you would invent to make junior developers feel insecure but the idea is genuinely simple.
Commands change things. Queries read things. Never mix them.
Here is the analogy that finally made it click for me. Think about how a hospital operates. When a doctor needs to know a patient’s history they go to the records department and ask for the file and that is a Query.
Nobody updates the patient’s treatment plan while fetching their history. When a doctor decides to change a patient’s medication they write a prescription and hand it to a nurse who executes it and that is a Command.
The records department and the prescription system are completely separate. One is optimised for fast retrieval of information.
The other is optimised for safe, validated changes to state. If you mixed them together both would work worse and every change would risk corrupting every read.
Your API works the same way. Reading a list of visitors to display on a dashboard has completely different requirements than registering a new visitor. The read operation needs to be fast, might be cached, returns a DTO shaped for the UI. The write operation needs validation, triggers business rules, might send an email notification, updates multiple tables atomically.
When you put both inside the same service method you are forcing two completely different things to share constraints they should never share.
Greg Young who invented CQRS said it best: it is not the pattern itself that is interesting. It is what it enables. When reads and writes are separated you can optimise each independently, test each in complete isolation and add new features to one without touching the other.
CQRS in Real .NET Code Using MediatR
MediatR is the library that makes CQRS practical in .NET. It acts as an in process mediator that means your controller does not call your handler directly. It sends a message and MediatR routes it to the right handler. Your controller has zero knowledge of what happens after it sends that message.
First install the packages:
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
Register MediatR in Program.cs:
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(
typeof(RegisterVisitorCommand).Assembly));
Now here is the Command for registering a visitor. This lives in Application/Features/Visitors/Commands/RegisterVisitor/:
// RegisterVisitorCommand.cs
public record RegisterVisitorCommand(
string Name,
string Email,
int EventId) : IRequest<int>;
// RegisterVisitorCommandHandler.cs
public class RegisterVisitorCommandHandler
: IRequestHandler<RegisterVisitorCommand, int>
{
private readonly IVisitorRepository _repository;
public RegisterVisitorCommandHandler(IVisitorRepository repository)
{
_repository = repository;
}
public async Task<int> Handle(
RegisterVisitorCommand request,
CancellationToken cancellationToken)
{
var visitor = new Visitor
{
Name = request.Name,
Email = request.Email,
EventId = request.EventId,
Status = VisitorStatus.Registered
};
await _repository.AddAsync(visitor, cancellationToken);
return visitor.Id;
}
}
Notice what this handler does not know about. It does not know about HTTP. It does not know about controllers. It does not know about EF Core directly. It talks to an interface. This means you can test it completely without spinning up a web server or a real database.
Now here is the Query for fetching a visitor. This lives in Application/Features/Visitors/Queries/GetVisitorById/:
// GetVisitorByIdQuery.cs
public record GetVisitorByIdQuery(int VisitorId) : IRequest<VisitorDto>;
// GetVisitorByIdQueryHandler.cs
public class GetVisitorByIdQueryHandler
: IRequestHandler<GetVisitorByIdQuery, VisitorDto>
{
private readonly IVisitorRepository _repository;
public GetVisitorByIdQueryHandler(IVisitorRepository repository)
{
_repository = repository;
}
public async Task<VisitorDto> Handle(
GetVisitorByIdQuery request,
CancellationToken cancellationToken)
{
var visitor = await _repository
.GetByIdAsync(request.VisitorId, cancellationToken);
if (visitor is null)
return null;
return new VisitorDto
{
Id = visitor.Id,
Name = visitor.Name,
Email = visitor.Email,
Status = visitor.Status.ToString()
};
}
}
And finally the controller becomes something worth looking at:
[ApiController]
[Route("api/[controller]")]
public class VisitorsController : ControllerBase
{
private readonly IMediator _mediator;
public VisitorsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Register(
RegisterVisitorCommand command)
{
var visitorId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetById),
new { id = visitorId }, visitorId);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var visitor = await _mediator.Send(
new GetVisitorByIdQuery(id));
if (visitor is null)
return NotFound();
return Ok(visitor);
}
}
The controller does one thing. It receives a request and sends it to MediatR that is it. It has no business logic. It does not know what RegisterVisitorCommandHandler does. It just knows that someone will handle it.
This is what the file that everyone is afraid to touch looks like when it has been replaced by CQRS. Focused handlers, clear responsibility and no forty method service class in sight.
The Honest Truth About When Not to Use This
Martin Fowler wrote something important about CQRS that most tutorials skip that is for most systems CQRS adds risky complexity. The majority of cases he encountered where it was applied were not good outcomes.
That warning is real and worth respecting.
If you are building a simple CRUD application with three or four entities and no complex business logic, CQRS and Clean Architecture will make your life harder not easier. You will spend time creating Commands and Queries for operations that a single service method would have handled in thirty seconds.
Use this pattern when your application has genuinely different read and write requirements. When your service classes are growing beyond control. When you need to test business logic in isolation.
When different parts of your system need to scale independently. When adding a new feature currently requires you to understand and risk breaking three others.
The pattern is a just a tool not a documentation book which needs to be followed by all. The developers who get it wrong are the ones who apply it everywhere without asking whether it justifies its complexity in a given context.
What Changes When You Work This Way
When we moved an ERP module to this pattern something shifted in how the codebase felt to work in.
Adding a new feature meant creating a new folder with a Command and a Handler. The rest of the system was completely untouched. The risk of breaking existing functionality went close to zero because each handler is self contained.
Testing became something we actually did rather than something we planned to do eventually because each handler had exactly one dependency to mock.
The file everyone was afraid to touch does not exist anymore. It got replaced by twenty small files that nobody is afraid of.
Architecture is not a way to write better code in the moment. It is a way to write code that your team can understand, change and extend six months from now when none of you remember what you were thinking when you built it.
CQRS inside Clean Architecture is not complexity for its own sake. It is the structure that makes a growing system feel smaller instead of bigger every time you open it.
Which part of CQRS or Clean Architecture has been confusing you the most? Drop your answers in comments and let’s talk more on this topic in detail.

Top comments (0)