Todo lo que hemos montado en el módulo es coreografía: cada componente reacciona a lo que ve y nadie tiene la foto completa. Funciona espléndidamente para hechos independientes —enviar un correo, cargar analítica, generar una miniatura— y se rompe en cuanto los pasos dependen unos de otros y hay que deshacer lo hecho si algo falla a mitad.
El proceso de pedido de MercadoFresco es exactamente ese caso: cobrar la tarjeta, reservar el stock, pedir al almacén que prepare la caja —que tarda entre dos minutos y una hora y la confirma una persona con un lector de códigos—, asignar reparto y confirmar al cliente. Si el pescado llega en mal estado y el almacén no puede preparar el pedido, hay que devolver el cobro y liberar el stock. Con eventos sueltos nadie sabe en qué punto estaba el proceso ni qué toca compensar; la única forma es una tabla de estado escrita a mano, un proceso que la vigile y un montón de casos raros que nunca se prueban.
AWS Step Functions es el orquestador sin servidor de AWS. Defines el proceso como una máquina de
estados en JSON, y el servicio se encarga de ejecutar cada paso, guardar el estado entre pasos,
reintentar con retroceso exponencial, capturar errores, esperar horas o días si hace falta, y dejar un
historial completo de cada ejecución. Aquí Luis construye mercadofresco-procesar-pedido con su camino
de compensación completo.
Aviso de coste. Los flujos estándar cuestan 0,025 USD por 1.000 transiciones de estado; los exprés, por número de ejecuciones y por GB-segundo. Un bucle mal diseñado con
Waitcorto puede generar cientos de miles de transiciones en una noche. Al final tienes la limpieza. Datos ficticios.
Contenido
- Coreografía frente a orquestación
- Flujos estándar y flujos exprés
- Amazon States Language: la estructura
- Los ocho tipos de estado
- Flujo de datos entre estados
- Integraciones y patrones de servicio
- El token de retorno para el paso humano del almacén
- Manejo de errores:
RetryyCatch - El patrón saga: compensar lo que ya se hizo
mercadofresco-procesar-pedidocompletaMapdistribuido para el volumen del viernes- Despliegue y arranque desde EventBridge
- Observabilidad: depurar una ejecución fallida
- Workflow Studio y coste
- Errores comunes y consejos
- Ejercicios
- Conclusión
Coreografía frente a orquestación
| Coreografía (eventos) | Orquestación (flujo con estado) | |
|---|---|---|
| Quién sabe el proceso | Nadie: está repartido | El orquestador, en un sitio |
| Acoplamiento | Mínimo | Medio: el orquestador conoce los pasos |
| Añadir un paso | Suscribir un consumidor | Editar la definición |
| Ver en qué punto va | Reconstruir de registros | Consulta directa |
| Deshacer lo hecho | Muy difícil | Catch + compensaciones |
| Esperar horas o días | Necesita almacenar estado | Nativo (Wait, token de retorno) |
| Punto único de fallo | No | El orquestador (gestionado, pero conceptual) |
| Escala | Enorme | Alta, con límites por cuenta |
Elige coreografía cuando los consumidores son independientes, el orden no importa, cada uno puede
fallar por su cuenta sin afectar a los demás y quieres poder añadir interesados sin tocar nada. Es lo
que hicimos con PedidoConfirmado: el correo, la analítica y márketing no se conocen ni les hace falta.
Elige orquestación cuando hay un proceso de negocio con nombre, sus pasos dependen del resultado de los anteriores, hay decisiones condicionales, hay esperas largas o intervención humana, y —sobre todo— hay que compensar lo hecho si algo falla a mitad. Cobrar una tarjeta y no poder servir el pedido no se arregla con reintentos: se arregla devolviendo el dinero, y alguien tiene que saber que hay que hacerlo.
MercadoFresco acaba con las dos: bus-mercadofresco reparte los hechos, y una de sus reglas arranca
la máquina de estados que gobierna el proceso de pedido. No compiten; se complementan.
Flujos estándar y flujos exprés
| Estándar | Exprés | |
|---|---|---|
| Duración máxima | 1 año | 5 minutos |
| Semántica de ejecución | Exactamente una vez | Al menos una vez (síncrono: como mucho una) |
| Precio | 0,025 USD/1.000 transiciones | Por ejecución + GB-segundo |
| Rendimiento | 2.000 arranques/s | 100.000 arranques/s |
| Historial de ejecución | 90 días en la consola, con detalle | Solo en CloudWatch Logs, si lo activas |
Token de retorno (waitForTaskToken) |
Sí | Solo en modo síncrono |
Integraciones .sync |
Sí | No |
| Caso de uso | Procesos de negocio largos y críticos | Alto volumen, corto, idempotente |
La fila decisiva es la semántica. Un flujo estándar garantiza que cada paso se ejecuta exactamente una vez: si el servicio tiene un problema interno, reanuda desde donde estaba sin repetir. Un flujo exprés puede reejecutar un paso, así que todos sus pasos deben ser idempotentes.
Para MercadoFresco: mercadofresco-procesar-pedido es estándar, porque cobra tarjetas, dura hasta
una hora y no puede permitirse repetir un cobro. En cambio, la validación del carrito antes de pagar
—comprobar stock, recalcular precios, aplicar cupones, todo en menos de un segundo y sin efectos
externos— es un candidato perfecto para un flujo exprés (mercadofresco-validar-carrito).
Amazon States Language: la estructura
Una máquina de estados es un objeto JSON con tres claves de nivel superior.
{
"Comment": "Proceso de pedido de MercadoFresco",
"StartAt": "CobrarPago",
"TimeoutSeconds": 5400,
"States": {
"CobrarPago": { "Type": "Task", "Resource": "...", "Next": "ReservarStock" },
"ReservarStock": { "Type": "Task", "Resource": "...", "End": true }
}
}StartAt: el nombre del primer estado. Obligatorio.States: un objeto donde cada clave es el nombre del estado. Los nombres son identificadores únicos y aparecen tal cual en la consola y en el historial, así que conviene que sean descriptivos y estables: cambiarlos rompe los enlaces de las alarmas y confunde el historial.- Cada estado tiene un
Typey, salvo los terminales, unNexto"End": true.
No hay bucles explícitos ni variables globales en el sentido clásico: el «bucle» se hace con un Choice
que vuelve a un estado anterior, y el «estado» es el JSON que viaja de un estado al siguiente. Esa
restricción es deliberada y hace que el flujo sea inspeccionable.
Los ocho tipos de estado
Task — hace trabajo. Es el único que llama a algo externo.
"CobrarPago": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "mercadofresco-cobrar-pago",
"Payload.$": "$.pedido"
},
"ResultSelector": { "referencia_cobro.$": "$.Payload.referencia" },
"ResultPath": "$.cobro",
"TimeoutSeconds": 30,
"Next": "ReservarStock"
}Choice — bifurca según el contenido. No hace trabajo, decide.
"EsUrgente": {
"Type": "Choice",
"Choices": [
{
"And": [
{ "Variable": "$.pedido.franja_entrega", "StringEquals": "24h" },
{ "Variable": "$.pedido.importe_eur", "NumericGreaterThanEquals": 60 }
],
"Next": "AsignarRepartoPrioritario"
},
{ "Variable": "$.pedido.provincia", "StringMatches": "Baleares", "Next": "RutaInsular" }
],
"Default": "AsignarReparto"
}Los comparadores cubren cadenas, números, booleanos, marcas de tiempo y presencia (IsPresent,
IsNull), con variantes Path para comparar dos campos entre sí. Default no es opcional en la
práctica: si ninguna condición encaja y no hay Default, la ejecución falla con States.NoChoiceMatched.
Parallel — ejecuta varias ramas a la vez con la misma entrada, y devuelve un array con los
resultados en el orden de las ramas.
"NotificarEnParalelo": {
"Type": "Parallel",
"Branches": [
{ "StartAt": "AvisarCliente", "States": { "AvisarCliente": {
"Type": "Task", "Resource": "arn:aws:states:::sns:publish",
"Parameters": {"TopicArn": "...", "Message.$": "$.pedido.pedido_id"}, "End": true } } },
{ "StartAt": "PublicarAnalitica", "States": { "PublicarAnalitica": {
"Type": "Task", "Resource": "arn:aws:states:::events:putEvents",
"Parameters": {"Entries": [{"Source": "mercadofresco.tienda",
"DetailType": "PedidoProcesado", "EventBusName": "bus-mercadofresco",
"Detail.$": "$.pedido"}]}, "End": true } } }
],
"ResultPath": "$.notificaciones", "Next": "Exito"
}Cuidado: si una rama falla, todo el Parallel falla y las demás se cancelan. Si quieres tolerancia,
pon Catch dentro de cada rama.
Map — ejecuta la misma sublógica para cada elemento de un array. Es la diferencia con
Parallel: mismas ramas distintas vs. misma rama, datos distintos.
"ReservarCadaLinea": {
"Type": "Map",
"ItemsPath": "$.pedido.lineas",
"MaxConcurrency": 5,
"ItemSelector": { "sku.$": "$$.Map.Item.Value.sku", "unidades.$": "$$.Map.Item.Value.unidades" },
"ItemProcessor": {
"ProcessorConfig": { "Mode": "INLINE" },
"StartAt": "ReservarLinea",
"States": {
"ReservarLinea": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "mercadofresco-reservar-stock", "Payload.$": "$" },
"End": true
}
}
},
"ResultPath": "$.reservas",
"Next": "PrepararEnAlmacen"
}$$ es el objeto de contexto, que da acceso a metadatos de la ejecución: $$.Map.Item.Value es el
elemento actual, $$.Execution.Name el nombre de la ejecución, $$.Task.Token el token de retorno.
Wait — espera. Por segundos fijos (Seconds), hasta una marca de tiempo (Timestamp), o con el
valor tomado de la entrada (SecondsPath, TimestampPath).
"EsperarVentanaDeReparto": {
"Type": "Wait",
"TimestampPath": "$.reparto.inicio_franja",
"Next": "NotificarRepartidor"
}Un Wait de tres días no consume nada: Step Functions no mantiene proceso alguno esperando y no se
cobran transiciones mientras espera. Es una de las diferencias más prácticas frente a implementarlo a
mano.
Pass — no hace trabajo; transforma o inyecta datos. Insustituible para depurar y para normalizar
formas del JSON entre pasos.
"NormalizarEntrada": {
"Type": "Pass",
"Parameters": { "pedido.$": "$.detail", "origen": "eventbridge" },
"Next": "CobrarPago"
}Succeed y Fail — terminan la ejecución. Fail acepta Error y Cause, que son lo que
aparecerá en el historial y en la métrica ExecutionsFailed; ponles textos útiles.
"PedidoCompensado": {
"Type": "Fail",
"Error": "PedidoNoPreparable",
"Cause": "El almacen rechazo la preparacion; cobro devuelto y stock liberado."
}Flujo de datos entre estados
Esta es la parte que más cuesta, y merece trazarse paso a paso. Cada estado Task aplica cinco
filtros en este orden exacto:
| Orden | Campo | Qué hace |
|---|---|---|
| 1 | InputPath |
Selecciona qué parte de la entrada se mira. Por defecto $ (todo) |
| 2 | Parameters |
Construye la carga que se envía al servicio |
| 3 | (el servicio responde) | — |
| 4 | ResultSelector |
Recorta y reordena la respuesta cruda del servicio |
| 5 | ResultPath |
Dice dónde se inserta el resultado dentro de la entrada original |
| 6 | OutputPath |
Selecciona qué se pasa al estado siguiente |
Los campos terminados en .$ toman su valor de una ruta JSONPath en lugar de un literal. Es la regla
que hay que aprender: "FunctionName": "mercadofresco-cobrar-pago" es un literal;
"Payload.$": "$.pedido" es una referencia.
Ejemplo trazado. La entrada del estado CobrarPago es:
{
"pedido": { "pedido_id": "PED-2026-084417", "importe_eur": 48.20, "cliente_id": "CLI-30912" },
"origen": "eventbridge"
}Con el estado definido más arriba:
InputPathno está, así que se toma todo ($).Parametersconstruye{"FunctionName": "mercadofresco-cobrar-pago", "Payload": {"pedido_id": "PED-2026-084417", "importe_eur": 48.20, "cliente_id": "CLI-30912"}}. Solo eso llega a Lambda.- Lambda responde, y la integración
lambda:invokeenvuelve la respuesta:{"ExecutedVersion": "$LATEST", "Payload": {"referencia": "PAY-77321", "estado": "capturado"}, "StatusCode": 200}. ResultSelector{"referencia_cobro.$": "$.Payload.referencia"}recorta a{"referencia_cobro": "PAY-77321"}, tirando el ruido de la integración.ResultPath"$.cobro"inserta ese objeto en la entrada original bajo la clavecobro.OutputPathno está, así que sale todo.
Salida final, que es la entrada de ReservarStock:
{
"pedido": { "pedido_id": "PED-2026-084417", "importe_eur": 48.20, "cliente_id": "CLI-30912" },
"origen": "eventbridge",
"cobro": { "referencia_cobro": "PAY-77321" }
}Tres valores especiales de ResultPath que hay que conocer:
| Valor | Efecto |
|---|---|
"$.algo" |
Inserta el resultado bajo esa clave y conserva la entrada |
"$" (por defecto) |
El resultado sustituye toda la entrada. Causa número uno de datos perdidos |
null |
Descarta el resultado y pasa la entrada intacta. Ideal para tareas cuyo resultado no importa |
El error clásico es dejar ResultPath por defecto en un Task intermedio: la respuesta de Lambda
machaca el pedido entero y el siguiente estado no encuentra $.pedido. Regla de MercadoFresco: en
todo Task intermedio, ResultPath explícito.
Integraciones y patrones de servicio
Step Functions llama a otros servicios de dos maneras. Las integraciones optimizadas tienen ARN
propio (arn:aws:states:::lambda:invoke, :::dynamodb:putItem, :::sns:publish, :::sqs:sendMessage,
:::ecs:runTask) y añaden comodidades. Las integraciones del SDK cubren más de 200 servicios y
9.000 acciones con la forma arn:aws:states:::aws-sdk:<servicio>:<accion>, por ejemplo
arn:aws:states:::aws-sdk:rds:describeDBClusters. Si algo se puede hacer con el SDK, se puede hacer sin
escribir una Lambda.
Y hay tres patrones de integración que cambian por completo el comportamiento del Task:
| Patrón | Sufijo del ARN | Qué hace | Ejemplo |
|---|---|---|---|
| Respuesta | ninguno | Llama y sigue en cuanto responde la API | sns:publish |
| Ejecutar y esperar | .sync |
Espera a que el trabajo termine de verdad | ecs:runTask.sync, states:startExecution.sync |
| Token de retorno | .waitForTaskToken |
Se detiene hasta que alguien devuelve un token | Paso humano, sistema externo |
La diferencia entre respuesta y .sync es enorme y se malinterpreta a menudo. ecs:runTask devuelve en
cuanto ECS acepta la petición —la tarea puede tardar diez minutos más—; ecs:runTask.sync no continúa
hasta que la tarea termina, y falla si la tarea falla. Sin .sync tendrías que montar un bucle de
sondeo con Wait y Choice, que es exactamente lo que el patrón te ahorra.
El token de retorno para el paso humano del almacén
La preparación en el almacén no es una llamada a una API: es una persona que coge una caja, la llena y pasa un lector de códigos. Puede tardar dos minutos o una hora, y a veces responde «no puedo».
.waitForTaskToken está hecho para esto. Step Functions genera un token, lo mete en la carga que
envía al destino y detiene el estado indefinidamente (hasta HeartbeatSeconds o TimeoutSeconds).
La ejecución no consume nada mientras espera. Cuando el sistema del almacén termina, llama a
SendTaskSuccess o SendTaskFailure con ese token, y la máquina continúa.
"PrepararEnAlmacen": {
"Type": "Task",
"Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken",
"Parameters": {
"QueueUrl": "https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-almacen",
"MessageBody": {
"pedido_id.$": "$.pedido.pedido_id",
"lineas.$": "$.pedido.lineas",
"franja.$": "$.pedido.franja_entrega",
"token_tarea.$": "$$.Task.Token"
}
},
"TimeoutSeconds": 3600,
"HeartbeatSeconds": 600,
"ResultPath": "$.preparacion",
"Retry": [
{ "ErrorEquals": ["States.Timeout"], "MaxAttempts": 1, "IntervalSeconds": 60 }
],
"Catch": [
{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "CompensarPedido" }
],
"Next": "AsignarReparto"
}El lado del almacén, que es una aplicación normal:
sfn = boto3.client("stepfunctions", region_name="eu-west-1")
def confirmar_preparacion(token, pedido_id, operario):
"""Lo llama el terminal del almacen cuando la caja esta lista."""
sfn.send_task_success(taskToken=token, output=json.dumps(
{"preparado": True, "operario": operario, "pedido_id": pedido_id}))
def rechazar_preparacion(token, motivo):
"""Producto en mal estado, rotura de stock real, incidencia de frio."""
sfn.send_task_failure(taskToken=token,
error="AlmacenNoPuedePreparar", # se compara en ErrorEquals
cause=motivo)
def sigo_trabajando(token):
"""Latido: se llama cada pocos minutos mientras se prepara la caja."""
sfn.send_task_heartbeat(taskToken=token)HeartbeatSeconds: 600 es la protección contra el operario que se va a comer con la caja a medias: si
no llega un latido en 10 minutos, el estado falla con States.Heartbeat y el proceso puede reaccionar
mucho antes del TimeoutSeconds de una hora. Y error="AlmacenNoPuedePreparar" no es decorativo: es
el valor que se compara en ErrorEquals para elegir el camino de compensación.
Dos advertencias. El token caduca con la ejecución: si la máquina se detiene, SendTaskSuccess
devolverá TaskTimedOut. Y hay que guardar el token en algún sitio persistente (en el propio
mensaje de la cola, como aquí, o en DynamoDB), porque es lo único que permite reanudar.
Manejo de errores: Retry y Catch
Retry reintenta el mismo estado. Catch abandona el estado y salta a otro. Se evalúan en ese orden:
primero se agotan los reintentos, y solo entonces actúa el Catch.
"Retry": [
{
"ErrorEquals": ["Lambda.ServiceException", "Lambda.TooManyRequestsException",
"States.TaskFailed", "PasarelaTemporalmenteNoDisponible"],
"IntervalSeconds": 2,
"MaxAttempts": 4,
"BackoffRate": 2.0,
"MaxDelaySeconds": 30,
"JitterStrategy": "FULL"
},
{
"ErrorEquals": ["TarjetaRechazada"],
"MaxAttempts": 0
}
]| Parámetro | Qué hace |
|---|---|
ErrorEquals |
Lista de nombres de error. States.ALL los captura todos |
IntervalSeconds |
Espera antes del primer reintento |
MaxAttempts |
Reintentos (no intentos). 0 desactiva el reintento para ese error |
BackoffRate |
Multiplicador entre reintentos: 2 → 2 s, 4 s, 8 s, 16 s |
MaxDelaySeconds |
Techo del intervalo, para que el backoff no se dispare |
JitterStrategy |
FULL aleatoriza la espera y evita que mil ejecuciones reintenten a la vez |
JitterStrategy: "FULL" debería ser tu valor por defecto. Sin él, si la pasarela de pago se cae 30
segundos, las 450 ejecuciones en curso reintentan exactamente en el mismo instante y la tumban otra vez
justo cuando se estaba recuperando. Es la tormenta de reintentos, que veremos a fondo en 07-05.
El orden de los bloques importa. El primero que encaje gana, así que los errores específicos van
antes que los genéricos. En el ejemplo, TarjetaRechazada con MaxAttempts: 0 desactiva
explícitamente el reintento para un error que no se arregla reintentando: la tarjeta seguirá rechazada.
Distinguir errores transitorios de permanentes es la misma disciplina de 07-01, ahora declarativa.
Catch funciona igual, pero en lugar de reintentar salta:
"Catch": [
{ "ErrorEquals": ["TarjetaRechazada"], "ResultPath": "$.error", "Next": "PedidoRechazado" },
{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "CompensarPedido" }
]ResultPath en el Catch es crítico. Con "$.error", el estado de destino recibe la entrada
original más el objeto {"Error": "...", "Cause": "..."} bajo error. Sin él (valor por defecto
$), el error sustituye toda la entrada y el estado de compensación se queda sin saber qué pedido
compensar ni qué referencia de cobro devolver. Es el fallo más frustrante de depurar en Step Functions.
Errores predefinidos que conviene conocer: States.Timeout, States.TaskFailed, States.Permissions,
States.ResultPathMatchFailed, States.NoChoiceMatched, States.Heartbeat, States.DataLimitExceeded
(la carga entre estados superó los 256 KB).
El patrón saga: compensar lo que ya se hizo
No hay transacciones distribuidas entre una pasarela de pago, Aurora, DynamoDB y el ERP de un socio. La alternativa realista es la saga: una secuencia de pasos donde cada uno tiene una compensación que deshace su efecto, y si algo falla se ejecutan las compensaciones de los pasos ya completados, en orden inverso.
| Paso | Compensación |
|---|---|
| Cobrar la tarjeta | Devolver el cobro (mercadofresco-devolver-cobro) |
| Reservar stock | Liberar stock (mercadofresco-liberar-stock) |
| Preparar en almacén | Cancelar la orden de preparación |
| Asignar reparto | Liberar la franja del repartidor |
La compensación no es un rollback: es una acción de negocio nueva y visible. Una devolución aparece en el extracto del cliente; el dinero estuvo retenido. Por eso las sagas se diseñan para minimizar el tiempo entre el paso arriesgado y su posible compensación, y por eso conviene ordenar los pasos de más a menos reversible cuando se pueda, dejando los irreversibles para el final.
stateDiagram-v2
[*] --> CobrarPago
CobrarPago --> ReservarStock: cobro OK
CobrarPago --> PedidoRechazado: TarjetaRechazada
ReservarStock --> PrepararEnAlmacen: stock reservado
ReservarStock --> DevolverCobro: SinStock
PrepararEnAlmacen --> AsignarReparto: token success
PrepararEnAlmacen --> LiberarStock: AlmacenNoPuedePreparar / Timeout / Heartbeat
AsignarReparto --> NotificarCliente: reparto asignado
AsignarReparto --> LiberarStock: SinRepartidor
NotificarCliente --> PedidoCompletado
LiberarStock --> DevolverCobro: stock liberado
DevolverCobro --> PedidoCompensado: cobro devuelto
DevolverCobro --> RevisionManual: fallo al devolver
PedidoCompletado --> [*]
PedidoCompensado --> [*]
PedidoRechazado --> [*]
RevisionManual --> [*]
Fíjate en RevisionManual. Las compensaciones también fallan, y cuando falla una devolución hay
dinero de un cliente retenido sin pedido: eso no puede terminar en un Fail silencioso. Ese estado
publica en alertas-mercadofresco y escribe en una cola que Marta revisa. Toda saga necesita su
camino de «esto ya es para un humano».
mercadofresco-procesar-pedido completa
{
"Comment": "Proceso de pedido de MercadoFresco con saga de compensacion",
"StartAt": "NormalizarEntrada",
"TimeoutSeconds": 5400,
"States": {
"NormalizarEntrada": {
"Type": "Pass",
"Parameters": { "pedido.$": "$.detail", "iniciado_en.$": "$$.Execution.StartTime" },
"Next": "CobrarPago"
},
"CobrarPago": {
"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "mercadofresco-cobrar-pago", "Payload.$": "$.pedido" },
"ResultSelector": { "referencia.$": "$.Payload.referencia" },
"ResultPath": "$.cobro", "TimeoutSeconds": 30,
"Retry": [
{ "ErrorEquals": ["TarjetaRechazada"], "MaxAttempts": 0 },
{ "ErrorEquals": ["States.ALL"], "IntervalSeconds": 2, "MaxAttempts": 4,
"BackoffRate": 2.0, "MaxDelaySeconds": 30, "JitterStrategy": "FULL" }
],
"Catch": [{ "ErrorEquals": ["TarjetaRechazada"], "ResultPath": "$.error",
"Next": "PedidoRechazado" }],
"Next": "ReservarStock"
},
"ReservarStock": {
"Type": "Map", "ItemsPath": "$.pedido.lineas", "MaxConcurrency": 5,
"ItemSelector": { "sku.$": "$$.Map.Item.Value.sku",
"unidades.$": "$$.Map.Item.Value.unidades",
"pedido_id.$": "$.pedido.pedido_id" },
"ItemProcessor": {
"ProcessorConfig": { "Mode": "INLINE" }, "StartAt": "ReservarLinea",
"States": { "ReservarLinea": {
"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "mercadofresco-reservar-stock", "Payload.$": "$" },
"ResultSelector": { "reservado.$": "$.Payload.reservado" }, "End": true } }
},
"ResultPath": "$.reservas",
"Catch": [{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.error",
"Next": "DevolverCobro" }],
"Next": "PrepararEnAlmacen"
},
"PrepararEnAlmacen": {
"Type": "Task", "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken",
"Parameters": {
"QueueUrl": "https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-almacen",
"MessageBody": { "pedido_id.$": "$.pedido.pedido_id", "lineas.$": "$.pedido.lineas",
"franja.$": "$.pedido.franja_entrega", "token_tarea.$": "$$.Task.Token" }
},
"TimeoutSeconds": 3600, "HeartbeatSeconds": 600, "ResultPath": "$.preparacion",
"Catch": [{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.error",
"Next": "LiberarStock" }],
"Next": "AsignarReparto"
},
"AsignarReparto": {
"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "mercadofresco-asignar-reparto",
"Payload": { "pedido_id.$": "$.pedido.pedido_id",
"franja.$": "$.pedido.franja_entrega",
"provincia.$": "$.pedido.provincia" } },
"ResultSelector": { "repartidor_id.$": "$.Payload.repartidor_id", "eta.$": "$.Payload.eta" },
"ResultPath": "$.reparto",
"Retry": [{ "ErrorEquals": ["States.ALL"], "IntervalSeconds": 5, "MaxAttempts": 3,
"BackoffRate": 2.0, "JitterStrategy": "FULL" }],
"Catch": [{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.error",
"Next": "LiberarStock" }],
"Next": "NotificarCliente"
},
"NotificarCliente": {
"Type": "Task", "Resource": "arn:aws:states:::events:putEvents",
"Parameters": { "Entries": [{
"EventBusName": "bus-mercadofresco", "Source": "mercadofresco.tienda",
"DetailType": "PedidoProcesado",
"Detail": { "pedido_id.$": "$.pedido.pedido_id",
"repartidor_id.$": "$.reparto.repartidor_id", "eta.$": "$.reparto.eta" } }] },
"ResultPath": null, "Next": "PedidoCompletado"
},
"PedidoCompletado": { "Type": "Succeed" },
"LiberarStock": {
"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "mercadofresco-liberar-stock",
"Payload": { "pedido_id.$": "$.pedido.pedido_id",
"lineas.$": "$.pedido.lineas" } },
"ResultPath": null,
"Retry": [{ "ErrorEquals": ["States.ALL"], "IntervalSeconds": 5, "MaxAttempts": 5,
"BackoffRate": 2.0, "JitterStrategy": "FULL" }],
"Catch": [{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.error_compensacion",
"Next": "RevisionManual" }],
"Next": "DevolverCobro"
},
"DevolverCobro": {
"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "mercadofresco-devolver-cobro",
"Payload": { "referencia.$": "$.cobro.referencia",
"pedido_id.$": "$.pedido.pedido_id" } },
"ResultPath": null,
"Retry": [{ "ErrorEquals": ["States.ALL"], "IntervalSeconds": 10, "MaxAttempts": 6,
"BackoffRate": 2.0, "MaxDelaySeconds": 300, "JitterStrategy": "FULL" }],
"Catch": [{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.error_compensacion",
"Next": "RevisionManual" }],
"Next": "PedidoCompensado"
},
"RevisionManual": {
"Type": "Task", "Resource": "arn:aws:states:::sns:publish",
"Parameters": { "TopicArn": "arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco",
"Subject": "Compensacion fallida: revision manual",
"Message.$": "States.JsonToString($)" },
"Next": "PedidoIncoherente"
},
"PedidoIncoherente": { "Type": "Fail", "Error": "CompensacionFallida",
"Cause": "No se pudo devolver el cobro o liberar el stock. Requiere intervencion humana." },
"PedidoCompensado": { "Type": "Fail", "Error": "PedidoNoPreparable",
"Cause": "El pedido se compenso correctamente: cobro devuelto y stock liberado." },
"PedidoRechazado": { "Type": "Fail", "Error": "TarjetaRechazada",
"Cause": "La pasarela rechazo el pago. No hay nada que compensar." }
}
}Detalles que no son casuales. ResultPath: null en las compensaciones y en la notificación: su
resultado no aporta nada y así no se ensucia el estado. Más reintentos en DevolverCobro (6) que en
el resto: fallar una devolución es mucho más caro que fallar cualquier otra cosa. PedidoRechazado
no pasa por compensación, porque si la tarjeta se rechazó no se cobró nada. Y States.JsonToString($)
es una de las funciones intrínsecas del lenguaje; hay más (States.Format, States.Array,
States.ArrayPartition, States.MathRandom, States.UUID) que evitan tener que escribir una Lambda
solo para transformar datos.
Map distribuido para el volumen del viernes
El Map en modo INLINE tiene dos límites: 40 iteraciones concurrentes y todos los datos en el
estado, sujetos al máximo de 256 KB. Para conciliar los 4.300 pedidos de un viernes contra los apuntes
de la pasarela, o para procesar un fichero de 200.000 líneas en S3, se necesita el modo
DISTRIBUTED.
"ConciliarPedidosDelDia": {
"Type": "Map",
"ItemReader": {
"Resource": "arn:aws:states:::s3:getObject",
"ReaderConfig": { "InputType": "CSV", "CSVHeaderLocation": "FIRST_ROW" },
"Parameters": { "Bucket": "mercadofresco-informes-analitica",
"Key.$": "$.fichero_conciliacion" }
},
"ItemProcessor": {
"ProcessorConfig": { "Mode": "DISTRIBUTED", "ExecutionType": "EXPRESS" },
"StartAt": "ConciliarPedido",
"States": {
"ConciliarPedido": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "mercadofresco-conciliar-pedido", "Payload.$": "$" },
"End": true
}
}
},
"MaxConcurrency": 200,
"ToleratedFailurePercentage": 2,
"ItemBatcher": { "MaxItemsPerBatch": 25 },
"ResultWriter": {
"Resource": "arn:aws:states:::s3:putObject",
"Parameters": { "Bucket": "mercadofresco-informes-analitica",
"Prefix": "conciliacion/resultados/" }
},
"Next": "PublicarInformeConciliacion"
}Las diferencias con el modo INLINE son sustanciales:
INLINE |
DISTRIBUTED |
|
|---|---|---|
| Concurrencia máxima | 40 | 10.000 |
| Origen de los elementos | Un array en el estado | Array, o S3: objetos, CSV, JSON, manifiesto |
| Ejecución de cada iteración | Dentro de la ejecución padre | Ejecución hija independiente |
| Historial | Cuenta en el de la padre | Propio, sin inflar el de la padre |
| Tolerancia a fallos | Todo o nada | ToleratedFailurePercentage |
| Volumen práctico | Cientos | Millones |
ToleratedFailurePercentage: 2 permite que hasta un 2 % de los apuntes falle sin abortar la
conciliación entera —lo típico cuando el fichero del banco trae líneas raras— y ItemBatcher agrupa 25
elementos por invocación de Lambda, dividiendo por 25 el número de invocaciones y su coste. Como cada
iteración es una ejecución hija, el historial de la padre no se llena con 4.300 entradas, que es lo que
hacía inservible la consola con el modo INLINE.
Despliegue y arranque desde EventBridge
export PERFIL="--profile mercadofresco-dev --region eu-west-1"
aws stepfunctions create-state-machine \
--name mercadofresco-procesar-pedido \
--definition file://mercadofresco-procesar-pedido.json \
--role-arn arn:aws:iam::111122223333:role/rol-mf-step-functions \
--type STANDARD \
--logging-configuration '{"level":"ERROR","includeExecutionData":true,
"destinations":[{"cloudWatchLogsLogGroup":{"logGroupArn":
"arn:aws:logs:eu-west-1:111122223333:log-group:/aws/vendedlogs/states/mercadofresco:*"}}]}' \
--tracing-configuration '{"enabled":true}' \
--tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
Key=Componente,Value=integracion Key=Propietario,Value=marta \
Key=CentroCoste,Value=tecnologia $PERFILEl rol rol-mf-step-functions necesita permiso para cada acción que la máquina ejecuta:
lambda:InvokeFunction sobre las cinco funciones, sqs:SendMessage sobre la cola del almacén,
events:PutEvents sobre el bus, sns:Publish sobre el tema de alertas, más
xray:PutTraceSegments y los permisos de escritura en el grupo de registros. Un States.Permissions
en mitad de una compensación es de los fallos más desagradables que hay: el pedido queda a medias
porque el orquestador no podía llamar a quien debía.
El arranque lo hace una regla de bus-mercadofresco:
aws events put-rule --name regla-mf-arrancar-proceso-pedido \
--event-bus-name bus-mercadofresco --state ENABLED \
--event-pattern '{"source":["mercadofresco.tienda"],"detail-type":["PedidoConfirmado"]}' $PERFIL
aws events put-targets --rule regla-mf-arrancar-proceso-pedido \
--event-bus-name bus-mercadofresco \
--targets '[{
"Id": "maquina-procesar-pedido",
"Arn": "arn:aws:states:eu-west-1:111122223333:stateMachine:mercadofresco-procesar-pedido",
"RoleArn": "arn:aws:iam::111122223333:role/rol-mf-eventbridge-invoca-sfn",
"DeadLetterConfig": {"Arn":"arn:aws:sqs:eu-west-1:111122223333:mercadofresco-eventbridge-fallidas"},
"RetryPolicy": {"MaximumRetryAttempts": 5, "MaximumEventAgeInSeconds": 3600}
}]' $PERFILUn truco muy recomendable: pasar como nombre de ejecución un valor derivado del pedido_id. Los nombres
de ejecución deben ser únicos en 90 días, así que si el mismo evento llega dos veces, la segunda
ejecución falla con ExecutionAlreadyExists en lugar de cobrar la tarjeta otra vez. Es idempotencia
gratis, y enlaza directamente con 07-05.
Observabilidad: depurar una ejecución fallida
Step Functions guarda el historial completo de cada ejecución estándar durante 90 días: cada entrada de estado, cada salida, cada reintento, cada error. Es su mejor característica y no tiene equivalente en una orquestación escrita a mano.
# Ejecuciones fallidas del dia
aws stepfunctions list-executions \
--state-machine-arn arn:aws:states:eu-west-1:111122223333:stateMachine:mercadofresco-procesar-pedido \
--status-filter FAILED --max-items 20 $PERFIL
# El historial completo de una de ellas
aws stepfunctions get-execution-history \
--execution-arn arn:aws:states:eu-west-1:111122223333:execution:mercadofresco-procesar-pedido:PED-2026-084417 \
--reverse-order --max-items 40 $PERFILUna ejecución fallida real. Marta ve una ejecución en FAILED y recorre el historial hacia atrás:
ExecutionFailedconError: "PedidoNoPreparable"→ terminó enPedidoCompensado, así que la compensación funcionó. Buena señal: el cliente tiene su dinero.TaskStateExiteddeDevolverCobroy deLiberarStock→ las dos compensaciones se ejecutaron.TaskFailedenPrepararEnAlmacenconError: "States.Heartbeat"→ no fue un rechazo del almacén, fue falta de latido. El terminal dejó de enviarSendTaskHeartbeat.TaskScheduleddePrepararEnAlmacena las 18:47,TaskFaileda las 18:57 → exactamente los 600 segundos deHeartbeatSeconds.
Diagnóstico: no es un problema de negocio, es que el terminal del almacén pierde la conexión wifi en la
cámara de frío y deja de enviar latidos. La corrección no está en la máquina de estados —que se comportó
correctamente— sino en el terminal, que debe reintentar el latido, y en subir HeartbeatSeconds a 900
para tolerar los cortes conocidos. Sin el historial, este diagnóstico habría llevado días.
Con --tracing-configuration '{"enabled":true}', X-Ray (05-02) muestra el mapa de servicios de la
ejecución y dónde se fue el tiempo: cuánto en la pasarela, cuánto esperando al almacén, cuánto en
Aurora. Y las métricas de CloudWatch a vigilar son ExecutionsFailed, ExecutionsTimedOut,
ExecutionsAborted y ExecutionTime, todas con alarma hacia alertas-mercadofresco.
Un detalle de configuración: "level": "ERROR" registra solo los estados fallidos. "ALL" registra
todo y es utilísimo mientras desarrollas, pero en producción, con 240.000 ejecuciones al mes y
includeExecutionData: true, el volumen de CloudWatch Logs puede costar más que la propia máquina.
Workflow Studio y coste
Workflow Studio es el editor visual de la consola: arrastras estados, configuras integraciones desde formularios y ves el JSON generarse en tiempo real, con validación al vuelo. Es la mejor forma de aprender el lenguaje y de explorar las 9.000 acciones del SDK, y también de dibujar el primer borrador de un flujo con Marta delante. Para producción, la definición vive en el repositorio y se despliega con CloudFormation o CDK (módulo 9): el editor visual sirve para diseñar y para leer, no para ser la fuente de la verdad.
Coste. Los flujos estándar se cobran por transición de estado: 4.000 gratis al mes y luego 0,025
USD por 1.000. mercadofresco-procesar-pedido recorre unos 9 estados por pedido en el camino feliz.
| Escenario | Transiciones/mes | Coste |
|---|---|---|
| 240.000 pedidos × 9 estados (estándar) | 2.160.000 | 54,00 USD |
| Compensaciones (1,2 % × 4 estados extra) | 11.520 | 0,29 USD |
mercadofresco-validar-carrito, exprés, 1,4 M ejecuciones de 200 ms y 64 MB |
— | ~2,20 USD |
Conciliación diaria (DISTRIBUTED, 30 × 4.300 hijas exprés) |
— | ~1,10 USD |
La comparación estándar/exprés es reveladora: el mismo proceso en exprés costaría unos 3 USD en lugar
de 54. ¿Por qué no usar exprés para todo, entonces? Porque el proceso de pedido dura hasta una hora
(el máximo de exprés son 5 minutos), necesita waitForTaskToken asíncrono, necesita semántica de
exactamente una vez para no cobrar dos veces, y necesita el historial de 90 días para las
reclamaciones. Los 51 USD de diferencia compran precisamente eso, y para 240.000 pedidos con un
importe medio de 48 € es una fracción despreciable de la facturación.
La regla práctica: exprés para lo corto, idempotente y de alto volumen; estándar para lo largo, lo crítico y lo que hay que poder auditar.
aws stepfunctions delete-state-machine \
--state-machine-arn arn:aws:states:eu-west-1:111122223333:stateMachine:mercadofresco-procesar-pedido $PERFIL
aws events remove-targets --rule regla-mf-arrancar-proceso-pedido \
--event-bus-name bus-mercadofresco --ids maquina-procesar-pedido $PERFIL
aws events delete-rule --name regla-mf-arrancar-proceso-pedido \
--event-bus-name bus-mercadofresco $PERFILBorrar una máquina de estados no cancela las ejecuciones en curso: pasan a ABORTED cuando terminen los
pasos actuales, y las que esperan un token quedan huérfanas.
Errores Comunes y Consejos
Dejar ResultPath por defecto en un Task intermedio. El resultado sustituye toda la entrada y el
siguiente estado no encuentra sus datos. Síntoma: States.Runtime con «no se pudo resolver la ruta».
Pon ResultPath explícito siempre, o null si el resultado no importa.
Olvidar ResultPath en el Catch. El estado de compensación recibe solo el error y no sabe qué
pedido compensar ni qué cobro devolver. Es el fallo más frustrante de esta lección.
Choice sin Default. Un caso no previsto termina en States.NoChoiceMatched y la ejecución muere
sin compensar nada.
No distinguir errores transitorios de permanentes en Retry. Reintentar cuatro veces una
TarjetaRechazada retrasa la respuesta al cliente y no arregla nada. Declara MaxAttempts: 0 para los
errores permanentes, y ponlos antes del bloque genérico.
Retry sin JitterStrategy: "FULL". Cuando la dependencia se recupera, todas las ejecuciones
reintentan a la vez y la vuelven a tumbar.
Compensaciones sin su propio Catch. Si la devolución falla y no hay camino a revisión manual, el
cliente se queda sin pedido y sin dinero, y nadie se entera.
Pasar cargas grandes entre estados. El límite es 256 KB. Un Map con 5.000 elementos revienta con
States.DataLimitExceeded. Pasa referencias a S3, no contenidos.
waitForTaskToken sin HeartbeatSeconds ni TimeoutSeconds. La ejecución se queda esperando hasta
el año, ocupando una ranura y sin que nadie lo note.
Usar exprés para procesos con efectos no idempotentes. La semántica de al menos una vez significa que un cobro puede ejecutarse dos veces.
Parallel sin Catch por rama. El fallo de una rama cancela las demás, incluidas las que ya iban
por la mitad.
Consejo: nombra la ejecución con el identificador de negocio. PED-2026-084417 como nombre de
ejecución da idempotencia gratis, y buscar en la consola pasa de imposible a inmediato.
Consejo: valida el flujo de datos con Pass antes de escribir la lógica. Monta la máquina entera
con estados Pass que devuelven datos ficticios, comprueba que el JSON llega bien de un extremo a otro,
y solo entonces sustituye cada Pass por su Task.
Consejo: "level": "ALL" en desarrollo, "ERROR" en producción. El registro completo con datos de
ejecución es carísimo a 240.000 ejecuciones al mes.
Ejercicios
Ejercicio 1: la devolución de un pedido entregado
Diseña mercadofresco-procesar-devolucion para cuando un cliente devuelve un pedido ya entregado. Pasos:
(1) validar que la devolución está dentro de plazo (48 h); (2) generar la etiqueta de recogida llamando
a la API del transportista; (3) esperar a que el transportista confirme la recogida, que puede tardar
hasta 3 días; (4) cuando llegue al almacén, un operario inspecciona el estado del producto y decide
aceptar, aceptar parcialmente o rechazar; (5) según la decisión, devolver el importe total, parcial o
nada; (6) reponer el stock solo si se aceptó.
Indica: (a) estándar o exprés y por qué; (b) qué tipo de estado usas en cada paso; (c) cómo modelas los
pasos 3 y 4; (d) qué Retry y qué Catch pones en el paso 5; (e) qué compensaciones necesitas y cuáles
no.
Ejercicio 2: trazar el flujo de datos
Dado este estado y esta entrada, escribe la salida exacta que recibe el estado siguiente.
"ComprobarCliente": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"InputPath": "$.pedido",
"Parameters": { "FunctionName": "mercadofresco-datos-cliente", "Payload": { "id.$": "$.cliente_id" } },
"ResultSelector": { "nivel.$": "$.Payload.nivel_fidelidad", "email.$": "$.Payload.email" },
"ResultPath": "$.cliente",
"OutputPath": "$",
"Next": "AplicarDescuento"
}Entrada: {"pedido": {"pedido_id": "PED-1", "cliente_id": "CLI-9", "importe_eur": 30.0}, "origen": "web"}. Lambda devuelve {"nivel_fidelidad": "oro", "email": "[email protected]", "telefono": "+34600000000"}.
Responde: (a) qué recibe exactamente la Lambda; (b) la salida completa del estado; (c) qué pasaría si se
quitara ResultPath; (d) qué pasaría si OutputPath fuera "$.cliente"; (e) por qué InputPath no
afecta a dónde inserta ResultPath.
Ejercicio 3: coreografía u orquestación
Para cada caso, decide y justifica en dos frases: (a) al confirmar un pedido hay que avisar a cuatro
equipos independientes; (b) el alta de un proveedor requiere validar documentos, aprobación de dos
personas y alta en Aurora, con plazo de 10 días; (c) hay que redimensionar 8.000 fotos del catálogo cada
noche; (d) al detectar StockBajo hay que pedir al proveedor, esperar confirmación y actualizar la
fecha prevista; (e) hay que registrar cada cambio de precio en Redshift para auditoría.
Soluciones
Solución 1
(a) Estándar, sin duda. El paso 3 puede durar 3 días, muy por encima de los 5 minutos de exprés. Además hay dinero de por medio y hace falta la semántica de exactamente una vez y el historial de 90 días para las reclamaciones.
(b) y (c) Los estados. (1) Choice comparando $$.Execution.StartTime con la fecha de entrega;
si está fuera de plazo, Fail con Error: "FueraDePlazoDevolucion" sin compensar nada, porque aún no se
ha hecho nada. (2) Task con Retry y JitterStrategy: FULL hacia la API del transportista.
(3) Task con .waitForTaskToken y TimeoutSeconds: 259200 (3 días): el webhook del transportista
llama a SendTaskSuccess; un Wait no serviría porque no sabemos cuándo ocurrirá, solo el plazo máximo,
y aquí no conviene HeartbeatSeconds porque el transportista no envía latidos. (4) Otro Task con
.waitForTaskToken hacia la cola del almacén, con TimeoutSeconds: 86400; la decisión del operario
llega en el output del SendTaskSuccess y un Choice posterior enruta según
$.inspeccion.decision (aceptada/parcial/rechazada), con Default a revisión manual.
(5) Task de devolución con importe calculado. (6) Map por línea para reponer stock.
(d) Retry y Catch del paso 5. Retry generoso —6 intentos, IntervalSeconds: 10,
BackoffRate: 2, MaxDelaySeconds: 300, JitterStrategy: FULL— porque una devolución fallida es lo
peor que puede pasar aquí. Catch sobre States.ALL con ResultPath: "$.error" hacia un estado
RevisionManualDevolucion que publica en alertas-mercadofresco. Nunca un Fail directo: dejaría
al cliente sin producto y sin dinero.
(e) Compensaciones. Aquí hay muchas menos que en el proceso de pedido, y el motivo es interesante: el flujo va de menos a más comprometido, y los pasos irreversibles están al final. La etiqueta de recogida sí necesita compensación (cancelarla) si la devolución se aborta antes de la recogida. La inspección no necesita compensación: es una lectura. La devolución del importe no se compensa volviendo a cobrar —eso sería inaceptable—; si se detecta un error posterior se abre una incidencia manual. Y la reposición de stock se compensa retirándolo si luego se descubre que el producto no era apto. Lección general: ordenar los pasos de más reversible a menos reversible reduce el número de compensaciones necesarias.
Solución 2
(a) Qué recibe la Lambda. InputPath: "$.pedido" reduce la entrada a
{"pedido_id": "PED-1", "cliente_id": "CLI-9", "importe_eur": 30.0}. Sobre eso, Parameters construye
la carga, y "id.$": "$.cliente_id" se resuelve contra el resultado de InputPath, no contra la
entrada original. La Lambda recibe exactamente:
(b) Salida completa. La respuesta de la integración es
{"ExecutedVersion": "...", "Payload": {"nivel_fidelidad": "oro", "email": "[email protected]", "telefono": "+34600000000"}, "StatusCode": 200}. ResultSelector la recorta a
{"nivel": "oro", "email": "[email protected]"} —el teléfono se descarta— y ResultPath: "$.cliente" lo
inserta en la entrada original completa, no en la recortada por InputPath. Con OutputPath: "$"
sale todo:
{
"pedido": { "pedido_id": "PED-1", "cliente_id": "CLI-9", "importe_eur": 30.0 },
"origen": "web",
"cliente": { "nivel": "oro", "email": "[email protected]" }
}(c) Sin ResultPath. El valor por defecto es $, así que el resultado sustituye toda la entrada.
La salida sería {"nivel": "oro", "email": "[email protected]"} y AplicarDescuento no encontraría
$.pedido.importe_eur, fallando con un error de resolución de ruta. Es el error más común del lenguaje.
(d) Con OutputPath: "$.cliente". La salida sería solo {"nivel": "oro", "email": "[email protected]"}. Se pierde el pedido igualmente, pero por un motivo distinto: no es que el resultado
lo haya sustituido, es que se ha recortado al final. Ilustra bien que ResultPath y OutputPath actúan
en momentos distintos y pueden estropear lo mismo por vías diferentes.
(e) Por qué InputPath no afecta a ResultPath. InputPath solo determina qué se le pasa a
Parameters para construir la llamada; el estado conserva internamente la entrada original y es
sobre ella sobre la que ResultPath inserta. Esa separación es deliberada y muy útil: permite enviar al
servicio una vista reducida sin perder el contexto acumulado del proceso.
Solución 3
(a) Coreografía. Cuatro interesados independientes en el mismo hecho, sin dependencias entre ellos y sin nada que compensar: es fan-out puro. SNS o EventBridge, según si hace falta enrutar por contenido.
(b) Orquestación. Proceso largo (10 días), con pasos dependientes, dos intervenciones humanas
—waitForTaskToken por partida doble— y necesidad de saber en qué punto va cada alta. Con eventos habría
que inventar una tabla de estado y un vigilante, que es reimplementar Step Functions peor.
(c) Ninguno de los dos como orquestación clásica: un Map distribuido, o simplemente S3 → SQS →
Lambda si no hace falta control del conjunto. Son 8.000 tareas independientes e idempotentes; lo que se
necesita es paralelismo y tolerancia a fallos parciales, no un flujo con estado. Si además quieres saber
cuándo terminaron todas y con qué tasa de error, el Map distribuido con ToleratedFailurePercentage
y ResultWriter es la opción correcta.
(d) Orquestación. Hay una espera de duración desconocida (la confirmación del proveedor) y pasos
encadenados. Máquina estándar arrancada por la regla de StockBajo, con waitForTaskToken para la
confirmación y Wait/Choice para reclamar si el proveedor no contesta en 24 horas.
(e) Coreografía. Un hecho suelto que interesa a un consumidor, sin dependencias ni compensación.
Evento en bus-mercadofresco → cola → carga a Redshift. Meter Step Functions aquí solo añadiría coste
por transición y una pieza más que mantener.
Conclusión
El proceso de pedido de MercadoFresco tiene ahora un dueño explícito.
mercadofresco-procesar-pedido sabe cobrar, reservar el stock línea a línea con un Map, esperar hasta
una hora a que una persona del almacén confirme la caja mediante un token de retorno con latido, asignar
reparto, publicar PedidoProcesado en bus-mercadofresco y —lo que ninguna coreografía sabía hacer—
deshacer lo hecho cuando algo falla: liberar el stock, devolver el cobro y, si hasta la compensación
falla, avisar a Marta por alertas-mercadofresco en lugar de morir en silencio. El historial de 90 días
convirtió un misterio («¿por qué se compensó este pedido?») en cuatro entradas leídas de abajo arriba:
el terminal del almacén perdía el wifi en la cámara de frío.
Las ideas que hay que llevarse: la elección entre coreografía y orquestación no es de gusto, la
dictan las dependencias entre pasos y la necesidad de compensar; estándar frente a exprés se decide
por duración, semántica de ejecución y necesidad de auditoría, y los 51 USD de diferencia mensual
compran exactamente eso; el flujo de datos —InputPath, Parameters, ResultSelector,
ResultPath, OutputPath— es donde se pierden más horas, y la regla de oro es ResultPath explícito
en todo Task intermedio; Retry con jitter y errores clasificados evita que la recuperación de
una dependencia la vuelva a tumbar; y toda saga necesita su camino a revisión manual, porque las
compensaciones también fallan.
Con esto, MercadoFresco tiene las cuatro piezas de integración: colas para desacoplar, temas para repartir, un bus para enrutar por contenido y flujos para orquestar. Pero quedan preguntas que atraviesan las cuatro y que hasta ahora hemos ido aplazando. ¿Qué significa exactamente «al menos una vez» cuando el que se duplica es un cobro? ¿Cómo se escribe un consumidor que pueda recibir el mismo mensaje tres veces sin cobrar tres veces? ¿Cuántas veces hay que reintentar, y cuándo dejar de hacerlo para no tumbar al que se está recuperando? ¿Qué se hace exactamente con los mensajes de una DLQ un lunes por la mañana? ¿Y cómo se garantiza que un pedido escrito en Aurora se publica siempre, si no hay transacción que abarque la base de datos y el bus?
En 07-05, «Patrones de integración», cerramos el módulo con las respuestas: garantías de entrega e idempotencia con tabla de deduplicación en DynamoDB, retroceso exponencial con jitter, interruptor de circuito para el proveedor de pagos, el runbook de la DLQ, orden y agrupación, el patrón outbox para la doble escritura, contrapresión, y una tabla decisoria final que responde de una vez a la pregunta «¿SQS, SNS, EventBridge, Step Functions o una llamada síncrona?».
Curso de AWS
Módulo 1: Introducción a AWS
- ¿Qué es AWS?
- Configuración de tu cuenta de AWS
- Infraestructura global de AWS
- Consola de administración de AWS
- AWS CLI y SDKs
Módulo 2: Servicios principales de AWS
Módulo 3: Redes y entrega de contenido
- Amazon VPC
- Grupos de seguridad y listas de control de acceso
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Módulo 4: Seguridad e identidad
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager y Parameter Store
- AWS Shield
- AWS WAF
Módulo 5: Monitorización y gestión
- Amazon CloudWatch
- AWS X-Ray y trazabilidad distribuida
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Módulo 6: Bases de datos
- Cómo elegir la base de datos adecuada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Módulo 7: Integración de aplicaciones
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrones de integración: idempotencia, reintentos y colas de mensajes fallidos
