Tot el que hem muntat al mòdul és coreografia: cada component reacciona al que veu i ningú no té la foto completa. Funciona esplèndidament per a fets independents —enviar un correu, carregar analítica, generar una miniatura— i es trenca tan bon punt els passos depenen els uns dels altres i cal desfer el que s'ha fet si alguna cosa falla a mig camí.

El procés de comanda de MercadoFresco és exactament aquest cas: cobrar la targeta, reservar l'estoc, demanar al magatzem que prepari la caixa —que triga entre dos minuts i una hora i la confirma una persona amb un lector de codis—, assignar repartiment i confirmar al client. Si el peix arriba en mal estat i el magatzem no pot preparar la comanda, cal tornar el cobrament i alliberar l'estoc. Amb esdeveniments solts ningú no sap en quin punt era el procés ni què toca compensar; l'única manera és una taula d'estat escrita a mà, un procés que la vigili i un munt de casos rars que no es proven mai.

AWS Step Functions és l'orquestrador sense servidor d'AWS. Defineixes el procés com una màquina d'estats en JSON, i el servei s'encarrega d'executar cada pas, desar l'estat entre passos, reintentar amb retrocés exponencial, capturar errors, esperar hores o dies si cal, i deixar un historial complet de cada execució. Aquí el Luis construeix mercadofresco-procesar-pedido amb el seu camí de compensació complet.

Avís de cost. Els fluxos estàndard costen 0,025 USD per 1.000 transicions d'estat; els exprés, per nombre d'execucions i per GB-segon. Un bucle mal dissenyat amb Wait curt pot generar centenars de milers de transicions en una nit. Al final tens la neteja. Dades fictícies.

Contingut

  1. Coreografia enfront d'orquestració
  2. Fluxos estàndard i fluxos exprés
  3. Amazon States Language: l'estructura
  4. Els vuit tipus d'estat
  5. Flux de dades entre estats
  6. Integracions i patrons de servei
  7. El testimoni de retorn per al pas humà del magatzem
  8. Gestió d'errors: Retry i Catch
  9. El patró saga: compensar el que ja s'ha fet
  10. mercadofresco-procesar-pedido completa
  11. Map distribuït per al volum del divendres
  12. Desplegament i arrencada des d'EventBridge
  13. Observabilitat: depurar una execució fallida
  14. Workflow Studio i cost
  15. Errors habituals i consells
  16. Exercicis
  17. Conclusió

Coreografia enfront d'orquestració

Coreografia (esdeveniments) Orquestració (flux amb estat)
Qui coneix el procés Ningú: està repartit L'orquestrador, en un sol lloc
Acoblament Mínim Mitjà: l'orquestrador coneix els passos
Afegir un pas Subscriure un consumidor Editar la definició
Veure en quin punt va Reconstruir a partir dels registres Consulta directa
Desfer el que s'ha fet Molt difícil Catch + compensacions
Esperar hores o dies Necessita desar l'estat Natiu (Wait, testimoni de retorn)
Punt únic de fallada No L'orquestrador (gestionat, però conceptual)
Escala Enorme Alta, amb límits per compte

Tria coreografia quan els consumidors són independents, l'ordre no importa, cadascun pot fallar pel seu compte sense afectar els altres i vols poder afegir interessats sense tocar res. És el que hem fet amb PedidoConfirmado: el correu, l'analítica i màrqueting no es coneixen ni els cal.

Tria orquestració quan hi ha un procés de negoci amb nom, els seus passos depenen del resultat dels anteriors, hi ha decisions condicionals, hi ha esperes llargues o intervenció humana, i —sobretot— cal compensar el que s'ha fet si alguna cosa falla a mig camí. Cobrar una targeta i no poder servir la comanda no s'arregla amb reintents: s'arregla tornant els diners, i algú ha de saber que cal fer-ho.

MercadoFresco acaba amb totes dues: bus-mercadofresco reparteix els fets, i una de les seves regles arrenca la màquina d'estats que governa el procés de comanda. No competeixen; es complementen.

Fluxos estàndard i fluxos exprés

Estàndard Exprés
Durada màxima 1 any 5 minuts
Semàntica d'execució Exactament una vegada Almenys una vegada (síncron: com a molt una)
Preu 0,025 USD/1.000 transicions Per execució + GB-segon
Rendiment 2.000 arrencades/s 100.000 arrencades/s
Historial d'execució 90 dies a la consola, amb detall Només a CloudWatch Logs, si l'actives
Testimoni de retorn (waitForTaskToken) Només en mode síncron
Integracions .sync No
Cas d'ús Processos de negoci llargs i crítics Volum alt, curt, idempotent

La fila decisiva és la semàntica. Un flux estàndard garanteix que cada pas s'executa exactament una vegada: si el servei té un problema intern, reprèn des d'on era sense repetir. Un flux exprés pot reexecutar un pas, així que tots els seus passos han de ser idempotents.

