Ir al contenido
wannacode

ADR: registros de decisiones de arquitectura

En resumen: Un ADR (architecture decision record) es un documento corto con el contexto, las opciones, la decisión y las consecuencias; una decisión antigua no se reescribe, se reemplaza por una nueva.

Problema

¿Por qué tenemos una cola?

Una colega nueva abre el código del pago. Las notificaciones pasan por una cola, el correo a veces llega con un minuto de retraso y su consumidor tiene que descartar duplicados. «Esto es complicado. ¿Por qué no llamamos al correo directamente?»

Nadie lo recuerda. Quien lo decidió ya no está en la empresa. La discusión fue en un chat que se borró hace tiempo. Quedan dos malas opciones: conservar algo que no entiendes, o quitarlo y, en el próximo Black Friday, pisar justo el rastrillo por el que se puso.

La idea

Un ADR: la decisión y el porqué

Un ADR (architecture decision record) es un documento corto sobre una decisión. Se escribe cuando se toma la decisión y vive en el repositorio junto al código (docs/adr/0007-notifications-queue.md). Tiene seis partes:

  • título y número: qué se decidió, con un número correlativo;
  • estado: propuesto, aceptado, reemplazado;
  • contexto: qué es cierto ahora, qué duele y qué limita;
  • opciones: al menos dos que de verdad se consideraron;
  • decisión: qué opción y por qué;
  • consecuencias: qué se vuelve más fácil y qué se vuelve más difícil.

Este es el ADR que la colega buscaba:

# ADR-0007: Las notificaciones pasan por una cola
## Estado
Aceptado. Reemplaza a ADR-0004.
## Contexto
ADR-0004: el pago llama al correo directamente. En Black Friday el
proveedor de correo respondió en 8 segundos y los pagos caducaron.
## Opciones
1. Una llamada directa con un timeout corto: el pago pasa, el correo se pierde.
2. Una cola: el pago publica un evento y las notificaciones lo leen después.
## Decisión
Una cola. Un correo perdido de un pedido pagado es peor que uno tardío.
## Consecuencias
- El pago no espera al correo.
- El correo puede llegar un minuto después.
- La entrega es «al menos una vez», así que el consumidor es idempotente.
- Una pieza más que vigilar: la cola.

La segunda mitad es la más valiosa: le dice a la colega por qué hay duplicados y qué se rompe si la cola cae. La cola y el consumidor idempotente son de la lección sobre eventos.

La idea

Los estados y cambiar una decisión

Un ADR pasa por unos cuantos estados:

  • propuesto (proposed): está escrito y se discute, normalmente en un pull request;
  • aceptado (accepted): desde hoy esta es la regla;
  • reemplazado (superseded): un ADR más nuevo lo cambió, y el antiguo apunta a él;
  • obsoleto (deprecated): ya no vale, y nada lo reemplaza.

Un ADR aceptado no se reescribe. Cuando la decisión deja de ser correcta, escribes un ADR nuevo que reemplaza al antiguo, y le das al antiguo el estado «reemplazado por ADR-0007». El antiguo era correcto en su contexto: si lo borras, pierdes por qué se eligió y qué pasó después.

Míralo

Una decisión, sus consecuencias, una decisión nueva

STATUSES = {"proposed", "accepted", "superseded", "deprecated"}
def check(adrs):
everything = by_number(adrs)
bad = []
for a in adrs:
n = a.name
if a.status not in STATUSES:
bad.append(f"{n}: estado desconocido {a.status}")
if not a.context:
bad.append(f"{n}: sin contexto")
if len(a.options) < 2:
bad.append(f"{n}: menos de dos opciones")
if not a.decision:
bad.append(f"{n}: sin decisión")
if not a.consequences:
bad.append(f"{n}: sin consecuencias")
if a.status == "superseded":
nxt = everything.get(a.superseded_by)
if nxt is None:
bad.append(f"{n}: reemplazado, pero no dice por cuál")
elif nxt.status != "accepted":
bad.append(f"{n} → {nxt.name}: el reemplazo no está aceptado")
return bad
Paso a paso, en palabras
  1. ADR-0004 se propone: el contexto, dos opciones y la decisión. El pago llama al correo directamente.
  2. El equipo lo acepta. El precio también queda escrito: un correo lento frena el pago.
  3. Black Friday: el proveedor de correo responde en 8 segundos y los pagos caducan. La consecuencia escrita se cumple.
  4. Una decisión nueva, ADR-0007: las notificaciones pasan por una cola. Su contexto apunta a ADR-0004.
  5. Las opciones y el precio: el pago ya no espera, pero un correo puede llegar más tarde, y dos veces.
  6. ADR-0007 se acepta. El antiguo no se borra ni se reescribe: su estado pasa a «reemplazado» y apunta al nuevo.
  7. La comprobación de la lección: un ADR reemplazado debe apuntar a uno aceptado. ADR-0004 → ADR-0007 pasa.
  8. Quien pregunta «¿por qué una cola?» lee ADR-0007 y, desde ahí, ADR-0004. La historia queda, con las opciones y el precio.

