Clean Architecture & DDD π΄
Clean Architectureβ
Propuesta por Robert C. Martin ("Uncle Bob"). Las capas internas no conocen a las externas.
βββββββββββββββββββββββββββββββββββββββ
β Frameworks & Drivers β Web, DB, External APIs
β βββββββββββββββββββββββββββββββββ β
β β Interface Adapters β β Controllers, Presenters, Gateways
β β βββββββββββββββββββββββββββ β β
β β β Application Layer β β β Use Cases
β β β βββββββββββββββββββββ β β β
β β β β Domain/Entities β β β β Business Rules
β β β βββββββββββββββββββββ β β β
β β βββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββ
Estructura de proyectoβ
src/
βββ MiApp.Domain/ β Sin dependencias externas
β βββ Entities/
β βββ ValueObjects/
β βββ Interfaces/ β IRepository (definiciΓ³n)
β βββ DomainEvents/
β
βββ MiApp.Application/ β Depende solo de Domain
β βββ Commands/
β βββ Queries/
β βββ DTOs/
β βββ Interfaces/ β IEmailService (definiciΓ³n)
β
βββ MiApp.Infrastructure/ β Implementa interfaces de Domain y Application
β βββ Persistence/ β EF Core, Repositories concretos
β βββ Services/ β EmailService, JwtService
β βββ External/
β
βββ MiApp.API/ β Punto de entrada, inyecciΓ³n de dependencias
βββ Controllers/
βββ Middleware/
βββ Program.cs
Domain-Driven Design (DDD)β
Entities y Value Objectsβ
// Entity: tiene identidad (Id), puede cambiar de estado
public class Pedido
{
public PedidoId Id { get; private set; }
public ClienteId ClienteId { get; private set; }
public EstadoPedido Estado { get; private set; }
private readonly List<PedidoItem> _items = new();
public IReadOnlyCollection<PedidoItem> Items => _items.AsReadOnly();
// Constructor privado β solo se crea mediante factory method
private Pedido(ClienteId clienteId)
{
Id = new PedidoId(Guid.NewGuid());
ClienteId = clienteId;
Estado = EstadoPedido.Pendiente;
}
// Factory method β valida invariantes del negocio
public static Pedido Crear(ClienteId clienteId)
{
if (clienteId is null) throw new ArgumentNullException(nameof(clienteId));
return new Pedido(clienteId);
}
// MΓ©todos de dominio β las reglas de negocio viven aquΓ
public void AgregarItem(ProductoId productoId, int cantidad, Dinero precio)
{
if (Estado != EstadoPedido.Pendiente)
throw new DomainException("Solo se pueden agregar items a pedidos pendientes");
var itemExistente = _items.FirstOrDefault(i => i.ProductoId == productoId);
if (itemExistente is not null)
itemExistente.IncrementarCantidad(cantidad);
else
_items.Add(new PedidoItem(productoId, cantidad, precio));
}
public void Confirmar()
{
if (!_items.Any())
throw new DomainException("No se puede confirmar un pedido sin items");
Estado = EstadoPedido.Confirmado;
AddDomainEvent(new PedidoConfirmadoEvent(Id));
}
}
// Value Object: igualdad por valor, inmutable
public record Dinero(decimal Monto, string Moneda)
{
public static Dinero Cero(string moneda) => new(0, moneda);
public Dinero Sumar(Dinero otro)
{
if (Moneda != otro.Moneda)
throw new DomainException("No se pueden sumar monedas distintas");
return new Dinero(Monto + otro.Monto, Moneda);
}
public bool EsPositivo() => Monto > 0;
}
// Strongly Typed IDs (evitan confundir IDs de distintas entidades)
public record PedidoId(Guid Value);
public record ClienteId(Guid Value);
public record ProductoId(int Value);
Aggregates y Aggregate Rootsβ
// Un Aggregate es un cluster de objetos del dominio tratados como unidad.
// El Aggregate Root es el ΓΊnico punto de entrada.
// Pedido es el Aggregate Root
// PedidoItem solo se accede a travΓ©s de Pedido
// Nunca: _context.PedidoItems.Add(item) directamente
// Repositorios solo para Aggregate Roots
public interface IPedidoRepository
{
Task<Pedido?> ObtenerPorIdAsync(PedidoId id);
Task GuardarAsync(Pedido pedido);
}
Domain Eventsβ
public abstract class Entity
{
private List<IDomainEvent> _domainEvents = new();
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(IDomainEvent evt) => _domainEvents.Add(evt);
public void ClearDomainEvents() => _domainEvents.Clear();
}
// El repositorio publica los eventos al guardar
public class PedidoRepository : IPedidoRepository
{
public async Task GuardarAsync(Pedido pedido)
{
_context.Pedidos.Update(pedido);
// Publicar domain events DESPUΓS de guardar exitosamente
foreach (var evento in pedido.DomainEvents)
await _publisher.Publish(evento);
pedido.ClearDomainEvents();
await _context.SaveChangesAsync();
}
}
Specification Patternβ
public abstract class Specification<T>
{
public abstract Expression<Func<T, bool>> ToExpression();
public bool EsSatisfechaPor(T entidad) =>
ToExpression().Compile()(entidad);
public Specification<T> Y(Specification<T> otra) =>
new AndSpecification<T>(this, otra);
}
public class ProductosActivosSpec : Specification<Producto>
{
public override Expression<Func<Producto, bool>> ToExpression() =>
p => p.Activo && p.Stock > 0;
}
public class ProductosPorCategoriaSpec : Specification<Producto>
{
private readonly int _categoriaId;
public ProductosPorCategoriaSpec(int categoriaId) => _categoriaId = categoriaId;
public override Expression<Func<Producto, bool>> ToExpression() =>
p => p.CategoriaId == _categoriaId;
}
// Uso
var spec = new ProductosActivosSpec().Y(new ProductosPorCategoriaSpec(1));
var productos = await _repo.ListarAsync(spec);
Preguntas frecuentes de entrevista π―β
1. ΒΏCuΓ‘l es la diferencia entre DDD y Clean Architecture?
No son lo mismo. DDD es una forma de modelar el dominio del negocio (Entities, Value Objects, Aggregates, Bounded Contexts). Clean Architecture es una forma de estructurar el cΓ³digo en capas con la regla de dependencias. Se complementan muy bien.
2. ΒΏQuΓ© es un Bounded Context?
Un lΓmite explΓcito dentro del cual un modelo de dominio particular es vΓ‘lido. Por ejemplo, "Producto" en el contexto de CatΓ‘logo tiene nombre, descripciΓ³n, precio. En el contexto de Inventario, "Producto" tiene SKU, ubicaciΓ³n, stock. Son modelos distintos del mismo concepto.
3. ΒΏCuΓ‘ndo aplicarΓas DDD completo y cuΓ‘ndo no?
DDD completo vale la pena en dominios complejos con muchas reglas de negocio. Para CRUD simple o dominios tΓ©cnicos (logs, configuraciones), es overengineering. TΓ‘cticas de DDD (Entities, Value Objects) siempre; el diseΓ±o estratΓ©gico completo solo cuando el dominio lo justifica.
4. ΒΏCΓ³mo manejas la consistencia entre Aggregates?
Los Aggregates no se modifican juntos en una transacciΓ³n. Se usa eventual consistency mediante Domain Events. El Aggregate A publica un evento, y el handler actualiza el Aggregate B en una transacciΓ³n separada.