Per a MercadoFresco: mercadofresco-procesar-pedido és estàndard, perquè cobra targetes, dura fins a una hora i no es pot permetre repetir un cobrament. En canvi, la validació de la cistella abans de pagar —comprovar estoc, recalcular preus, aplicar cupons, tot en menys d'un segon i sense efectes externs— és una candidata perfecta per a un flux exprés (mercadofresco-validar-carrito).

Amazon States Language: l'estructura

Una màquina d'estats és un objecte JSON amb tres claus de nivell 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 nom del primer estat. Obligatori.
  • States: un objecte on cada clau és el nom de l'estat. Els noms són identificadors únics i apareixen tal qual a la consola i a l'historial, així que convé que siguin descriptius i estables: canviar-los trenca els enllaços de les alarmes i confon l'historial.
  • Cada estat té un Type i, tret dels terminals, un Next o "End": true.

No hi ha bucles explícits ni variables globals en el sentit clàssic: el «bucle» es fa amb un Choice que torna a un estat anterior, i l'«estat» és el JSON que viatja d'un estat al següent. Aquesta restricció és deliberada i fa que el flux sigui inspeccionable.

Els vuit tipus d'estat

Task — fa feina. És l'únic que crida alguna cosa externa.

"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 segons el contingut. No fa feina, decideix.

"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"
}

Els comparadors cobreixen cadenes, nombres, booleans, marques de temps i presència (IsPresent, IsNull), amb variants Path per comparar dos camps entre si. Default no és opcional a la pràctica: si cap condició no encaixa i no hi ha Default, l'execució falla amb States.NoChoiceMatched.

Parallel — executa diverses branques alhora amb la mateixa entrada, i retorna un array amb els resultats en l'ordre de les branques.

"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"
}

Compte: si una branca falla, tot el Parallel falla i la resta es cancel·len. Si vols tolerància, posa Catch dins de cada branca.

Map — executa la mateixa sublògica per a cada element d'un array. És la diferència amb Parallel: mateixes branques diferents vs. mateixa branca, dades diferents.

"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"
}

$$ és l'objecte de context, que dona accés a metadades de l'execució: $$.Map.Item.Value és l'element actual, $$.Execution.Name el nom de l'execució, $$.Task.Token el testimoni de retorn.

Wait — espera. Per segons fixos (Seconds), fins a una marca de temps (Timestamp), o amb el valor pres de l'entrada (SecondsPath, TimestampPath).

"EsperarVentanaDeReparto": {
  "Type": "Wait",
  "TimestampPath": "$.reparto.inicio_franja",
  "Next": "NotificarRepartidor"
}

Un Wait de tres dies no consumeix res: Step Functions no manté cap procés esperant i no es cobren transicions mentre espera. És una de les diferències més pràctiques respecte a implementar-ho a mà.

Pass — no fa feina; transforma o injecta dades. Insubstituïble per depurar i per normalitzar formes del JSON entre passos.

"NormalizarEntrada": {
  "Type": "Pass",
  "Parameters": { "pedido.$": "$.detail", "origen": "eventbridge" },
  "Next": "CobrarPago"
}

Succeed i Fail — acaben l'execució. Fail accepta Error i Cause, que són el que apareixerà a l'historial i a la mètrica ExecutionsFailed; posa-hi textos útils.

"PedidoCompensado": {
  "Type": "Fail",
  "Error": "PedidoNoPreparable",
  "Cause": "El almacen rechazo la preparacion; cobro devuelto y stock liberado."
}

Flux de dades entre estats

Aquesta és la part que més costa, i mereix traçar-se pas a pas. Cada estat Task aplica cinc filtres en aquest ordre exacte:

Ordre Camp Què fa
1 InputPath Selecciona quina part de l'entrada es mira. Per defecte $ (tot)
2 Parameters Construeix la càrrega que s'envia al servei
3 (el servei respon)
4 ResultSelector Retalla i reordena la resposta crua del servei
5 ResultPath Diu on s'insereix el resultat dins de l'entrada original
6 OutputPath Selecciona què es passa a l'estat següent

Els camps acabats en .$ prenen el seu valor d'una ruta JSONPath en lloc d'un literal. És la regla que cal aprendre: "FunctionName": "mercadofresco-cobrar-pago" és un literal; "Payload.$": "$.pedido" és una referència.

Exemple traçat. L'entrada de l'estat CobrarPago és:

{
  "pedido": { "pedido_id": "PED-2026-084417", "importe_eur": 48.20, "cliente_id": "CLI-30912" },
  "origen": "eventbridge"
}

Amb l'estat definit més amunt:

  1. InputPath no hi és, així que es pren tot ($).
  2. Parameters construeix {"FunctionName": "mercadofresco-cobrar-pago", "Payload": {"pedido_id": "PED-2026-084417", "importe_eur": 48.20, "cliente_id": "CLI-30912"}}. Només això arriba a Lambda.
  3. Lambda respon, i la integració lambda:invoke embolcalla la resposta: {"ExecutedVersion": "$LATEST", "Payload": {"referencia": "PAY-77321", "estado": "capturado"}, "StatusCode": 200}.
  4. ResultSelector {"referencia_cobro.$": "$.Payload.referencia"} retalla a {"referencia_cobro": "PAY-77321"}, llençant el soroll de la integració.
  5. ResultPath "$.cobro" insereix aquest objecte a l'entrada original sota la clau cobro.
  6. OutputPath no hi és, així que surt tot.

