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 moverlo a otro lugar. Todo lo demás es inválido por diseño.
Cómo ayudan
- Hacen las reglas explícitas: Los estados y las transiciones están codificados en lugar de estar implícitos, por lo que los casos de borde son visibles en lugar de estar ocultos en condicionales.
- Previenen movimientos ilegales: Las transiciones imposibles (por ejemplo, "enviar" antes de "pagado") simplemente no existen, eliminando enteras clases de errores.
- Mejoran la observabilidad: El estado actual y las transiciones recientes actúan como una línea de tiempo que se puede registrar, auditar y monitorizar mediante alertas.
- Simplifican las pruebas: Cada transición es un pequeño contrato a probar; los fixtures se centran en los eventos y los siguientes estados esperados.
- Gestionan reintentos e idempotencia: Las transiciones se basan en eventos con precondiciones claras, lo que facilita los reintentos seguros y la supresión de duplicados.
- Dominan la concurrencia: Con un único estado de autoridad y transiciones protegidas, las condiciones de carrera se reducen a puntos de conflicto bien definidos.
- Facilitan las reversiones: Revertir es simplemente otra transición (hacia un estado anterior o de error) en lugar de una cirugía de banderas (flags) ad-hoc.
- Alinean a los equipos: Un diagrama o tabla compartida de estados y transiciones proporciona al producto, al QA y a la ingeniería el mismo vocabulario.
Cuándo recurrir a 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 una recuperación elegante (por ejemplo, pagos, facturación, incorporación de usuarios).
- Procesos con múltiples actores (cliente, oficina central, proveedor externo) donde se necesite evitar saltos ilegales.
Directrices de diseño
- Mantén los estados generales: prefiere un puñado de estados significativos en lugar de docenas de microestados. Codifica los detalles como datos o banderas.
- Haz que las transiciones se basen en eventos: nombra los eventos según hechos del negocio (por ejemplo,
payment_captured,document_approved). - Centraliza las protecciones (guards): cada transición tiene precondiciones; recházala con una razón clara cuando no se cumplan.
- Modela el fallo de forma explícita: añade estados de error terminales o bucles de recuperación, no operaciones nulas silenciosas.
- Separa el estado de los efectos secundarios: calcula el siguiente estado primero; ejecuta los efectos (correos electrónicos, trabajos) después de que el estado se persista.
- Registra las transiciones: incluye el actor, la marca de tiempo, el estado anterior, el siguiente estado, el evento e ID de correlación.
- Versiona la máquina: si cambias estados o transiciones, versiona o migra las instancias persistidas de forma segura.
Errores comunes
- Explosión de estados al modelar cada subcaso como un nuevo estado en lugar de datos.
- Transiciones ocultas enterradas en condicionales ad-hoc 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 responsabilidad: no está claro qué servicio o componente es la fuente de verdad del estado.
Lista de verificación de implementación
- Enumera los estados y eventos; rechaza cualquier cosa que no esté en la lista.
- Define las transiciones permitidas como una tabla o grafo; bloquea el resto.
- Especifica las protecciones y efectos por transición; mantén los efectos reintentables.
- Persiste el estado de forma atómica con el evento activador o el ID de correlación.
- Haz que las transiciones sean idempotentes; gestiona duplicados y eventos fuera de orden.
- Expón el estado + el historial para observabilidad y auditorías.
- Añade métricas: recuentos por estado, éxito/fallo de transiciones, tiempo en 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 luego ejecuta un pequeño manejador que impone la tabla.
Estados: CREATED, PAID, PACKING, SHIPPED, FAILED.
Eventos: pay, pack, ship, fail.
Tabla de transiciones (protecciones y efectos secundarios en línea):
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 (impone la tabla, protecciones, persistencia y luego efectos secundarios):
- Busca la transición permitida para el estado actual y el evento entrante.
- Rechaza si no existe ninguna transición o si falla su protección.
- Aplica la transición para obtener el siguiente estado y cualquier efecto secundario.
- Persiste el cambio de estado con los datos del evento, 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 protecciones se ejecutan antes de cambiar el estado; si una protección falla, el estado permanece intacto.
- Persiste el estado (y el evento/ID de correlación) antes de ejecutar los efectos secundarios para que los reintentos sean seguros.
- Los efectos secundarios deben ser reintentables o idempotentes; registra cada transición para auditorías y alertas.
- Para añadir tiempos de espera, modélalos como eventos y transiciones (por ejemplo,
timeout -> FAILED).
