Organize use cases by feature or aggregate, one folder per use case, containing the command or query, its handler, and its validator. Name them with verb-noun domain language, and the Application layer reads like a list of things the system can do.
In a poorly organized project, you get Handlers, Dtos, and Validators, and no idea what the application is for.
Here are the three layouts you'll encounter, and why only one of them stays navigable at 200 use cases, not just at 20.
Use Cases Are the Application Layer
In Clean Architecture, the Application layer contains use cases - the actions your system can perform. Each use case represents a single thing the user can do.
But as your project grows from 5 use cases to 50 to 200, organization becomes critical.
Strategy 1: Group by Feature (Recommended)
Organize use cases by business feature or aggregate:
Application/
Orders/
PlaceOrder/
PlaceOrderCommand.cs
PlaceOrderCommandHandler.cs
PlaceOrderValidator.cs
CancelOrder/
CancelOrderCommand.cs
CancelOrderCommandHandler.cs
GetOrderById/
GetOrderByIdQuery.cs
GetOrderByIdQueryHandler.cs
OrderResponse.cs
GetOrders/
GetOrdersQuery.cs
GetOrdersQueryHandler.cs
OrderSummaryResponse.cs
Customers/
RegisterCustomer/
RegisterCustomerCommand.cs
RegisterCustomerCommandHandler.cs
GetCustomerProfile/
GetCustomerProfileQuery.cs
GetCustomerProfileQueryHandler.cs
Each use case gets its own folder. Everything related to placing an order - the command, handler, validator, DTOs - lives in one place.
Benefits:
- Find any use case instantly
- Related code is co-located
- Fewer merge conflicts (teams work on different features)
- Mirrors feature folders in Vertical Slice Architecture
This is Screaming Architecture in practice: the folder names shout Orders and Customers, not Handlers and DTOs.
Strategy 2: Group by CQRS
When using CQRS, separate commands from queries:
Application/
Commands/
Orders/
PlaceOrder/
PlaceOrderCommand.cs
PlaceOrderCommandHandler.cs
CancelOrder/
CancelOrderCommand.cs
CancelOrderCommandHandler.cs
Queries/
Orders/
GetOrderById/
GetOrderByIdQuery.cs
GetOrderByIdQueryHandler.cs
GetOrders/
GetOrdersQuery.cs
GetOrdersQueryHandler.cs
This makes it clear which operations modify state and which are read-only.
The downside shows up as the project grows: everything about Orders is now split across two top-level trees.
To understand the Orders feature, you jump between Commands/Orders and Queries/Orders constantly.
I'd only pick this layout if your read and write sides are owned by different teams or deployed separately.
The Command/Query suffix already tells you which side you're on.
Strategy 3: Group by Technical Type (Avoid This)
For completeness, here's the layout you should not use:
Application/
Commands/
PlaceOrderCommand.cs
CancelOrderCommand.cs
RegisterCustomerCommand.cs
Handlers/
PlaceOrderCommandHandler.cs
CancelOrderCommandHandler.cs
RegisterCustomerCommandHandler.cs
Validators/
PlaceOrderValidator.cs
Dtos/
OrderResponse.cs
CustomerResponse.cs
It looks tidy at 10 use cases.
At 100, adding one feature means touching four distant folders, and Handlers/ is a 100-file wall where nothing is related to its neighbors.
Folders should group things that change together. A command and its handler change together. Two unrelated handlers don't.
One Use Case Per Folder
Each use case should have:
- A request (command or query)
- A handler
- Optionally: a validator, DTOs, and mapping
// PlaceOrderCommand.cs
public sealed record PlaceOrderCommand(
Guid CustomerId,
List<OrderItemRequest> Items) : ICommand<Guid>;
// PlaceOrderCommandHandler.cs
public sealed 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 ct)
{
var order = Order.Create(command.CustomerId, command.Items);
_orderRepository.Add(order);
await _unitOfWork.SaveChangesAsync(ct);
return order.Id;
}
}
// PlaceOrderValidator.cs
public sealed class PlaceOrderValidator
: AbstractValidator<PlaceOrderCommand>
{
public PlaceOrderValidator()
{
RuleFor(x => x.CustomerId).NotEmpty();
RuleFor(x => x.Items).NotEmpty();
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.ProductId).NotEmpty();
item.RuleFor(x => x.Quantity).GreaterThan(0);
});
}
}
Keep handlers small. If a handler grows beyond 30-40 lines, the business logic probably belongs in a domain service or the domain entity itself.
Single-File Use Cases
Some teams take co-location one step further and put the whole use case in one file with a static wrapper class:
// PlaceOrder.cs
public static class PlaceOrder
{
public sealed record Command(
Guid CustomerId,
List<OrderItemRequest> Items) : ICommand<Guid>;
public sealed class Validator : AbstractValidator<Command>
{
public Validator()
{
RuleFor(x => x.CustomerId).NotEmpty();
RuleFor(x => x.Items).NotEmpty();
}
}
internal sealed class Handler : ICommandHandler<Command, Guid>
{
// ...
}
}
You reference it as PlaceOrder.Command, which reads nicely at call sites.
The tradeoff is longer files and slightly unusual navigation.
I use separate files by default and reach for this style in smaller projects or vertical slice codebases.
Pick one convention per project and stick with it.
Naming Conventions
Use verb-noun naming that describes what the use case does:
PlaceOrderCommand, notOrderCommand- the verb says what happensCancelOrderCommand, notUpdateOrderStatusCommand- name the business operation, not the database effectGetOrderByIdQuery, notOrderQuery- be specific about what's fetchedRegisterCustomerCommand, notCreateCustomerCommand- match how the business talks
PlaceOrder is domain language. CreateOrder is CRUD language. Use the Ubiquitous Language from your domain.
Shared DTOs and Contracts
When multiple use cases share the same response:
Application/
Orders/
PlaceOrder/...
CancelOrder/...
GetOrderById/...
Shared/
OrderResponse.cs
OrderSummaryResponse.cs
OrderItemResponse.cs
Or at the feature root level:
Application/
Orders/
OrderResponse.cs ← shared by multiple queries
PlaceOrder/...
GetOrderById/...
Keep shared DTOs minimal. If two queries need slightly different data, create separate response types rather than one bloated class.
Interfaces and Abstractions
Define repository and service interfaces close to the features that use them:
Application/
Orders/
IOrderRepository.cs
PlaceOrder/...
CancelOrder/...
Customers/
ICustomerRepository.cs
RegisterCustomer/...
Or in a centralized Abstractions folder if they're used across features:
Application/
Abstractions/
IUnitOfWork.cs
ICurrentUserService.cs
IDateTimeProvider.cs
Orders/
IOrderRepository.cs
PlaceOrder/...
Behaviors (Cross-Cutting Concerns)
Cross-cutting concerns like validation, logging, and caching are pipeline behaviors that wrap use case handlers:
Application/
Behaviors/
ValidationBehavior.cs
LoggingBehavior.cs
CachingBehavior.cs
Orders/
PlaceOrder/...
These apply to all use cases automatically - no need to duplicate logic in each handler.
Enforcing the Conventions
Conventions decay without enforcement. A few architecture tests keep the structure honest:
[Fact]
public void CommandHandlers_Should_Have_CommandHandler_Suffix()
{
var result = Types
.InAssembly(ApplicationAssembly)
.That()
.ImplementInterface(typeof(ICommandHandler<,>))
.Should()
.HaveNameEndingWith("CommandHandler")
.GetResult();
result.IsSuccessful.Should().BeTrue();
}
[Fact]
public void Handlers_Should_Be_Sealed()
{
var result = Types
.InAssembly(ApplicationAssembly)
.That()
.ImplementInterface(typeof(ICommandHandler<,>))
.Should()
.BeSealed()
.GetResult();
result.IsSuccessful.Should().BeTrue();
}
ApplicationAssembly is a shared static field (typeof(PlaceOrderCommand).Assembly) so every test scans the same project.
These tests also assume the separate-file convention.
If you use the single-file style with nested Handler classes, adjust the naming rule to match.
Cheap to write, and they catch drift in code review before it becomes precedent.
What Goes Wrong?
Fat handlers. A handler with 100+ lines is doing too much. Extract business logic to domain entities or domain services. The handler should only orchestrate.
Shared commands. One command used for both creating and updating. Create separate PlaceOrderCommand and UpdateOrderCommand - they have different validation and different business rules.
Anemic use cases. A use case that just calls repository.Add(entity) with no business logic. This is fine for simple CRUD, but if there are invariants to enforce, they belong in the domain.
Key Takeaways
Organize use cases by feature, one per folder, with clear verb-noun names:
- Group by feature or aggregate
- One use case per folder (command/query + handler + validator)
- Use domain language for naming
- Keep handlers thin - orchestration only
- Share DTOs sparingly
The structure should make it obvious what your application does just by looking at the folder names.
Thanks for reading, and stay awesome!
Frequently Asked Questions
How should I organize use cases in Clean Architecture?
Group them by feature or aggregate, with one folder per use case containing the command or query, its handler, and its validator. The folder structure should read like a list of things your application can do.
Should commands and queries be in separate folders?
You can split top-level Commands and Queries folders, but grouping by feature first usually works better. Everything about Orders lives together, and the Command or Query suffix already tells you which side of CQRS you are on.
Is organizing code by technical type bad?
Folders like Handlers, Validators, and DTOs scatter each use case across the project, so a single change touches three or four distant folders. Group by feature instead, and let types live next to the use case that owns them.
How big should a use case handler be?
Roughly 30 to 40 lines. Handlers should orchestrate: load an aggregate, call domain behavior, save. If a handler grows past that, the business logic probably belongs in the domain entity or a domain service.
Where do shared DTOs go when multiple use cases need them?
At the feature root or in a Shared folder inside the feature. Keep them minimal; if two queries need slightly different data, two small response types beat one bloated shared class.