Sortida final, que és l'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 valors especials de ResultPath que cal conèixer:

Valor Efecte
"$.alguna_cosa" Insereix el resultat sota aquesta clau i conserva l'entrada
"$" (per defecte) El resultat substitueix tota l'entrada. Causa número u de dades perdudes
null Descarta el resultat i passa l'entrada intacta. Ideal per a tasques amb resultat irrellevant

L'error clàssic és deixar ResultPath per defecte en un Task intermedi: la resposta de Lambda esclafa la comanda sencera i l'estat següent no troba $.pedido. Regla de MercadoFresco: en tot Task intermedi, ResultPath explícit.

Integracions i patrons de servei

Step Functions crida altres serveis de dues maneres. Les integracions optimitzades tenen ARN propi (arn:aws:states:::lambda:invoke, :::dynamodb:putItem, :::sns:publish, :::sqs:sendMessage, :::ecs:runTask) i afegeixen comoditats. Les integracions de l'SDK cobreixen més de 200 serveis i 9.000 accions amb la forma arn:aws:states:::aws-sdk:<servei>:<accio>, per exemple arn:aws:states:::aws-sdk:rds:describeDBClusters. Si alguna cosa es pot fer amb l'SDK, es pot fer sense escriure una Lambda.

I hi ha tres patrons d'integració que canvien completament el comportament del Task:

Patró Sufix de l'ARN Què fa Exemple
Resposta cap Crida i continua tan bon punt respon l'API sns:publish
Executar i esperar .sync Espera que la feina acabi de debò ecs:runTask.sync, states:startExecution.sync
Testimoni de retorn .waitForTaskToken S'atura fins que algú retorna un testimoni Pas humà, sistema extern

La diferència entre resposta i .sync és enorme i sovint es malinterpreta. ecs:runTask retorna tan bon punt ECS accepta la petició —la tasca pot trigar deu minuts més—; ecs:runTask.sync no continua fins que la tasca acaba, i falla si la tasca falla. Sense .sync hauries de muntar un bucle de sondeig amb Wait i Choice, que és exactament el que el patró t'estalvia.

El testimoni de retorn per al pas humà del magatzem

La preparació al magatzem no és una crida a una API: és una persona que agafa una caixa, l'omple i passa un lector de codis. Pot trigar dos minuts o una hora, i de vegades respon «no puc».

.waitForTaskToken està fet per a això. Step Functions genera un testimoni, el fica a la càrrega que envia a la destinació i atura l'estat indefinidament (fins a HeartbeatSeconds o TimeoutSeconds). L'execució no consumeix res mentre espera. Quan el sistema del magatzem acaba, crida SendTaskSuccess o SendTaskFailure amb aquest testimoni, i la màquina continua.

"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 costat del magatzem, que és una aplicació normal:

sfn = boto3.client("stepfunctions", region_name="eu-west-1")

def confirmar_preparacio(token, comanda_id, operari):
    """El crida el terminal del magatzem quan la caixa esta llesta."""
    sfn.send_task_success(taskToken=token, output=json.dumps(
        {"preparado": True, "operario": operari, "pedido_id": comanda_id}))

def rebutjar_preparacio(token, motiu):
    """Producte en mal estat, trencament d'estoc real, incidencia de fred."""
    sfn.send_task_failure(taskToken=token,
                          error="AlmacenNoPuedePreparar",  # es compara a ErrorEquals
                          cause=motiu)

def continuo_treballant(token):
    """Batec: es crida cada pocs minuts mentre es prepara la caixa."""
    sfn.send_task_heartbeat(taskToken=token)

HeartbeatSeconds: 600 és la protecció contra l'operari que se'n va a dinar amb la caixa a mig fer: si no arriba cap batec en 10 minuts, l'estat falla amb States.Heartbeat i el procés pot reaccionar molt abans del TimeoutSeconds d'una hora. I error="AlmacenNoPuedePreparar" no és decoratiu: és el valor que es compara a ErrorEquals per triar el camí de compensació.

Dos advertiments. El testimoni caduca amb l'execució: si la màquina s'atura, SendTaskSuccess retornarà TaskTimedOut. I cal desar el testimoni en algun lloc persistent (al mateix missatge de la cua, com aquí, o a DynamoDB), perquè és l'única cosa que permet reprendre.

Gestió d'errors: Retry i Catch

