Una web vende entradas para conciertos. Al elegir un asiento, el sistema lo retiene diez minutos mientras pagas. La regla parece clara: «cuando llega el pago, se emite la entrada». Hasta que un día el banco confirma un pago once minutos después. La retención ya había vencido y otra persona había comprado ese mismo asiento.
El código no ayuda mucho. En algún lugar hay algo así:
// Initial code: the rule only knows that a payment arrived
if (payment.isConfirmed()) {
order.setStatus("CONFIRMED");
}
Con este código hay dos personas con entrada para el mismo asiento y un equipo de atención al cliente con una tarde complicada. Lo interesante es que no estamos ante un fallo técnico: el código hace exactamente lo que dice. Lo que falta es conocer una regla del negocio que nadie había escrito.
Domain-Driven Design, o DDD, propone diseñar el software a partir de un modelo del negocio construido junto a las personas que lo conocen. No empieza por elegir carpetas o capas, sino por entender qué decisiones toma el negocio y en qué casos cambian.
Un modelo es una selección útil
Un modelo representa los conceptos y reglas que necesitamos para resolver un problema concreto. No es una copia completa de la realidad.
Pasa lo mismo con un plano de evacuación: muestra pasillos, salidas y extintores, y deja fuera el color de las paredes. No es un plano peor que el del arquitecto; está hecho para otra decisión.
Para confirmar una compra de entradas importan el asiento, cuánto dura su retención y cuándo se confirmó el pago. La biografía del grupo o el mapa del recinto serán importantes en otras partes de la web, pero no cambian esa decisión. Elegir qué dejamos dentro del modelo ya es una decisión de diseño.
El vocabulario de DDD procede del libro de Eric Evans, resumido en su referencia de DDD. Se suele dividir en dos partes. La estratégica trata de las fronteras entre modelos y de cómo colaboran. La táctica ofrece piezas para expresar las reglas dentro de cada frontera: entidades, objetos de valor, agregados, servicios y repositorios. Esta entrega se centra en la idea de partida; las demás piezas tienen su propio artículo.
Un lenguaje compartido que exprese reglas
El lenguaje ubicuo es el vocabulario que negocio y desarrollo usan con el mismo significado, tanto en las conversaciones como en el código. El nombre se debe a que aparece en todas partes: reuniones, documentación, pruebas y clases.
No basta con acordar que algo se llama «reserva». Hay que poder decir qué es, cuándo empieza, cuándo termina y qué ocurre después. Una buena forma de llegar ahí es preguntar por casos reales, especialmente los incómodos:
- ¿Qué pasó la última vez que un pago llegó tarde?
- ¿La retención vence a los diez minutos exactos o hay un margen?
- Si el asiento sigue libre cuando llega el pago tardío, ¿se emite la entrada de todos modos?
- ¿Quién decide si se devuelve el dinero o se ofrece otro asiento?
Las respuestas incompletas también son valiosas. Si nadie sabe qué hacer con un pago tardío, hemos encontrado una decisión pendiente. Es mejor descubrirla en una conversación que en producción, con dos personas discutiendo por la fila 7.
Supongamos que el equipo de ventas responde así: «Un asiento se retiene durante diez minutos. La retención está activa antes de su vencimiento; en el instante exacto del vencimiento ya ha caducado. Si el pago se confirma con la retención activa, se emite la entrada. Si se confirma después, se devuelve el pago, porque el asiento puede haberse vendido».
Esa frase ya contiene vocabulario y reglas. Podemos fijar su traducción al código:
| En el negocio | En el código |
|---|---|
| Retención de asiento | SeatHold |
| Vencimiento de la retención | expiresAt |
| Retención activa en un momento | isActiveAt(moment) |
| Emitir la entrada | ISSUE_TICKET |
| Devolver un pago tardío | REFUND_PAYMENT |
El código está en inglés y la conversación en español, y no pasa nada mientras la correspondencia sea explícita y estable. Lo que no puede ocurrir es que «retención» signifique una cosa en las reuniones y Reservation otra distinta en el código.
Convertir la conversación en código
Primero, la retención. Sabe qué asiento retiene y cuándo vence, y responde a una pregunta:
class SeatHold {
private final String seatCode;
private final Instant expiresAt;
public SeatHold(String seatCode, Instant expiresAt) {
if (seatCode == null || seatCode.trim().isEmpty()) {
throw new IllegalArgumentException("Seat code is required.");
}
if (expiresAt == null) {
throw new IllegalArgumentException("Expiration time is required.");
}
this.seatCode = seatCode;
this.expiresAt = expiresAt;
}
public String getSeatCode() {
return seatCode;
}
public boolean isActiveAt(Instant moment) {
return moment.isBefore(expiresAt);
}
}
Instant representa un instante exacto en el tiempo, sin depender de la zona horaria del equipo. isBefore() devuelve true solo si moment es anterior a expiresAt. En el instante exacto del vencimiento devuelve false, tal como acordó el negocio.
Fíjate en que isActiveAt() recibe el momento como parámetro en lugar de consultar el reloj por su cuenta. Así podemos preguntar por cualquier instante, también por uno del pasado, y comprobar la regla sin esperar diez minutos reales.
Ahora, la decisión. Su resultado tiene dos valores con nombre propio:
enum PurchaseDecision {
ISSUE_TICKET,
REFUND_PAYMENT
}
class PaymentConfirmationPolicy {
public PurchaseDecision decide(SeatHold hold, Instant paymentConfirmedAt) {
if (hold.isActiveAt(paymentConfirmedAt)) {
return PurchaseDecision.ISSUE_TICKET;
}
return PurchaseDecision.REFUND_PAYMENT;
}
}
PaymentConfirmationPolicy se lee casi como la frase del equipo de ventas: si la retención seguía activa cuando se confirmó el pago, se emite la entrada; si no, se devuelve el pago. El método usa el instante en que se confirmó el pago, no el instante en que nuestro sistema procesa la noticia. Esa diferencia también salió de la conversación: si el aviso del banco nos llega con retraso, el cliente no tiene la culpa.
Seguir tres pagos
Creamos una retención que vence a las 20:10:00 y probamos tres momentos de confirmación:
SeatHold hold = new SeatHold("ROW7-SEAT12", Instant.parse("2026-11-14T20:10:00Z"));
PaymentConfirmationPolicy policy = new PaymentConfirmationPolicy();
policy.decide(hold, Instant.parse("2026-11-14T20:09:59Z")); // ISSUE_TICKET
policy.decide(hold, Instant.parse("2026-11-14T20:10:00Z")); // REFUND_PAYMENT
policy.decide(hold, Instant.parse("2026-11-14T20:11:00Z")); // REFUND_PAYMENT
La Z final indica que los instantes están en UTC. El primer pago llega un segundo antes del vencimiento: la retención está activa y se emite la entrada. El segundo llega en el instante exacto, y la regla acordada dice que ya ha caducado. El tercero, el del pago tardío del principio, ahora tiene una respuesta definida en lugar de un asiento duplicado.
Estos tres casos son también las pruebas más útiles de la regla. Si alguien cambia isBefore() por una comparación que incluya el instante exacto, la segunda línea dará otro resultado y lo detectaremos. Es la idea de definir qué significa funcionar bien y comprobarlo con ejemplos concretos.
Hay cosas que este código todavía no hace, y conviene decirlo. No impide que dos personas retengan el mismo asiento a la vez, no guarda nada y no ejecuta la devolución. La política responde a una pregunta: qué hay que hacer con este pago. El resto del recorrido tendrá que coordinarse en otras piezas.
Descubrir hechos, acciones y decisiones
Una forma práctica de explorar un dominio es ordenar en una línea temporal lo que ocurre, con notas y ejemplos. Alguien elige un asiento, el asiento queda retenido, se confirma un pago y el sistema decide qué hacer.