Míralo

Comprobar los ADR

En un proyecto real los ADR son archivos markdown, y la comprobación lee sus encabezados. Aquí cada ADR es un registro con los mismos campos, para que el programa sea corto:

@dataclass
class Adr:
number: int
title: str
status: str
context: str
options: list
decision: str
consequences: str
superseded_by: int | None = None
@property
def name(self):
return f"ADR-{self.number:04d}"
adr4 = Adr(
number=4,
title="Las notificaciones se envían de forma síncrona",
status="superseded",
context="Primera versión: el correo es rápido",
options=["llamada directa", "cola"],
decision="El pago llama al correo directamente",
consequences="Simple, pero un correo lento frena el pago",
superseded_by=7,
)
adr7 = Adr(
number=7,
title="Las notificaciones pasan por una cola",
status="accepted",
context="ADR-0004: el correo frena los pagos",
options=["directo con timeout", "cola"],
decision="El pago pone un evento en la cola",
consequences="No espera; un correo puede llegar dos veces",
superseded_by=None,
)

La comprobación vigila las dos cosas que se olvidan: las partes de un ADR y la cadena «reemplazado por».

def by_number(adrs):
return {a.number: a for a in adrs}
STATUSES = {"proposed", "accepted", "superseded", "deprecated"}
def check(adrs):
everything = by_number(adrs)
bad = []
for a in adrs:
n = a.name
if a.status not in STATUSES:
bad.append(f"{n}: estado desconocido {a.status}")
if not a.context:
bad.append(f"{n}: sin contexto")
if len(a.options) < 2:
bad.append(f"{n}: menos de dos opciones")
if not a.decision:
bad.append(f"{n}: sin decisión")
if not a.consequences:
bad.append(f"{n}: sin consecuencias")
if a.status == "superseded":
nxt = everything.get(a.superseded_by)
if nxt is None:
bad.append(f"{n}: reemplazado, pero no dice por cuál")
elif nxt.status != "accepted":
bad.append(f"{n} → {nxt.name}: el reemplazo no está aceptado")
return bad

ADR-0005 está marcado como reemplazado, pero no dice por cuál. ADR-0009 tiene una opción y ninguna consecuencia:

adr5 = Adr(5, "El descuento se calcula en el controlador", "superseded", "Una pantalla, una regla",
["en el controlador", "en el dominio"], "La regla está en el controlador", "Rápido, pero la regla se duplica", None)
adr9 = Adr(9, "Caché para las páginas de productos", "proposed", "La base de datos está al 100 % en las campañas",
["Redis"], "Cache-aside con TTL de 5 minutos", "", None)
problems = check([adr4, adr5, adr7, adr9])
for p in problems:
print(p)
print(len(problems), "problemas")
Salida
ADR-0005: reemplazado, pero no dice por cuál
ADR-0009: menos de dos opciones
ADR-0009: sin consecuencias
3 problemas

Una comprobación así corre con las pruebas en cada pull request, y un ADR sin consecuencias nunca llega a la rama principal. superseded_by: int | None = None dice que el campo es un número o None.

Pruébalo

Predice la salida

La comprobación de arriba recibe dos ADR sobre los informes. ¿Qué imprime?

adr11 = Adr(11, "Los informes leen la base principal", "superseded", "Hay pocos informes",
["la principal", "una réplica"], "Los informes leen la principal", "Simple, pero carga la base de datos", 12)
adr12 = Adr(12, "Los informes leen una réplica", "proposed", "Los informes frenan los pagos",
["una réplica"], "Los informes leen una réplica", "Los datos tienen un minuto de retraso", None)
for p in check([adr11, adr12]):
print(p)
Respuesta

Míralo

Por qué una cola: a lo largo de la cadena

La colega parte de ADR-0004, que aparece en discusiones antiguas, y llega a la decisión vigente:

def history(everything, number):
a = everything[number]
print(f"{a.name} {a.title} [{a.status}]")
while a.superseded_by is not None:
a = everything[a.superseded_by]
print(f"→ {a.name} {a.title} [{a.status}]")
# ¿por qué las notificaciones pasan por una cola?
history(by_number([adr4, adr7]), 4)
Salida
ADR-0004 Las notificaciones se envían de forma síncrona [superseded]
→ ADR-0007 Las notificaciones pasan por una cola [accepted]

Dos minutos de lectura en vez de una semana de adivinar. Si el contexto de ADR-0007 deja de ser cierto (los correos van a otro sistema que responde rápido), la colega escribe ADR-0015, que lo reemplaza, y la cadena crece.

Pruébalo

Encuentra el error