Retry reintenta el mateix estat. Catch abandona l'estat i salta a un altre. S'avaluen en aquest ordre: primer s'esgoten els reintents, i només aleshores actua 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àmetre Què fa
ErrorEquals Llista de noms d'error. States.ALL els captura tots
IntervalSeconds Espera abans del primer reintent
MaxAttempts Reintents (no intents). 0 desactiva el reintent per a aquest error
BackoffRate Multiplicador entre reintents: 2 → 2 s, 4 s, 8 s, 16 s
MaxDelaySeconds Sostre de l'interval, perquè el retrocés no es dispari
JitterStrategy FULL aleatoritza l'espera i evita que mil execucions reintentin alhora

JitterStrategy: "FULL" hauria de ser el teu valor per defecte. Sense ell, si la passarel·la de pagament cau 30 segons, les 450 execucions en curs reintenten exactament en el mateix instant i la tomben altre cop just quan s'estava recuperant. És la tempesta de reintents, que veurem a fons a 07-05.

L'ordre dels blocs importa. El primer que encaixi guanya, així que els errors específics van abans que els genèrics. A l'exemple, TarjetaRechazada amb MaxAttempts: 0 desactiva explícitament el reintent per a un error que no s'arregla reintentant: la targeta continuarà rebutjada. Distingir errors transitoris de permanents és la mateixa disciplina de 07-01, ara declarativa.

Catch funciona igual, però en lloc de reintentar salta:

"Catch": [
  { "ErrorEquals": ["TarjetaRechazada"], "ResultPath": "$.error", "Next": "PedidoRechazado" },
  { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "CompensarPedido" }
]

ResultPath al Catch és crític. Amb "$.error", l'estat de destinació rep l'entrada original més l'objecte {"Error": "...", "Cause": "..."} sota error. Sense ell (valor per defecte $), l'error substitueix tota l'entrada i l'estat de compensació es queda sense saber quina comanda compensar ni quina referència de cobrament tornar. És la fallada més frustrant de depurar a Step Functions.

Errors predefinits que convé conèixer: States.Timeout, States.TaskFailed, States.Permissions, States.ResultPathMatchFailed, States.NoChoiceMatched, States.Heartbeat, States.DataLimitExceeded (la càrrega entre estats ha superat els 256 KB).

El patró saga: compensar el que ja s'ha fet

No hi ha transaccions distribuïdes entre una passarel·la de pagament, Aurora, DynamoDB i l'ERP d'un soci. L'alternativa realista és la saga: una seqüència de passos on cadascun té una compensació que desfà el seu efecte, i si alguna cosa falla s'executen les compensacions dels passos ja completats, en ordre invers.

Pas Compensació
Cobrar la targeta Tornar el cobrament (mercadofresco-devolver-cobro)
Reservar estoc Alliberar estoc (mercadofresco-liberar-stock)
Preparar al magatzem Cancel·lar l'ordre de preparació
Assignar repartiment Alliberar la franja del repartidor

La compensació no és un rollback: és una acció de negoci nova i visible. Una devolució apareix a l'extracte del client; els diners van estar retinguts. Per això les sagues es dissenyen per minimitzar el temps entre el pas arriscat i la seva possible compensació, i per això convé ordenar els passos de més a menys reversible quan es pugui, deixant els irreversibles per al final.

stateDiagram-v2
    [*] --> CobrarPago
    CobrarPago --> ReservarStock: cobrament OK
    CobrarPago --> PedidoRechazado: TarjetaRechazada
    ReservarStock --> PrepararEnAlmacen: estoc reservat
    ReservarStock --> DevolverCobro: SinStock
    PrepararEnAlmacen --> AsignarReparto: token success
    PrepararEnAlmacen --> LiberarStock: AlmacenNoPuedePreparar / Timeout / Heartbeat
    AsignarReparto --> NotificarCliente: repartiment assignat
    AsignarReparto --> LiberarStock: SinRepartidor
    NotificarCliente --> PedidoCompletado
    LiberarStock --> DevolverCobro: estoc alliberat
    DevolverCobro --> PedidoCompensado: cobrament tornat
    DevolverCobro --> RevisionManual: fallada en tornar
    PedidoCompletado --> [*]
    PedidoCompensado --> [*]
    PedidoRechazado --> [*]
    RevisionManual --> [*]

Fixa't en RevisionManual. Les compensacions també fallen, i quan falla una devolució hi ha diners d'un client retinguts sense comanda: això no pot acabar en un Fail silenciós. Aquest estat publica a alertas-mercadofresco i escriu en una cua que la Marta revisa. Tota saga necessita el seu camí de «això ja és per a un humà».

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." }
  }
}

Detalls que no són casuals. ResultPath: null a les compensacions i a la notificació: el seu resultat no aporta res i així no s'embruta l'estat. Més reintents a DevolverCobro (6) que a la resta: fallar una devolució és molt més car que fallar qualsevol altra cosa. PedidoRechazado no passa per compensació, perquè si la targeta es va rebutjar no es va cobrar res. I States.JsonToString($) és una de les funcions intrínseques del llenguatge; n'hi ha més (States.Format, States.Array, States.ArrayPartition, States.MathRandom, States.UUID) que eviten haver d'escriure una Lambda només per transformar dades.

Map distribuït per al volum del divendres

