Saltar al contenido principal
Home
HexagonalGuru
Arquitectura de Software

¿Qué es Domain-Driven Design? Guía Práctica con un Ejemplo Real

Avatar de HexagonalGuru

HexagonalGuru

¿Qué es Domain-Driven Design? Guía Práctica con un Ejemplo Real

En una reunión, el negocio dice «pedido», «cliente» y «factura». En tu código, esas mismas cosas se llaman OrderDTO, UserEntity e InvoiceRow. Cada conversación, cada ticket y cada nueva incorporación al equipo paga un impuesto de traducción — y las reglas de negocio acaban dispersas entre servicios que nadie se atreve a tocar.

Domain-Driven Design (DDD) es la respuesta de Eric Evans a ese problema, descrita en su libro de 2003 Domain-Driven Design: Tackling Complexity in the Heart of Software. No es una arquitectura ni un framework: es una forma de construir software donde el código habla el idioma del negocio y las reglas viven en un solo lugar. Es el complemento natural de la arquitectura hexagonal: aquella te dice dónde poner el dominio, DDD te dice cómo modelarlo.

El Problema: El Modelo Anémico

La mayoría de los codebases que auditamos tienen el mismo síntoma: entidades que son bolsas de getters y setters, y toda la lógica de negocio apilada en clases Service de 2.000 líneas. Es lo que Martin Fowler llamó el Anemic Domain Model.

Las consecuencias son conocidas:

  • Las reglas se duplican. «Un pedido confirmado no se puede modificar» está validado en tres servicios distintos... y en un cuarto no.
  • Nadie sabe dónde vive cada regla. Cambiar una política de descuentos exige arqueología por media docena de archivos.
  • El código miente. Los nombres dicen una cosa y el negocio quiere decir otra. Cada nuevo desarrollador tarda meses en aprender el dialecto local.

DDD Estratégico: Primero, el Mapa

Antes de escribir una sola clase, DDD propone dos herramientas para entender el territorio.

Lenguaje ubicuo

Si el negocio dice «confirmar un pedido», el método se llama confirmar() — no updateStatus(2). El lenguaje ubicuo es el vocabulario compartido que desarrolladores y expertos de negocio usan por igual: en las reuniones, en la documentación y en el código. Cuando una conversación revela que «cliente» y «usuario» son la misma cosa, una de las dos palabras desaparece del código.

Bounded contexts

Aquí está la idea más contraintuitiva de DDD: no existe un modelo único correcto para toda la empresa. Un «cliente» para Ventas es un histórico de oportunidades; para Facturación es una dirección fiscal; para Soporte es una cola de incidencias. Forzar una sola clase Cliente con 40 campos es la receta del acoplamiento.

Un bounded context es un límite explícito dentro del cual un modelo (y su lenguaje) tiene un significado preciso:

Tres bounded contexts — Ventas, Facturación y Soporte — cada uno con su propio modelo de Cliente

Cada contexto evoluciona por su cuenta, y las relaciones entre ellos se mapean de forma explícita (un context map). Esto, además, es lo que delimita los candidatos naturales a microservicio cuando el sistema lo justifica.

DDD Táctico: Los Bloques Constructivos

Dentro de un contexto, DDD aporta un puñado de patrones para modelar con precisión:

  • Entidades. Objetos con identidad propia que persiste en el tiempo: un Pedido es el mismo pedido aunque cambien sus líneas o su estado.
  • Value objects. Valores inmutables sin identidad, definidos por sus atributos: Dinero, Email, Direccion. Dos billetes de 20 € son intercambiables; dos pedidos no.
  • Agregados. Un cluster de entidades y value objects con una única raíz, que es la frontera de consistencia: desde fuera solo se toca el agregado a través de su raíz.
  • Repositorios. Uno por agregado, no por tabla. Persistes y recuperas agregados completos.
  • Eventos de dominio. Algo que ocurrió en el negocio: PedidoConfirmado, FacturaEmitida. Otros contextos (o adaptadores) reaccionan a ellos.
Anatomía de un agregado: Pedido como raíz, líneas de pedido dentro, value objects, y el repositorio tocando solo la raíz

Un Ejemplo Real: El Pedido

Sigamos con el universo de nuestro artículo anterior. Primero, un value object que hace imposible sumar euros con dólares:

final class Dinero
{
    private function __construct(
        public readonly int $centimos,
        public readonly string $moneda,
    ) {}

    public static function euros(float $cantidad): self
    {
        return new self((int) round($cantidad * 100), 'EUR');
    }

    public function sumar(self $otro): self
    {
        if ($this->moneda !== $otro->moneda) {
            throw new MonedasIncompatibles();
        }

        return new self($this->centimos + $otro->centimos, $this->moneda);
    }
}

El agregado protege sus invariantes — es imposible dejarlo en un estado inválido:

final class Pedido
{
    private array $lineas = [];
    private array $eventos = [];

    private function __construct(
        private PedidoId $id,
        private ClienteId $clienteId,
        private EstadoPedido $estado,
    ) {}

    public static function crear(PedidoId $id, ClienteId $clienteId): self
    {
        return new self($id, $clienteId, EstadoPedido::Borrador);
    }

