Agregados como frontera de consistencia
Tres cosas a la vez
Black Friday, las nueve de la mañana. En el mismo segundo, a Ana le llegan tres cosas: un pago con tarjeta de 90 €, la nómina y el recibo de la luz. En el primer diseño de Cuentas, cada una bloquea a «Ana» entera mientras se procesa, y las otras dos esperan su turno.
La nómina y el pago no tienen nada que ver: ni siquiera van a la misma cuenta. ¿Por qué se esperan? Porque alguien decidió que lo que se guarda y se bloquea de una vez es el Cliente, con todas sus cuentas y tarjetas dentro. Esa decisión, qué se cambia junto en una sola transacción, es el diseño de agregados.
Los episodios anteriores trazaban fronteras entre contextos. Este traza la más pequeña: dentro de un contexto, qué cosas tienen que ser consistentes en todo momento, y cuáles pueden ponerse de acuerdo un poco después.
Un agregado o tres
Ana tiene una cuenta nómina (donde entran la nómina y sale el recibo) y una de gastos, con la tarjeta asociada. A la izquierda, un solo agregado: Cliente, con las dos cuentas y la tarjeta dentro. A la derecha, tres agregados: cada cuenta y la tarjeta, referenciadas por id. Cada transacción tarda 400 ms. Lanza las tres operaciones a la vez, con bloqueo pesimista y luego optimista. Después prueba el pago solo, sin saldo, y la transferencia interna.
Qué ha pasado
Qué es un agregado
Un agregado es un grupo de objetos que se cambia de una vez, en una sola transacción, y que protege sus propias reglas. La cuenta de Ana, con sus movimientos y sus retenciones, garantiza que el saldo disponible nunca queda negativo: cualquier cargo o retención pasa por ella, y ella decide si cabe. Desde fuera, nadie toca sus movimientos directamente; se le pide a la cuenta.
Eso lo convierte en una frontera de consistencia: dentro, todo es consistente en todo momento; entre agregados, se acepta que las cosas se pongan de acuerdo un poco después. La pregunta de diseño es dónde poner esa frontera, y la simulación enseña lo que cuesta ponerla demasiado lejos o demasiado cerca.
Demasiado grande: contención
Con el agregado grande, las tres operaciones compiten por el mismo candado.probar en el laboratorio Con bloqueo pesimista se ponen en fila: el recibo espera 800 ms a que acaben el pago y la nómina, aunque no tengan nada que ver con él. Con bloqueo optimista nadie espera, pero es peor de otra forma:probar en el laboratorio las tres leen la misma versión de Ana, la primera en guardar gana y las otras dos fallan por conflicto de versión, releen y repiten el trabajo. Tres conflictos para tres operaciones.
Con agregados pequeños, solo compite lo que toca la misma cuenta: la nómina y el recibo. El pago va por su lado. Tres operaciones, un conflicto. En un banco con millones de clientes y picos como Black Friday, esa diferencia es la que separa un sistema que escala de uno que se atasca en sus propios candados.
Entre agregados: dos pasos, no una transacción
Hay un precio, y la simulación lo enseña sin disimular: con agregados pequeños, el pago tarda más.probar en el laboratorio Necesita el límite diario de la Tarjeta y el saldo de la Cuenta, que ahora son agregados distintos. Tarjetas autoriza en su transacción y le manda a Cuentas el comando de aplicar la retención, que Cuentas procesa en la suya. Son dos pasos, y entre uno y otro el sistema está un momento a medias.
Si Cuentas rechaza la retención porque no hay saldo,probar en el laboratorio la Tarjeta ya había gastado 90 € de su límite diario. Hay que compensar: devolver el límite, con otra transacción. Con el agregado grande habría bastado un rollback. Es exactamente la saga de la serie de eventos (episodio 4), en pequeño: entre agregados se habla con comandos y eventos, y se asume la consistencia eventual.
De ahí la regla clásica: una transacción modifica un solo agregado; lo que afecta a varios se coordina con mensajes.
Romper la regla, a propósito
Y aun así, BCPS Bank la rompe en un sitio.probar en el laboratorio La transferencia interna, de una cuenta de BCPS Bank a otra, carga en origen y abona en destino en una sola transacción: dos agregados a la vez. Es una decisión escrita (un ADR, un registro de decisión de arquitectura) y tiene tres motivos:
- Las dos cuentas viven en el mismo contexto y en la misma base de datos: la transacción es posible y barata.
- La regla que importa, «el dinero nunca queda a medias», abarca las dos cuentas a la vez. No es una regla de una cuenta; es de la operación.
- La alternativa pura, una saga interna (retener en origen, abonar en destino, confirmar), añadiría estados intermedios que Ana vería («100 € retenidos por una transferencia a ti misma») en algo que el banco puede hacer atómico.
Se paga con contención entre operaciones que tocan las mismas cuentas y con riesgo de deadlocks (dos transferencias en sentidos opuestos que se bloquean mutuamente), que se evita bloqueando siempre en el mismo orden. Y la decisión tiene fecha de caducidad escrita: si algún día las cuentas se repartieran entre bases de datos distintas, dejaría de valer y habría que pasar a la saga. Romper una regla está bien si se sabe por qué y hasta cuándo.
Criterio
-
¿Hay una regla que tiene que cumplirse siempre, en todo momento?«El saldo disponible nunca es negativo.» No «el extracto cuadra a final de mes».eso es un agregado
-
¿La regla necesita ver a la vez dos cosas que hoy están separadas?Si no la necesita, no las juntes: consistencia eventual entre ellas.júntalas en uno
-
¿Hay operaciones frecuentes y concurrentes sobre partes independientes del agregado?La nómina y el pago de Ana.pártelo
Y cuatro consejos que se repiten en la literatura de DDD: diseña agregados pequeños; referencia a otros agregados por id, no conteniéndolos; modifica uno por transacción; y usa consistencia eventual (eventos, comandos) fuera de la frontera. Todos se pueden romper, como la transferencia interna, pero con un motivo escrito.
En el mundo real
La referencia sobre tamaño de agregados es la serie de artículos «Effective Aggregate Design» de Vaughn Vernon, de donde salen las reglas del criterio. Evans definió el agregado en su libro de 2003 como la unidad de consistencia y de acceso: solo se llega a lo de dentro a través de la raíz.
En la práctica, el bloqueo optimista con un número de versión es lo más habitual para agregados: JPA/Hibernate lo hace con @Version, y muchas bases de datos de documentos lo ofrecen con escrituras condicionales. En sistemas con Event Sourcing, cada agregado es un flujo de eventos y el conflicto se detecta al añadir un evento con una versión esperada.
Los bancos reales con núcleos modernos suelen tener la cuenta como agregado central, con sus movimientos y retenciones dentro, y tratan como procesos entre agregados todo lo que cruza cuentas de distintos sistemas o entidades. Dentro de una misma base de datos, la transacción que toca dos cuentas sigue siendo lo habitual.
Final de la serie
Hemos bajado del banco entero al lenguaje, del lenguaje a los contextos, de los contextos a sus relaciones y de ahí a lo más pequeño: qué se cambia junto. En cada nivel, la misma idea: las fronteras se ponen donde cambia el significado o la regla, no donde está la tabla. Y en cada nivel, lo que cruza una frontera se habla con mensajes y se asume que llega un poco después, que es de lo que iba la serie de eventos.
Hay preguntas que este episodio ha dejado abiertas a propósito. ¿Qué pasa cuando Ana y Luis, cotitulares de una cuenta, sacan dinero a la vez en dos cajeros? ¿Qué ve una transacción de lo que hace otra mientras tanto? ¿Y cómo se evita el deadlock de dos transferencias cruzadas? Eso es la consistencia, dentro de una base de datos y entre varias.
Siguiente serie · Consistencia y concurrencia Aislamiento, bloqueos y CAP La cuenta conjunta, los niveles de aislamiento, los deadlocks y el cajero sin conexión.