Qué es una máquina de estados

Una máquina de estados define un conjunto finito de estados, las transiciones permitidas entre ellos y los eventos que activan dichas transiciones. En cualquier momento, el sistema se encuentra en exactamente un estado; solo las transiciones declaradas pueden llevarlo a otro. Todo lo demás queda prohibido por diseño.

Cómo ayudan

  • Hacen explícitas las reglas: los estados y las transiciones están escritos en el código en lugar de darse por supuestos, así que los casos límite quedan a la vista en vez de esconderse en condicionales.
  • Impiden saltos no permitidos: las transiciones imposibles (por ejemplo, «enviar» antes de «pagado») sencillamente no existen, lo que elimina categorías enteras de errores.
  • Mejoran la observabilidad: el estado actual y las transiciones recientes forman una línea de tiempo que se puede registrar, auditar y vigilar con alertas.
  • Simplifican las pruebas: cada transición es un pequeño contrato que se puede probar, y los fixtures se centran en los eventos y en el siguiente estado esperado.
  • Facilitan los reintentos y la idempotencia: las transiciones se disparan por eventos con precondiciones claras, lo que permite reintentar sin riesgo y descartar duplicados.
  • Acotan la concurrencia: con un único estado de referencia y transiciones protegidas, las condiciones de carrera se reducen a puntos de conflicto bien definidos.
  • Facilitan la marcha atrás: revertir es simplemente otra transición (hacia un estado anterior o de error) en lugar de retocar flags sobre la marcha.
  • Dan un vocabulario común: un diagrama o una tabla compartida de estados y transiciones permite que producto, QA e ingeniería hablen de lo mismo.

Cuándo usar una

  • Flujos de trabajo con aprobaciones, pagos, aprovisionamiento o envíos donde el orden importa.
  • Procesos de larga duración con reintentos, tiempos de espera (timeouts) y callbacks o webhooks externos.
  • Sistemas con muchas rutas de error que necesitan recuperarse de forma ordenada (por ejemplo, pagos, facturación o alta de clientes).
  • Procesos con múltiples actores (cliente, back office, proveedor externo) en los que hay que impedir saltos no permitidos.

Directrices de diseño

  • Mantenga pocos estados: es mejor un puñado de estados con significado que docenas de microestados. Guarde los detalles como datos o flags.
  • Dispare las transiciones con eventos y nómbrelos según hechos del negocio (por ejemplo, payment_captured, document_approved).
  • Centralice las condiciones de guarda (guards): cada transición tiene precondiciones y, si no se cumplen, se rechaza con un motivo claro.
  • Modele el fallo de forma explícita: añada estados de error finales o ciclos de recuperación, no operaciones que no hacen nada en silencio.
  • Separe el estado de los efectos secundarios: calcule primero el siguiente estado y ejecute los efectos (correos, tareas) después de guardarlo.
  • Registre las transiciones: incluya el actor, la marca de tiempo, el estado anterior, el siguiente, el evento y el ID de correlación.
  • Versione la máquina: si cambia estados o transiciones, versione o migre con cuidado las instancias ya guardadas.

Errores habituales

  • Explosión de estados al modelar cada subcaso como un nuevo estado en lugar de datos.
  • Transiciones ocultas en condicionales sueltos fuera de la máquina.
  • Efectos secundarios dentro de las funciones de transición que pueden aplicarse parcialmente en caso de fallo.
  • Omitir la idempotencia: los eventos duplicados deben ignorarse o tratarse como reintentos seguros.
  • Ignorar el tiempo: no gestionar los tiempos de espera o caducidades deja elementos bloqueados.
  • Falta de dueño: no está claro qué servicio o componente es la fuente de verdad del estado.

Lista de comprobación para implementarla

  • Enumere los estados y los eventos, y rechace todo lo que no esté en la lista.
  • Defina las transiciones permitidas como una tabla o un grafo y bloquee el resto.
  • Especifique las guardas y los efectos de cada transición, y haga que los efectos se puedan reintentar.
  • Guarde el estado de forma atómica junto con el evento que lo provocó o su ID de correlación.
  • Haga que las transiciones sean idempotentes y gestione duplicados y eventos fuera de orden.
  • Exponga el estado y el historial para la observabilidad y las auditorías.
  • Añada métricas: recuentos por estado, transiciones correctas y fallidas, y tiempo en cada estado.

Ejemplo de pseudocódigo (flujo de pedidos)

Este ejemplo es independiente del lenguaje y mantiene claras las responsabilidades: define estados/eventos, declara transiciones permitidas con protecciones y efectos secundarios opcionales, y después un pequeño manejador que hace cumplir la tabla.

Estados: CREATED, PAID, PACKING, SHIPPED, FAILED.

Eventos: pay, pack, ship, fail.

Tabla de transiciones (con guardas y efectos secundarios):

machine = {
  CREATED: {
    pay(event) -> PAID if event.valid_payment
    fail(reason) -> FAILED
  },
  PAID: {
    pack(event) -> PACKING
    fail(reason) -> FAILED
  },
  PACKING: {
    ship(trackingId) -> SHIPPED with sideEffect(sendEmail(trackingId))
    fail(reason) -> FAILED
  },
  SHIPPED: {},
  FAILED: {}
}

Manejador (aplica la tabla y las guardas, guarda el estado y después ejecuta los efectos secundarios):

  • Busca la transición permitida para el estado actual y el evento entrante.
  • Rechaza el evento si no existe ninguna transición o si falla su guarda.
  • Aplica la transición para obtener el siguiente estado y cualquier efecto secundario.
  • Guarda el cambio de estado con los datos del evento y luego ejecuta los efectos secundarios.
  • Devuelve el nuevo estado.
state = CREATED

function handle(event):
  current = state
  transition = machine[current][event.type]
  if not transition:
    return error("illegal transition")

  if transition.hasGuard and not transition.guard(event):
    return error("guard failed")

  nextState, sideEffects = transition.apply(event)
  persist(currentState=current, nextState=nextState, event=event)
  for effect in sideEffects:
    effect.run()
  state = nextState
  return ok(state)

Notas:

  • Las guardas se evalúan antes de cambiar el estado; si una falla, el estado no cambia.
  • Guarde el estado (y el evento o ID de correlación) antes de ejecutar los efectos secundarios, para que los reintentos sean seguros.
  • Los efectos secundarios deben poder reintentarse o ser idempotentes. Registre cada transición para auditorías y alertas.
  • Para añadir tiempos de espera, modélelos como eventos y transiciones (por ejemplo, timeout -> FAILED).