El Map en mode INLINE té dos límits: 40 iteracions concurrents i totes les dades a l'estat, subjectes al màxim de 256 KB. Per conciliar les 4.300 comandes d'un divendres contra els apunts de la passarel·la, o per processar un fitxer de 200.000 línies a S3, cal el mode 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"
}

Les diferències amb el mode INLINE són substancials:

INLINE DISTRIBUTED
Concurrència màxima 40 10.000
Origen dels elements Un array a l'estat Array, o S3: objectes, CSV, JSON, manifest
Execució de cada iteració Dins de l'execució pare Execució filla independent
Historial Compta al de la pare Propi, sense inflar el de la pare
Tolerància a fallades Tot o res ToleratedFailurePercentage
Volum pràctic Centenars Milions

ToleratedFailurePercentage: 2 permet que fins a un 2 % dels apunts falli sense avortar la conciliació sencera —el típic quan el fitxer del banc porta línies rares— i ItemBatcher agrupa 25 elements per invocació de Lambda, dividint per 25 el nombre d'invocacions i el seu cost. Com que cada iteració és una execució filla, l'historial de la pare no s'omple amb 4.300 entrades, que és el que feia inservible la consola amb el mode INLINE.

Desplegament i arrencada des d'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 necessita permís per a cada acció que la màquina executa: lambda:InvokeFunction sobre les cinc funcions, sqs:SendMessage sobre la cua del magatzem, events:PutEvents sobre el bus, sns:Publish sobre el tema d'alertes, més xray:PutTraceSegments i els permisos d'escriptura al grup de registres. Un States.Permissions enmig d'una compensació és de les fallades més desagradables que hi ha: la comanda queda a mitges perquè l'orquestrador no podia cridar qui tocava.

L'arrencada la fa 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 truc molt recomanable: passar com a nom d'execució un valor derivat del pedido_id. Els noms d'execució han de ser únics en 90 dies, així que si el mateix esdeveniment arriba dues vegades, la segona execució falla amb ExecutionAlreadyExists en lloc de cobrar la targeta un altre cop. És idempotència de franc, i enllaça directament amb 07-05.

Observabilitat: depurar una execució fallida

Step Functions desa l'historial complet de cada execució estàndard durant 90 dies: cada entrada d'estat, cada sortida, cada reintent, cada error. És la seva millor característica i no té equivalent en una orquestració escrita a mà.

# Execucions fallides 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

# L'historial complet d'una d'elles
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 execució fallida real. La Marta veu una execució en FAILED i recorre l'historial cap enrere:

  1. ExecutionFailed amb Error: "PedidoNoPreparable" → va acabar a PedidoCompensado, així que la compensació va funcionar. Bon senyal: el client té els seus diners.
  2. TaskStateExited de DevolverCobro i de LiberarStock → les dues compensacions es van executar.
  3. TaskFailed a PrepararEnAlmacen amb Error: "States.Heartbeat"no va ser un rebuig del magatzem, va ser falta de batec. El terminal va deixar d'enviar SendTaskHeartbeat.
  4. TaskScheduled de PrepararEnAlmacen a les 18:47, TaskFailed a les 18:57 → exactament els 600 segons de HeartbeatSeconds.

Diagnòstic: no és un problema de negoci, és que el terminal del magatzem perd la connexió wifi a la cambra de fred i deixa d'enviar batecs. La correcció no és a la màquina d'estats —que es va comportar correctament— sinó al terminal, que ha de reintentar el batec, i a apujar HeartbeatSeconds a 900 per tolerar els talls coneguts. Sense l'historial, aquest diagnòstic hauria trigat dies.

Amb --tracing-configuration '{"enabled":true}', X-Ray (05-02) mostra el mapa de serveis de l'execució i on se'n va anar el temps: quant a la passarel·la, quant esperant el magatzem, quant a Aurora. I les mètriques de CloudWatch a vigilar són ExecutionsFailed, ExecutionsTimedOut, ExecutionsAborted i ExecutionTime, totes amb alarma cap a alertas-mercadofresco.

Un detall de configuració: "level": "ERROR" registra només els estats fallits. "ALL" registra tot i és utilíssim mentre desenvolupes, però en producció, amb 240.000 execucions al mes i includeExecutionData: true, el volum de CloudWatch Logs pot costar més que la mateixa màquina.

Workflow Studio i cost

Workflow Studio és l'editor visual de la consola: arrossegues estats, configures integracions des de formularis i veus el JSON generar-se en temps real, amb validació al vol. És la millor manera d'aprendre el llenguatge i d'explorar les 9.000 accions de l'SDK, i també de dibuixar el primer esborrany d'un flux amb la Marta al davant. Per a producció, la definició viu al repositori i es desplega amb CloudFormation o CDK (mòdul 9): l'editor visual serveix per dissenyar i per llegir, no per ser la font de la veritat.

Cost. Els fluxos estàndard es cobren per transició d'estat: 4.000 de franc al mes i després 0,025 USD per 1.000. mercadofresco-procesar-pedido recorre uns 9 estats per comanda en el camí feliç.