    public function anadirLinea(ProductoId $productoId, Dinero $precio, int $cantidad): void
    {
        if ($this->estado !== EstadoPedido::Borrador) {
            throw new PedidoNoModificable($this->id);
        }

        $this->lineas[] = LineaDePedido::crear($productoId, $precio, $cantidad);
    }

    public function confirmar(): void
    {
        if (count($this->lineas) === 0) {
            throw new PedidoVacio($this->id);
        }

        $this->estado = EstadoPedido::Confirmado;
        $this->eventos[] = new PedidoConfirmado($this->id, $this->total());
    }

    public function total(): Dinero
    {
        return array_reduce(
            $this->lineas,
            fn (Dinero $total, LineaDePedido $linea) => $total->sumar($linea->subtotal()),
            Dinero::euros(0),
        );
    }
}

Fíjate en tres cosas:

  1. El constructor es privado. Solo se nace en un estado válido, a través de crear(). No existe un pedido «a medias».
  2. Las reglas viven dentro. «Un pedido confirmado no se modifica» y «no se confirma un pedido vacío» están en un único sitio — y es imposible saltárselas, porque no hay setters.
  3. El código se lee como el negocio habla. $pedido->confirmar() es exactamente lo que diría un comercial.

Cómo Encaja con la Arquitectura Hexagonal

Los dos enfoques se complementan: la arquitectura hexagonal dibuja la casa y DDD amuebla el centro. El agregado Pedido vive en el núcleo, sin framework. El repositorio RepositorioPedidos es un puerto de salida que Doctrine implementa como adaptador. El caso de uso ConfirmarPedido orquesta: carga el agregado, llama a confirmar(), lo persiste y publica los eventos — y un adaptador de mensajería los reparte a Facturación, al email o a donde toque.

El resultado: la regla «un pedido vacío no se confirma» se prueba en milisegundos, sin base de datos ni HTTP, y suena exactamente igual en el test que en la reunión de negocio.

Qué Ganas

  • Cero impuesto de traducción. Las conversaciones con negocio y el código comparten vocabulario. Los requisitos dejan de perderse en la traducción.
  • Invariantes blindados. Si un estado inválido es irrepresentable, deja de ser un bug posible. Menos validaciones dispersas, menos if defensivos.
  • La complejidad donde importa. Inviertes el esfuerzo de modelado en el core del negocio — lo que te diferencia — y no en el CRUD del catálogo.
  • Límites que escalan. Los bounded contexts son los candidatos naturales a módulos o microservicios cuando el sistema crece.
  • Onboarding real. Un desarrollador nuevo lee el dominio y entiende el negocio, no solo la sintaxis.

Cuándo No Usarlo

La misma honestidad de siempre: DDD cuesta. Exige conversaciones con expertos de negocio, iteración sobre el modelo y disciplina. No merece la pena cuando:

  • El dominio es simple. Un CRUD de catálogo con formularios y listados no necesita agregados ni eventos — necesita un scaffold.
  • No hay acceso al negocio. Sin expertos de dominio con los que construir el lenguaje ubicuo, DDD se degrada en patrones tácticos aplicados por deporte.
  • El proyecto es desechable. En un prototipo de tres semanas, el retorno no llega.

Regla práctica: aplica DDD completo en tu core domain (lo que te hace ganar dinero) y soluciones más simples en los contextos de soporte.

Errores Comunes

  1. DDD en todas partes. Agregados, eventos y repositorios hasta para el CRUD de países. El mapa de contextos existe precisamente para elegir dónde invertir.
  2. Modelo anémico disfrazado. Entidades llenas de setters y toda la lógica en servicios PedidoManager: has pagado el coste y no has cobrado el beneficio.
  3. Obsesión por los primitivos. Todo son string e int: $email, $precio, $moneda sueltos. Los value objects son baratos y eliminan categorías enteras de bugs.
  4. El modelo único global. Una clase Cliente compartida por ventas, facturación y soporte es acoplamiento con buen marketing.
  5. Eventos de dominio como excusa para una cola. No necesitas RabbitMQ el día uno: un bus en memoria o el Messenger de Symfony publica eventos perfectamente. La infraestructura llega cuando el dominio la pide.

En Resumen

DDD no es escribir más clases — es conseguir que el código y el negocio digan lo mismo. Modela el lenguaje, protege los invariantes dentro de los agregados y reserva tu mejor ingeniería para el dominio que te diferencia. Combinado con la arquitectura hexagonal, es la base de cómo construimos software que sobrevive a los años y a los cambios de equipo.

Si tu codebase sufre modelo anémico, reglas duplicadas o miedo a tocar ciertos servicios, una auditoría de software es el mejor punto de partida: mapeamos tus bounded contexts, localizamos la deuda y te entregamos un plan priorizado. Solicita una consultoría gratuita →

¿Quieres profundizar? Próximamente: CQRS — cuándo separar lectura y escritura (y cuándo es puro sobreengineering).

  • #arquitectura-hexagonal
  • #ddd
Artículos relacionados
¿Empezamos?

¿Listo para construir algo que crezca con tu negocio?

Cuéntanos tus objetivos y trazaremos juntos la ruta de diseño, desarrollo y tecnología que te lleve de la visión a resultados medibles.