What It Is
Clean architecture is an approach to software design that keeps business rules at the center and pushes frameworks, databases, and external services to the edges. The main goal is to make the core of the application independent of delivery mechanisms and infrastructure details.
The central rule is usually called the dependency rule: source code dependencies should point inward. Inner layers may define policies and contracts, while outer layers implement technical details. That direction helps prevent the domain model and use cases from being shaped by a web framework or ORM.
Why Teams Use It
Teams use clean architecture when they want business behavior to remain stable even as tools, integrations, and UI choices change. It is especially attractive in systems where use cases matter more than the transport layer and where the cost of coupling business logic to infrastructure would accumulate over time.
Typical Layers
- Entities: core business rules and invariants
- Use Cases: application-specific workflows that coordinate domain behavior
- Interface Adapters: controllers, presenters, view models, and repository implementations that translate between layers
- Frameworks and Drivers: web frameworks, databases, queues, file systems, and external APIs
Different teams name these layers differently, but the idea stays the same: policies are inside, details are outside.
How It Differs from Onion Architecture
Clean architecture and onion architecture are closely related. Both protect the domain from infrastructure and both keep dependencies flowing inward. In practice, many codebases use the same project structure for either label.
The main difference is emphasis. Onion architecture is often explained in terms of concentric dependency layers around the domain. Clean architecture usually emphasizes use cases, input and output boundaries, and adapters that isolate the application from delivery concerns such as HTTP, persistence, or messaging.
Pros
- Business rules stay isolated from web, database, and vendor-specific details
- Use cases become easier to test because they can run without real I/O
- Infrastructure choices are easier to replace when boundaries are explicit
- The structure makes application workflows and responsibilities more visible
- It can support long-lived systems where technical dependencies are expected to change
Cons
- There is more ceremony than in a straightforward CRUD application
- Poorly chosen abstractions can create indirection without delivering real flexibility
- Small apps may end up with too many folders, interfaces, and mapping types too early
- Teams need discipline to stop framework concerns from leaking inward over time
Common Misconceptions
- "Clean architecture means no framework." You can absolutely use frameworks. The point is to keep them at the edge instead of letting them dictate core design.
- "Every dependency needs an interface." It is better to introduce boundaries around volatile details and side effects, not to abstract every class by default.
- "It guarantees good code." The structure helps, but naming, tests, and business modeling still matter more than folder layout.
- "Controllers should contain use-case logic." In clean architecture, controllers should translate requests and delegate to use cases rather than becoming the place where application rules live.
When to Choose It
Choose clean architecture when the application has meaningful business workflows, multiple integrations, or a long expected lifetime. It is a strong fit when you want use cases to remain readable and testable without being buried in controller actions, ORM entities, or SDK-specific code.
Avoid forcing the full structure into a tiny app that mostly performs simple CRUD with little domain logic. In that case, the ceremony can outweigh the benefit until complexity actually appears.
Example Structure
A simple .NET solution might separate policy from implementation like this:
src/
SupportTickets.Domain/
Entities/
Ticket.cs
ValueObjects/
TicketPriority.cs
SupportTickets.Application/
Tickets/
CreateTicket/
CreateTicketCommand.cs
CreateTicketHandler.cs
ICreateTicketOutputPort.cs
GetTicket/
GetTicketQuery.cs
GetTicketHandler.cs
Abstractions/
ITicketRepository.cs
IUnitOfWork.cs
SupportTickets.Infrastructure/
Persistence/
AppDbContext.cs
TicketRepository.cs
Notifications/
EmailNotifier.cs
SupportTickets.Api/
Controllers/
TicketsController.cs
Presenters/
CreateTicketPresenter.cs
DependencyInjection.cs
Program.csThe application layer defines the workflow and contracts. The API and Infrastructure layers translate requests and implement the details needed to execute that workflow.
Code Example
A common pattern is to keep the use case free from HTTP and ORM concerns, then have outer layers adapt to it.
// SupportTickets.Application/Tickets/CreateTicket/CreateTicketHandler.cs
namespace SupportTickets.Application.Tickets.CreateTicket;
public sealed class CreateTicketHandler
{
private readonly ITicketRepository _ticketRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly ICreateTicketOutputPort _outputPort;
public CreateTicketHandler(
ITicketRepository ticketRepository,
IUnitOfWork unitOfWork,
ICreateTicketOutputPort outputPort)
{
_ticketRepository = ticketRepository;
_unitOfWork = unitOfWork;
_outputPort = outputPort;
}
public async Task Handle(CreateTicketCommand command, CancellationToken cancellationToken)
{
var ticket = Ticket.Create(command.Title, command.Description, command.Priority);
await _ticketRepository.AddAsync(ticket, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
_outputPort.Ok(new CreateTicketResponse(ticket.Id));
}
}
// SupportTickets.Api/Controllers/TicketsController.cs
[ApiController]
[Route("api/tickets")]
public sealed class TicketsController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Create(
[FromBody] CreateTicketRequest request,
[FromServices] CreateTicketHandler handler,
CancellationToken cancellationToken)
{
await handler.Handle(
new CreateTicketCommand(request.Title, request.Description, request.Priority),
cancellationToken);
return Accepted();
}
}The controller is thin. It accepts transport-specific input, translates it into a command, and lets the use case own the workflow. That separation is the point.
Conclusion
Clean architecture is most useful when business behavior deserves protection from short-lived implementation details. It adds structure and indirection, so it should be chosen deliberately, but it can pay off when an application needs stable use cases, explicit boundaries, and room to evolve over time.