Software Architect · Módulo 09

La mayoría de los bugs difíciles no vienen de los algoritmos. Vienen de estados implícitos y de las transiciones entre ellos.

State machine · lifecycle · concurrency · consistency

§ 01

Si un objeto tiene ciclo de vida, diséñalo como state machine — no como un montón de flags booleanos.

El estado tiene que ser explícito

Un semáforo se entiende porque tiene un conjunto limitado de estados y transiciones permitidas. Si todas las luces se prendieran a la vez, los conductores discutirían en el cruce.

Order, payment, delivery, subscription, ticket, deployment — son entidades con lifecycle. Tienen estados y transiciones: draft, pending_payment, paid, cancelled, refunded. El trabajo arquitectónico es definir las transiciones permitidas y el owner de cada una.

Cuando aparecen flags como isPaid, isCancelled, isRefunded, isArchived en lugar de una state machine, el sistema llega rápido a combinaciones imposibles: pagado y cancelado a la vez, entregado sin pago, refund sin capture.

La concurrency es parte del modelo

Si dos personas editan un documento al mismo tiempo sin reglas, no gana quien tenga razón — gana el último que guardó.

En un sistema real, varios procesos pueden cambiar el mismo estado: el usuario, un worker, un webhook, un admin, un retry. Hacen falta optimistic locking, un version field, transactions, unique constraints u operaciones compare-and-swap.

El arquitecto está obligado a preguntar: quién puede cambiar este estado, cómo se resuelven las races, qué operaciones son idempotentes y qué cuenta como estado final.

§ 02

Un buen estado se explica con un diagrama. Un mal estado se explica con una lista de excepciones.

Ejemplo: payment lifecycle

Una operación bancaria pasa por etapas. No puedes devolver el dinero primero y decidir después si fue cobrado.

Un payment puede pasar de created a authorized, y luego a captured o voided. Un refund solo es posible después de captured. El webhook del proveedor no escribe campos arbitrarios — dispara una transición que valida el estado actual y el version.

Ese enfoque convierte a los edge cases en parte del modelo, no en un montón de if regados por todo el código.

Antiejemplo: flags en lugar de ciclo de vida

Un formulario con diez checkboxes parece flexible — hasta que dos de ellos se contradicen.

El order tiene paid_at, cancelled_at, failed_at, refunded_at, completed_at, pero ningún status claro. Distintas partes del código interpretan las combinaciones de manera distinta. Soporte ve una cosa, contabilidad otra, el API una tercera.

Eso no es flexibilidad. Es una state machine implícita sin reglas.

Autoevaluación
  • ¿Qué estados tiene esta entidad? - ¿Qué transiciones están prohibidas? - ¿Quién es el owner de la transición: el usuario, el sistema o un proveedor externo? - ¿Cómo nos protegemos contra retries y races?