Las flechas representan una secuencia del negocio, no llamadas entre clases. Las piezas tienen naturalezas distintas. «Retener asiento» es una acción que alguien pide. SeatHeld y PaymentConfirmed son hechos, cosas que ya han ocurrido; por eso se nombran en pasado. La última pieza es una decisión, y es donde vive la regla que antes faltaba.
Un hecho como PaymentConfirmed cuenta que el banco cobró. No obliga por sí solo a emitir la entrada: la decisión todavía tiene que comprobar sus condiciones. Confundir «ha ocurrido algo» con «hay que hacer algo» era exactamente el error del código inicial.
Este tipo de ejercicio no necesita herramientas especiales. Lo importante ocurre cuando alguien dice «eso depende» y conseguimos concretar de qué dato, plazo o persona depende.
No todo merece el mismo esfuerzo
Un subdominio es una parte del problema del negocio. En la web de entradas podríamos distinguir varios: la venta y asignación de asientos, la gestión de conciertos y recintos, los pagos y el envío de correos.
No todos son igual de importantes para este negocio. La asignación de asientos y sus reglas, como las retenciones o las preventas, es lo que diferencia a la web de sus competidores: es su subdominio principal. Los pagos son imprescindibles, pero el problema es común a muchos negocios y suele resolverse con un proveedor externo. Enviar correos es un subdominio genérico: hay que hacerlo bien, pero no hace falta inventar nada.
Esta clasificación ayuda a decidir dónde invertir. El subdominio principal merece conversaciones frecuentes, ejemplos cuidadosos y un modelo trabajado. Una pantalla para cambiar el texto de un correo puede resolverse con un formulario y unas validaciones. Aplicar todas las herramientas de DDD a cada formulario es una manera muy eficaz de complicar lo sencillo, la advertencia de KISS aplicada al modelado.
Un glosario no sustituye a los ejemplos
Un glosario ayuda, pero tiene límites. «Retención: bloqueo temporal de un asiento» deja abiertas casi todas las preguntas importantes. «Una retención está activa antes de su vencimiento; un pago confirmado después se devuelve» ya permite discutir casos y escribir comprobaciones.
También conviene conservar el porqué de las reglas. Si el negocio decide algún día emitir la entrada de un pago tardío cuando el asiento sigue libre, eso cambia PaymentConfirmationPolicy y necesita saber si el asiento se ha vendido. Es una regla distinta, con datos distintos, y debería quedar igual de visible que la actual.
El resultado de una buena sesión de modelado puede parecer modesto: tres términos bien definidos, unos cuantos ejemplos límite y una pregunta que alguien tiene que responder. Pero eso es lo que separa un código que adivina de uno que expresa decisiones del negocio. Antes de crear la siguiente clase, merece la pena preguntar qué decide exactamente y a quién habría que preguntarle si mañana cambia.