Escenari Transicions/mes Cost
240.000 comandes × 9 estats (estàndard) 2.160.000 54,00 USD
Compensacions (1,2 % × 4 estats extra) 11.520 0,29 USD
mercadofresco-validar-carrito, exprés, 1,4 M execucions de 200 ms i 64 MB ~2,20 USD
Conciliació diària (DISTRIBUTED, 30 × 4.300 filles exprés) ~1,10 USD

La comparació estàndard/exprés és reveladora: el mateix procés en exprés costaria uns 3 USD en lloc de 54. Per què no fer servir exprés per a tot, doncs? Perquè el procés de comanda dura fins a una hora (el màxim d'exprés són 5 minuts), necessita waitForTaskToken asíncron, necessita semàntica d'exactament una vegada per no cobrar dues vegades, i necessita l'historial de 90 dies per a les reclamacions. Els 51 USD de diferència compren precisament això, i per a 240.000 comandes amb un import mitjà de 48 € és una fracció menyspreable de la facturació.

La regla pràctica: exprés per al que és curt, idempotent i d'alt volum; estàndard per al que és llarg, crític i s'ha de 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

Esborrar una màquina d'estats no cancel·la les execucions en curs: passen a ABORTED quan acabin els passos actuals, i les que esperen un testimoni queden òrfenes.

Errors Habituals i Consells

Deixar ResultPath per defecte en un Task intermedi. El resultat substitueix tota l'entrada i l'estat següent no troba les seves dades. Símptoma: States.Runtime amb «no s'ha pogut resoldre la ruta». Posa ResultPath explícit sempre, o null si el resultat no importa.

Oblidar ResultPath al Catch. L'estat de compensació rep només l'error i no sap quina comanda compensar ni quin cobrament tornar. És la fallada més frustrant d'aquesta lliçó.

Choice sense Default. Un cas no previst acaba en States.NoChoiceMatched i l'execució mor sense compensar res.

No distingir errors transitoris de permanents al Retry. Reintentar quatre vegades una TarjetaRechazada endarrereix la resposta al client i no arregla res. Declara MaxAttempts: 0 per als errors permanents, i posa'ls abans del bloc genèric.

Retry sense JitterStrategy: "FULL". Quan la dependència es recupera, totes les execucions reintenten alhora i la tornen a tombar.

Compensacions sense el seu propi Catch. Si la devolució falla i no hi ha camí a revisió manual, el client es queda sense comanda i sense diners, i ningú no se n'assabenta.

Passar càrregues grans entre estats. El límit és 256 KB. Un Map amb 5.000 elements peta amb States.DataLimitExceeded. Passa referències a S3, no continguts.

waitForTaskToken sense HeartbeatSeconds ni TimeoutSeconds. L'execució es queda esperant fins a l'any, ocupant una ranura i sense que ningú se n'adoni.

Fer servir exprés per a processos amb efectes no idempotents. La semàntica d'almenys una vegada significa que un cobrament es pot executar dues vegades.

Parallel sense Catch per branca. La fallada d'una branca cancel·la les altres, incloses les que ja anaven per la meitat.

Consell: anomena l'execució amb l'identificador de negoci. PED-2026-084417 com a nom d'execució dona idempotència de franc, i cercar a la consola passa d'impossible a immediat.

Consell: valida el flux de dades amb Pass abans d'escriure la lògica. Munta la màquina sencera amb estats Pass que retornen dades fictícies, comprova que el JSON arriba bé d'un extrem a l'altre, i només aleshores substitueix cada Pass pel seu Task.

Consell: "level": "ALL" en desenvolupament, "ERROR" en producció. El registre complet amb dades d'execució és caríssim a 240.000 execucions al mes.

Exercicis

Exercici 1: la devolució d'una comanda lliurada

Dissenya mercadofresco-procesar-devolucion per quan un client torna una comanda ja lliurada. Passos: (1) validar que la devolució és dins de termini (48 h); (2) generar l'etiqueta de recollida cridant l'API del transportista; (3) esperar que el transportista confirmi la recollida, que pot trigar fins a 3 dies; (4) quan arribi al magatzem, un operari inspecciona l'estat del producte i decideix acceptar, acceptar parcialment o rebutjar; (5) segons la decisió, tornar l'import total, parcial o cap; (6) reposar l'estoc només si es va acceptar.

Indica: (a) estàndard o exprés i per què; (b) quin tipus d'estat fas servir a cada pas; (c) com modeles els passos 3 i 4; (d) quin Retry i quin Catch poses al pas 5; (e) quines compensacions necessites i quines no.

Exercici 2: traçar el flux de dades

Donat aquest estat i aquesta entrada, escriu la sortida exacta que rep l'estat següent.

"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"}. La Lambda retorna {"nivel_fidelidad": "oro", "email": "[email protected]", "telefono": "+34600000000"}.

Respon: (a) què rep exactament la Lambda; (b) la sortida completa de l'estat; (c) què passaria si es tragués ResultPath; (d) què passaria si OutputPath fos "$.cliente"; (e) per què InputPath no afecta on insereix ResultPath.

