Durante un tiempo mi forma de encadenar pasos en AWS fue la que sale sola: una Lambda que hace su parte y al terminar invoca a la siguiente, que invoca a la siguiente. Funciona con dos pasos. Con seis, con reintentos, con un paso que a veces tarda diez minutos y con la necesidad de saber en qué punto se quedó un proceso que falló anoche, ese enfoque se convierte en una madeja imposible de depurar. Step Functions cambió eso para mí: saqué la lógica de coordinación del código y la puse en una máquina de estados explícita. Elegí este modelo en varios flujos porque el coste de no saber dónde estaba cada ejecución me superaba; a cambio acepté aprender un lenguaje de definición nuevo y pagar por transición de estado. Este artículo es cuándo compensa y cuándo no.
El problema: la orquestación escondida en el código
Cuando una Lambda invoca a la siguiente, la lógica del proceso —el orden, los reintentos, qué hacer si el paso tres falla— vive repartida entre funciones. No hay un sitio donde leer "así funciona este flujo completo". Para entenderlo abres cinco archivos y reconstruyes el grafo en tu cabeza.
El día que ese proceso falla en producción a mitad de camino, la pregunta clave es "¿en qué paso se quedó y con qué datos?". Con Lambdas encadenadas, la respuesta está enterrada en logs de cinco funciones distintas que tienes que correlacionar a mano. Con un orquestador, la respuesta es una pantalla: ves la ejecución, el estado exacto donde murió y el input que recibió.
La máquina de estados como documentación ejecutable
Lo que más valoro de Step Functions es que la definición del flujo es el diagrama. No hay un doc en Confluence que se desactualiza: la fuente de verdad del proceso es la máquina de estados que corre en producción.
{
"StartAt": "ValidarPedido",
"States": {
"ValidarPedido": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:validar",
"Retry": [{ "ErrorEquals": ["Timeout"], "MaxAttempts": 3 }],
"Next": "CobrarPago"
},
"CobrarPago": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:cobrar",
"Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "Compensar" }],
"Next": "Confirmar"
},
"Compensar": { "Type": "Task", "Resource": "arn:aws:lambda:...:revertir", "End": true },
"Confirmar": { "Type": "Task", "Resource": "arn:aws:lambda:...:confirmar", "End": true }
}
}
Fíjate en dónde vive la política de reintentos y la de compensación: en la definición, declarativa, no repartida por try/catch en cada función. Cada Lambda vuelve a hacer una sola cosa y el orquestador se encarga del resto. Ese es el reparto de responsabilidades que perseguía.
Reintentos y compensación sin escribirlos a mano
El motivo por el que más migro flujos a Step Functions es el manejo de fallos. En Lambdas encadenadas, reintentar con backoff, distinguir errores transitorios de permanentes y compensar pasos ya ejecutados cuando algo falla a mitad es código que escribes, testeas y mantienes tú, mal, en cada función.
En la máquina de estados, Retry y Catch son parte de la definición. Un timeout se reintenta tres veces con backoff exponencial sin que yo escriba un bucle. Y el patrón saga —si el pago falla tras reservar inventario, revierte la reserva— se expresa con un estado de compensación al que saltas con Catch, en vez de con banderas repartidas por medio backend.
💡 La pregunta que uso para decidir: ¿necesito saber en qué paso exacto se quedó una ejecución que falló, semanas después? Si la respuesta es sí, la orquestación explícita se paga sola en el primer incidente.
Dónde NO uso Step Functions
Ser honesto con los trade-offs es lo que separa una decisión de un acto de fe. No meto Step Functions en todo.
Para un flujo síncrono y simple donde el usuario espera una respuesta en la pantalla, un orquestador solo añade latencia y una capa que nadie pidió; una Lambda directa es lo correcto. Tampoco lo uso para procesos de altísimo volumen y baja duración donde cada transición de estado se factura: el modelo Standard cobra por transición y a millones de ejecuciones cortas eso escala en tu factura de formas que sorprenden. Para ese caso miro Express, que cambia el modelo de precio y la garantía de durabilidad, o directamente una cola SQS con un consumidor que hace todo el trabajo de una vez.
Y hay un coste humano: el lenguaje de definición es una cosa más que el equipo tiene que aprender y leer. Si tu flujo tiene dos pasos y nunca crecerá, esa curva no vale la pena.
El precio real que pagué
Lo más caro de adoptar Step Functions no fue el servicio, fue reescribir la mentalidad del equipo. Pasamos de "la lógica está en el código" a "la lógica está en la definición del estado", y durante un tiempo la gente seguía metiendo coordinación dentro de las Lambdas por costumbre, duplicando lo que la máquina ya hacía. La regla que fijé fue clara: las Lambdas hacen trabajo, la máquina de estados decide el orden. Cuando el equipo interiorizó esa frontera, depurar un proceso largo dejó de ser arqueología de logs y pasó a ser mirar un diagrama que te dice exactamente dónde estás. Ese cambio, y no la sintaxis JSON, es lo que hace que valga la pena.