Saltar al contenido principal

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.