O que é Clean Architecture?
Proposta por Robert C. Martin (Uncle Bob), Clean Architecture organiza o código em camadas concêntricas onde as dependências sempre apontam para o centro — o domínio. O resultado é um sistema onde regras de negócio são completamente independentes de frameworks, bancos de dados e interfaces externas.
┌─────────────────────────────────────┐
│ Presentation │ ← Controllers, APIs, UI
│ ┌─────────────────────────────┐ │
│ │ Application │ │ ← Use Cases, DTOs
│ │ ┌───────────────────────┐ │ │
│ │ │ Domain │ │ │ ← Entities, Value Objects
│ │ │ (sem dependências) │ │ │
│ │ └───────────────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
Infrastructure (implementações)
Estrutura de Pastas
src/
├── Domain/
│ ├── Entities/
│ │ └── User.cs
│ ├── ValueObjects/
│ │ └── Email.cs
│ ├── Interfaces/
│ │ └── IUserRepository.cs
│ └── Exceptions/
│ └── DomainException.cs
│
├── Application/
│ ├── Users/
│ │ ├── Commands/
│ │ │ └── CreateUser/
│ │ │ ├── CreateUserCommand.cs
│ │ │ └── CreateUserCommandHandler.cs
│ │ └── Queries/
│ │ └── GetUser/
│ │ ├── GetUserQuery.cs
│ │ └── GetUserQueryHandler.cs
│ └── Common/
│ └── Interfaces/
│ └── IApplicationDbContext.cs
│
├── Infrastructure/
│ ├── Persistence/
│ │ ├── ApplicationDbContext.cs
│ │ └── Repositories/
│ │ └── UserRepository.cs
│ └── Services/
│ └── EmailService.cs
│
└── Presentation/
└── Controllers/
└── UsersController.cs
A Camada de Domínio
O coração do sistema. Zero dependências externas.
// Domain/ValueObjects/Email.cs
public record Email
{
public string Value { get; }
private Email(string value) => Value = value;
public static Email Create(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new DomainException("Email não pode ser vazio.");
if (!value.Contains('@'))
throw new DomainException("Email inválido.");
return new Email(value.ToLowerInvariant());
}
public static implicit operator string(Email email) => email.Value;
}
// Domain/Entities/User.cs
public class User : BaseEntity
{
public string Name { get; private set; }
public Email Email { get; private set; }
public DateTime CreatedAt { get; private set; }
private User() { } // EF Core
public static User Create(string name, string email)
{
if (string.IsNullOrWhiteSpace(name))
throw new DomainException("Nome é obrigatório.");
return new User
{
Id = Guid.NewGuid(),
Name = name,
Email = Email.Create(email),
CreatedAt = DateTime.UtcNow
};
}
public void UpdateName(string newName)
{
if (string.IsNullOrWhiteSpace(newName))
throw new DomainException("Nome não pode ser vazio.");
Name = newName;
}
}A Camada de Aplicação (Use Cases)
Orquestra o domínio. Depende apenas do Domain — nunca de Infrastructure.
// Application/Users/Commands/CreateUser/CreateUserCommand.cs
public record CreateUserCommand(string Name, string Email) : IRequest<Guid>;
// Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
{
private readonly IUserRepository _repository;
private readonly IUnitOfWork _unitOfWork;
public CreateUserCommandHandler(
IUserRepository repository,
IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}
public async Task<Guid> Handle(
CreateUserCommand request,
CancellationToken cancellationToken)
{
var existingUser = await _repository.GetByEmailAsync(request.Email);
if (existingUser is not null)
throw new ConflictException("Email já cadastrado.");
var user = User.Create(request.Name, request.Email);
await _repository.AddAsync(user);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return user.Id;
}
}A Interface do Repositório (Domain/Interfaces)
// Domain/Interfaces/IUserRepository.cs
public interface IUserRepository
{
Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<User?> GetByEmailAsync(string email, CancellationToken ct = default);
Task<IEnumerable<User>> GetAllAsync(CancellationToken ct = default);
Task AddAsync(User user, CancellationToken ct = default);
void Update(User user);
void Delete(User user);
}A Implementação em Infrastructure
// Infrastructure/Persistence/Repositories/UserRepository.cs
public class UserRepository : IUserRepository
{
private readonly ApplicationDbContext _context;
public UserRepository(ApplicationDbContext context)
=> _context = context;
public async Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default)
=> await _context.Users.FindAsync([id], ct);
public async Task<User?> GetByEmailAsync(string email, CancellationToken ct = default)
=> await _context.Users
.FirstOrDefaultAsync(u => u.Email.Value == email.ToLower(), ct);
public async Task AddAsync(User user, CancellationToken ct = default)
=> await _context.Users.AddAsync(user, ct);
public void Update(User user)
=> _context.Users.Update(user);
public void Delete(User user)
=> _context.Users.Remove(user);
public async Task<IEnumerable<User>> GetAllAsync(CancellationToken ct = default)
=> await _context.Users.AsNoTracking().ToListAsync(ct);
}O Controller (Presentation)
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly ISender _sender;
public UsersController(ISender sender) => _sender = sender;
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateUserRequest request)
{
var command = new CreateUserCommand(request.Name, request.Email);
var userId = await _sender.Send(command);
return CreatedAtAction(nameof(GetById), new { id = userId }, new { id = userId });
}
}Registrando as Dependências
// Program.cs
builder.Services
.AddDomain()
.AddApplication()
.AddInfrastructure(builder.Configuration);
// Infrastructure/DependencyInjection.cs
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration config)
{
services.AddDbContext<ApplicationDbContext>(opts =>
opts.UseSqlServer(config.GetConnectionString("Default")));
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
return services;
}Por que Vale a Pena?
- Testabilidade: use cases testados com repositórios mock, sem banco real
- Manutenibilidade: trocar EF Core por Dapper? Só muda Infrastructure
- Clareza: ao abrir
Application/Users/Commands, você entende o negócio imediatamente - Onboarding: novos devs entendem onde cada coisa deve ficar
Clean Architecture exige uma curva de aprendizado, mas em projetos que crescem, o investimento se paga rápido.