Un compañero propone un ADR para la caché de la lección sobre caching. ¿Qué línea hace que el ADR sea inútil para la siguiente persona?

Elige la línea con el error.

La idea

Cuándo ayuda un ADR y cuándo es burocracia

  • Ayuda en decisiones caras de cambiar (una cola, una base de datos, los límites de los módulos, un formato de API) y con más de una respuesta razonable.
  • Ayuda cuando se escribe antes de la decisión: las opciones y el precio salen mientras aún puedes elegir otra cosa.
  • Perjudica cuando cada cosa pequeña recibe un ADR. El nombre de una clase, o una biblioteca que cambias en una hora, no es una decisión de arquitectura.
  • Perjudica cuando se escribe después para justificar lo que ya está hecho: entonces las opciones son inventadas y las consecuencias son todas buenas.
  • Es inútil cuando nadie lo encuentra. Pon el número del ADR en un comentario en el lugar del código y en el pull request.
Más precisamente, para curiosos

De dónde viene. El formato es de Michael Nygard (2011). Las plantillas populares son la suya y MADR (Markdown Any Decision Records), que añade «ventajas y desventajas de cada opción».

Herramientas. adr-tools crea un archivo nuevo con el número siguiente y cambia los estados; log4brains construye un sitio a partir de los ADR. Una comprobación como la de esta lección se escribe en una tarde y corre con las pruebas.

ADR, RFC y design doc. Un design doc describe un sistema entero o un cambio grande antes de construirlo. Un RFC es una propuesta para que la discutan muchas personas. Un ADR es una decisión, corta, y se queda como historia. Un design doc suele terminar en unos cuantos ADR.

Quién los escribe. No solo el arquitecto. Quien propone el cambio escribe el ADR en el pull request, y la revisión de código se convierte también en una revisión de la decisión.

La idea

Qué forma y cuándo

El módulo mostró nueve maneras de organizar un sistema. Ninguna es buena por sí sola: cada una resuelve un dolor y trae un precio.

Forma Ayuda cuando Perjudica cuando
Capas las reglas deben probarse sin base de datos y sin pantalla la aplicación son solo formularios sobre tablas y las capas solo pasan llamadas
Hexagonal el dominio vive mucho, mientras el pago y la base de datos cambian cada clase tiene una interfaz sin segunda implementación
Monolito modular uno o pocos equipos, un deploy no se respetan los límites y los módulos meten la mano en las tablas de otros módulos
Servicios equipos independientes, escala distinta, deploys separados un solo equipo: la red, los datos y el monitoreo cuestan más de lo que dan
Eventos y colas una consecuencia lenta no debe frenar la acción principal la respuesta se necesita enseguida; los duplicados y el orden se resuelven a mano
Outbox un evento no debe perderse en una caída no hay cola: basta una llamada en el mismo proceso
Contrato de API clientes que no puedes actualizar (la app móvil) código en un solo proceso que cambia en conjunto
Caché se lee mucho lo mismo y puede estar un poco viejo el valor debe ser exacto o no se puede invalidar
Escalado un servidor no basta o no debe ser el único basta un servidor y el cuello de botella es la base de datos

Primero el monolito. Un sistema nuevo empieza como un monolito modular con límites claros. Un servicio se separa cuando hay una razón concreta: un equipo aparte, otra escala, deploys independientes. La razón va a un ADR, junto con el precio. Lo contrario (servicios desde el primer día) paga el precio de un sistema distribuido antes de que nadie sepa dónde están los límites reales.

Ejercicio

El contenido de la carpeta

Resuélvelo en tu editor.

La carpeta docs/adr/ crece y nadie recuerda qué ADR están vigentes. Escribe toc: imprime todos los ADR ordenados por número con su estado, y en uno reemplazado también el número de su reemplazo. Pásale ADR-0007, ADR-0001, ADR-0009 y ADR-0004 en ese orden.

Salida
ADR-0001 Monolito modular en vez de servicios [accepted]
ADR-0004 Las notificaciones se envían de forma síncrona [superseded → ADR-0007]
ADR-0007 Las notificaciones pasan por una cola [accepted]
ADR-0009 Caché para las páginas de productos [proposed]

Más tareas

  • Los vigentes. Imprime solo los ADR aceptados: eso es «la arquitectura de hoy».
  • Un ciclo. ¿Qué le pasa a history si ADR-0004 está reemplazado por 0007 y 0007 por 0004? Añade una comprobación que encuentre un ciclo así.

Test

Ponte a prueba

1. ¿Cuál de estas NO es una parte obligatoria de un ADR?
2. Un ADR aceptado ya no es cierto. ¿Qué haces?
3. La sección «Consecuencias» dice solo «todo será más rápido». ¿Cuál es el problema?
4. ¿Cuándo es un ADR burocracia innecesaria?
5. Una tienda con un equipo de cinco personas empieza un sistema nuevo. ¿Con qué debería empezar?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·