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 Wait corto puede generar cientos de miles de transiciones en una noche. Al final tienes la limpieza. Datos ficticios.

Contenido

  1. Coreografía frente a orquestación
  2. Flujos estándar y flujos exprés
  3. Amazon States Language: la estructura
  4. Los ocho tipos de estado
  5. Flujo de datos entre estados
  6. Integraciones y patrones de servicio
  7. El token de retorno para el paso humano del almacén
  8. Manejo de errores: Retry y Catch
  9. El patrón saga: compensar lo que ya se hizo
  10. mercadofresco-procesar-pedido completa
  11. Map distribuido para el volumen del viernes
  12. Despliegue y arranque desde EventBridge
  13. Observabilidad: depurar una ejecución fallida
  14. Workflow Studio y coste
  15. Errores comunes y consejos
  16. Ejercicios
  17. 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) Solo en modo síncrono
Integraciones .sync 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 Type y, salvo los terminales, un Next o "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:

  1. InputPath no está, así que se toma todo ($).
  2. Parameters construye {"FunctionName": "mercadofresco-cobrar-pago", "Payload": {"pedido_id": "PED-2026-084417", "importe_eur": 48.20, "cliente_id": "CLI-30912"}}. Solo eso llega a Lambda.
  3. Lambda responde, y la integración lambda:invoke envuelve la respuesta: {"ExecutedVersion": "$LATEST", "Payload": {"referencia": "PAY-77321", "estado": "capturado"}, "StatusCode": 200}.
  4. ResultSelector {"referencia_cobro.$": "$.Payload.referencia"} recorta a {"referencia_cobro": "PAY-77321"}, tirando el ruido de la integración.
  5. ResultPath "$.cobro" inserta ese objeto en la entrada original bajo la clave cobro.
  6. OutputPath no 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 $PERFIL

El 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}
  }]' $PERFIL

Un 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 $PERFIL

Una ejecución fallida real. Marta ve una ejecución en FAILED y recorre el historial hacia atrás:

  1. ExecutionFailed con Error: "PedidoNoPreparable" → terminó en PedidoCompensado, así que la compensación funcionó. Buena señal: el cliente tiene su dinero.
  2. TaskStateExited de DevolverCobro y de LiberarStock → las dos compensaciones se ejecutaron.
  3. TaskFailed en PrepararEnAlmacen con Error: "States.Heartbeat"no fue un rechazo del almacén, fue falta de latido. El terminal dejó de enviar SendTaskHeartbeat.
  4. TaskScheduled de PrepararEnAlmacen a las 18:47, TaskFailed a las 18:57 → exactamente los 600 segundos de HeartbeatSeconds.

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 $PERFIL

Borrar 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:

{ "id": "CLI-9" }

(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 datosInputPath, 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

Módulo 2: Servicios principales de AWS

Módulo 3: Redes y entrega de contenido

Módulo 4: Seguridad e identidad

Módulo 5: Monitorización y gestión

Módulo 6: Bases de datos

Módulo 7: Integración de aplicaciones

Módulo 8: Herramientas para desarrolladores

Módulo 9: Infraestructura como código y gobierno de cuentas

Módulo 10: Contenedores en AWS

Módulo 11: Mejores prácticas y gestión de costos

© Copyright 2026. Todos los derechos reservados