Exercici 3: coreografia o orquestració

Per a cada cas, decideix i justifica en dues frases: (a) en confirmar una comanda cal avisar quatre equips independents; (b) l'alta d'un proveïdor requereix validar documents, aprovació de dues persones i alta a Aurora, amb termini de 10 dies; (c) cal redimensionar 8.000 fotos del catàleg cada nit; (d) en detectar StockBajo cal demanar al proveïdor, esperar confirmació i actualitzar la data prevista; (e) cal registrar cada canvi de preu a Redshift per a auditoria.

Solucions

Solució 1

(a) Estàndard, sens dubte. El pas 3 pot durar 3 dies, molt per damunt dels 5 minuts d'exprés. A més hi ha diners pel mig i cal la semàntica d'exactament una vegada i l'historial de 90 dies per a les reclamacions.

(b) i (c) Els estats. (1) Choice comparant $$.Execution.StartTime amb la data de lliurament; si és fora de termini, Fail amb Error: "FueraDePlazoDevolucion" sense compensar res, perquè encara no s'ha fet res. (2) Task amb Retry i JitterStrategy: FULL cap a l'API del transportista. (3) Task amb .waitForTaskToken i TimeoutSeconds: 259200 (3 dies): el webhook del transportista crida SendTaskSuccess; un Wait no serviria perquè no sabem quan passarà, només el termini màxim, i aquí no convé HeartbeatSeconds perquè el transportista no envia batecs. (4) Un altre Task amb .waitForTaskToken cap a la cua del magatzem, amb TimeoutSeconds: 86400; la decisió de l'operari arriba a l'output del SendTaskSuccess i un Choice posterior encamina segons $.inspeccion.decision (aceptada/parcial/rechazada), amb Default a revisió manual. (5) Task de devolució amb import calculat. (6) Map per línia per reposar estoc.

(d) Retry i Catch del pas 5. Retry generós —6 intents, IntervalSeconds: 10, BackoffRate: 2, MaxDelaySeconds: 300, JitterStrategy: FULL— perquè una devolució fallida és el pitjor que pot passar aquí. Catch sobre States.ALL amb ResultPath: "$.error" cap a un estat RevisionManualDevolucion que publica a alertas-mercadofresco. Mai un Fail directe: deixaria el client sense producte i sense diners.

(e) Compensacions. Aquí n'hi ha moltes menys que al procés de comanda, i el motiu és interessant: el flux va de menys a més compromès, i els passos irreversibles són al final. L'etiqueta de recollida sí que necessita compensació (cancel·lar-la) si la devolució s'avorta abans de la recollida. La inspecció no necessita compensació: és una lectura. La devolució de l'import no es compensa tornant a cobrar —això seria inacceptable—; si es detecta un error posterior s'obre una incidència manual. I la reposició d'estoc es compensa retirant-lo si després es descobreix que el producte no era apte. Lliçó general: ordenar els passos de més reversible a menys reversible redueix el nombre de compensacions necessàries.

Solució 2

(a) Què rep la Lambda. InputPath: "$.pedido" redueix l'entrada a {"pedido_id": "PED-1", "cliente_id": "CLI-9", "importe_eur": 30.0}. Sobre això, Parameters construeix la càrrega, i "id.$": "$.cliente_id" es resol contra el resultat d'InputPath, no contra l'entrada original. La Lambda rep exactament:

{ "id": "CLI-9" }

(b) Sortida completa. La resposta de la integració és {"ExecutedVersion": "...", "Payload": {"nivel_fidelidad": "oro", "email": "[email protected]", "telefono": "+34600000000"}, "StatusCode": 200}. ResultSelector la retalla a {"nivel": "oro", "email": "[email protected]"} —el telèfon es descarta— i ResultPath: "$.cliente" la insereix a l'entrada original completa, no a la retallada per InputPath. Amb OutputPath: "$" surt tot:

{
  "pedido": { "pedido_id": "PED-1", "cliente_id": "CLI-9", "importe_eur": 30.0 },
  "origen": "web",
  "cliente": { "nivel": "oro", "email": "[email protected]" }
}

(c) Sense ResultPath. El valor per defecte és $, així que el resultat substitueix tota l'entrada. La sortida seria {"nivel": "oro", "email": "[email protected]"} i AplicarDescuento no trobaria $.pedido.importe_eur, i fallaria amb un error de resolució de ruta. És l'error més comú del llenguatge.

(d) Amb OutputPath: "$.cliente". La sortida seria només {"nivel": "oro", "email": "[email protected]"}. Es perd la comanda igualment, però per un motiu diferent: no és que el resultat l'hagi substituïda, és que s'ha retallat al final. Il·lustra bé que ResultPath i OutputPath actuen en moments diferents i poden espatllar el mateix per vies diferents.

(e) Per què InputPath no afecta ResultPath. InputPath només determina què es passa a Parameters per construir la crida; l'estat conserva internament l'entrada original i és sobre aquesta que ResultPath insereix. Aquesta separació és deliberada i molt útil: permet enviar al servei una vista reduïda sense perdre el context acumulat del procés.

Solució 3

(a) Coreografia. Quatre interessats independents en el mateix fet, sense dependències entre ells i sense res a compensar: és fan-out pur. SNS o EventBridge, segons si cal encaminar per contingut.

(b) Orquestració. Procés llarg (10 dies), amb passos dependents, dues intervencions humanes —waitForTaskToken per partida doble— i necessitat de saber en quin punt va cada alta. Amb esdeveniments caldria inventar una taula d'estat i un vigilant, que és reimplementar Step Functions pitjor.

(c) Cap dels dos com a orquestració clàssica: un Map distribuït, o simplement S3 → SQS → Lambda si no cal control del conjunt. Són 8.000 tasques independents i idempotents; el que es necessita és paral·lelisme i tolerància a fallades parcials, no un flux amb estat. Si a més vols saber quan van acabar totes i amb quina taxa d'error, el Map distribuït amb ToleratedFailurePercentage i ResultWriter és l'opció correcta.

(d) Orquestració. Hi ha una espera de durada desconeguda (la confirmació del proveïdor) i passos encadenats. Màquina estàndard arrencada per la regla de StockBajo, amb waitForTaskToken per a la confirmació i Wait/Choice per reclamar si el proveïdor no contesta en 24 hores.

(e) Coreografia. Un fet solt que interessa a un consumidor, sense dependències ni compensació. Esdeveniment a bus-mercadofresco → cua → càrrega a Redshift. Ficar-hi Step Functions només afegiria cost per transició i una peça més per mantenir.

Conclusió

El procés de comanda de MercadoFresco té ara un amo explícit. mercadofresco-procesar-pedido sap cobrar, reservar l'estoc línia a línia amb un Map, esperar fins a una hora que una persona del magatzem confirmi la caixa mitjançant un testimoni de retorn amb batec, assignar repartiment, publicar PedidoProcesado a bus-mercadofresco i —el que cap coreografia sabia fer— desfer el que s'ha fet quan alguna cosa falla: alliberar l'estoc, tornar el cobrament i, si fins i tot la compensació falla, avisar la Marta per alertas-mercadofresco en lloc de morir en silenci. L'historial de 90 dies va convertir un misteri («per què s'ha compensat aquesta comanda?») en quatre entrades llegides de baix a dalt: el terminal del magatzem perdia el wifi a la cambra de fred.

Les idees que cal endur-se: l'elecció entre coreografia i orquestració no és de gust, la dicten les dependències entre passos i la necessitat de compensar; estàndard enfront d'exprés es decideix per durada, semàntica d'execució i necessitat d'auditoria, i els 51 USD de diferència mensual compren exactament això; el flux de dadesInputPath, Parameters, ResultSelector, ResultPath, OutputPath— és on es perden més hores, i la regla d'or és ResultPath explícit en tot Task intermedi; Retry amb fluctuació (jitter) i errors classificats evita que la recuperació d'una dependència la torni a tombar; i tota saga necessita el seu camí a revisió manual, perquè les compensacions també fallen.

Amb això, MercadoFresco té les quatre peces d'integració: cues per desacoblar, temes per repartir, un bus per encaminar per contingut i fluxos per orquestrar. Però queden preguntes que travessen les quatre i que fins ara hem anat ajornant. Què significa exactament «almenys una vegada» quan el que es duplica és un cobrament? Com s'escriu un consumidor que pugui rebre el mateix missatge tres vegades sense cobrar tres vegades? Quantes vegades cal reintentar, i quan cal deixar de fer-ho per no tombar qui s'està recuperant? Què es fa exactament amb els missatges d'una DLQ un dilluns al matí? I com es garanteix que una comanda escrita a Aurora es publica sempre, si no hi ha transacció que abasti la base de dades i el bus?

A 07-05, «Patrons d'integració», tanquem el mòdul amb les respostes: garanties de lliurament i idempotència amb taula de deduplicació a DynamoDB, retrocés exponencial amb fluctuació, interruptor de circuit per al proveïdor de pagaments, el runbook de la DLQ, ordre i agrupació, el patró outbox per a la doble escriptura, contrapressió, i una taula decisòria final que respon d'una vegada la pregunta «SQS, SNS, EventBridge, Step Functions o una crida síncrona?».

Curs d'AWS

Mòdul 1: Introducció a AWS

Mòdul 2: Serveis principals d'AWS

Mòdul 3: Xarxes i lliurament de contingut

Mòdul 4: Seguretat i identitat

Mòdul 5: Monitoratge i gestió

Mòdul 6: Bases de dades

Mòdul 7: Integració d'aplicacions

Mòdul 8: Eines per a desenvolupadors

Mòdul 9: Infraestructura com a codi i govern de comptes

Mòdul 10: Contenidors a AWS

Mòdul 11: Millors pràctiques i gestió de costos

© Copyright 2026. Tots els drets reservats