Skip to content

Step Functions

10 min read · main part only; folded sections are optional

After this you can answer: How would you design a booking and payment saga without pretending distributed operations are one transaction?

The existing API would start a workflow after recording the booking request durably. Each proposed task owns a small business operation. The arrows below describe proposed permissions; they are not grants in the current stack.

flowchart LR
api["minitickets-dev-api"] -.->|"starts booking - states:StartExecution, proposed API role grant"| sf["Step Functions (designed, not built)"]
sf -.->|"reserves seats - lambda:InvokeFunction, proposed execution role"| seats["ReserveSeats Lambda (designed, not built)"]
sf -.->|"requests payment - lambda:InvokeFunction, proposed execution role"| pay["TakePayment Lambda (designed, not built)"]
sf -.->|"confirms booking - lambda:InvokeFunction, proposed execution role"| confirm["ConfirmBooking Lambda (designed, not built)"]
confirm -.->|"records event - proposed VPC route, SG TCP 5432, DB credentials"| db["RDS Postgres"]
class sf service
class sf designed
class api app
class seats,pay,confirm app
class seats,pay,confirm designed
class db aws

Legend: highlighted = Step Functions; app = MiniTickets code; AWS = managed service; dashed = designed, not built.

Open the existing booking flow in the map →. There is no Step Functions map piece because it is not implemented in this repository.

  1. The API accepts a stable booking ID and records a pending request. A durable dispatcher starts the execution with that ID. This closes the failure window where the API commits a request and crashes before starting its workflow.
  2. ReserveSeats creates a temporary hold in a short Postgres transaction. A hold prevents another buyer taking those seats until its expiry time. It is a proposed business record, different from the current Redis inventory reservation.
  3. TakePayment starts a payment attempt using a stable payment idempotency key. An idempotency key identifies one intended operation so repeated requests return the same result.
  4. The execution waits for an authenticated payment callback. A callback is a later request reporting the result. The hold deadline sets how long this wait may last.
  5. Success leads to ConfirmBooking. This transaction verifies that the hold is still valid, changes the booking state, and records an outbox event. An outbox is a database row recording an event to publish after the transaction commits.
  6. A failure takes a compensation path. Compensation is a new business operation that reverses an earlier effect: refund a captured payment, then release the hold. It can fail and needs its own retry and alert policy.
sequenceDiagram
participant API as MiniTickets API
box rgb(237,233,254) Step Functions
participant SF as Step Functions (designed, not built)
end
participant Seats as ReserveSeats (designed, not built)
participant Pay as Payment adapter (designed, not built)
participant Confirm as ConfirmBooking (designed, not built)
API->>SF: StartExecution with bookingId
SF->>Seats: Reserve with hold deadline
Seats-->>SF: holdId and expiresAt
SF->>Pay: Start payment with idempotency key and task token
Pay-->>SF: SendTaskSuccess after verified payment
SF->>Confirm: Confirm active hold and record outbox event
Confirm-->>SF: Booking confirmed

Legend: the API exists; workflow participants and payment integration are designed, not built; arrows show request order.

This sequence shows the successful path. A timeout does not prove the payment failed. The provider may have captured money before its response was lost. Query the payment using its stable reference before deciding whether to retry, refund, or require investigation.

Step Functions executes an Amazon States Language definition. Amazon States Language, or ASL, is a JSON document declaring states, transitions, and error handling. A transition moves execution to another state. An execution role is the IAM role Step Functions assumes to call downstream services; the calling API’s role is a separate identity.

Standard workflows persist their state between transitions. Waiting does not occupy one of our Lambda execution environments. AWS manages the workflow engine; our code still implements seat ownership and payment correctness. Express workflows use a different execution model and do not persist internal execution state between transitions in the same way. AWS workflow types

StateWhat happens
TaskCall a service, Lambda function, or worker.
ChoiceEvaluate conditions and select the next state.
ParallelRun branches concurrently, then combine results.
MapApply a workflow to each item in a collection.
WaitPause until a duration or timestamp is reached.
PassPass or transform data without calling another service.
SucceedFinish successfully.
FailFinish with an error.

These are the state types in AWS’s state reference. Parallel provides concurrency, not a database transaction. If one branch charges a card before another fails, cancellation does not reverse that charge.

Three integration patterns control what completion means. Request-response continues when the API request returns. .sync waits for a supported job to finish, such as an ECS task. .waitForTaskToken waits for a later SendTaskSuccess or SendTaskFailure call containing a task token. A task token identifies the waiting task; treat it as sensitive data. Express supports request-response, while job and callback patterns require Standard. AWS integration patterns

Retry repeats selected failures with bounded delays. Catch routes an error to recovery after applicable retries are exhausted. Match temporary throttling differently from a card decline. Exponential backoff increases the delay after failures; jitter varies that delay so many executions do not retry together. States.ALL does not catch every possible failure, including runtime errors and payload-limit errors unless handled as AWS documents. AWS error handling

State input and output are JSON. Keep IDs and small results in the workflow. Store images and large documents in S3 and pass object references. JSONPath selects parts of JSON with paths such as $.bookingId; JSONata is another supported language that selects and transforms JSON. AWS recommends JSONata for new workflows; a definition that omits the language defaults to JSONPath. AWS data processing

At the time of writing; check the AWS docs before choosing capacity or pricing. These are service facts, not MiniTickets measurements.

PropertyStandardExpressSource
Maximum execution duration1 year5 minutesAWS quotas
Execution semanticsExactly once, except configured retriesAsync: at least once; sync: at most onceAWS workflow types
Billing basisState transitionsExecutions, duration, memoryAWS workflow types
Task, state, execution input/output256 KiB256 KiBAWS quotas
Execution history events25,000 per executionUse configured CloudWatch LogsAWS quotas
.sync and callback patternsSupported for compatible integrationsUnsupportedAWS integration patterns

At least once means a run may happen again. At most once means a run is not repeated automatically, but it does not promise that every requested business operation completes. Exactly once describes Standard’s workflow execution semantics; it is not an end-to-end transaction with a payment provider.

Explain-only: designed, not built. There is no state machine, payment adapter, payment secret, or workflow execution role in infra/src today.

Current booking correctness lives in Postgres. apps/api/src/queries/bookings.ts changes seats and records the booking with its outbox event. apps/api/src/queries/outbox.ts locks pending rows and records successful delivery; apps/api/src/services/outbox.ts publishes through apps/api/src/queues/jobs.ts. SNS distributes booking events to independent email and tickets queues. See the outbox chapter and SNS fan-out.

That is enough for today’s work: confirm a booking, then send email and generate tickets independently. Neither result determines whether the already-confirmed booking exists. Adding a coordinator would introduce another state definition, deployment, role, and failure surface without replacing the outbox’s transaction guarantee.

Step Functions becomes useful if a booking must span payment, temporary seat holds, expiry, cancellation, and refunds over time. The database still decides whether a hold can be confirmed. A workflow’s delayed callback cannot turn an expired hold into a valid booking. Keep the existing API layers; a service would request orchestration through a dedicated integration module rather than putting payment logic in a tRPC procedure.

These are proposed connections, unless marked existing.

ConnectionWhat allows itWhat the permission does not do
API → Step FunctionsAPI task role: states:StartExecution on the proposed state machine ARNIt does not authorize its downstream tasks.
Step Functions → task LambdaExecution-role trust for states.amazonaws.com; lambda:InvokeFunction on selected function ARNsIt does not authorize that Lambda’s database access.
Payment callback adapter → waiting taskAdapter role: states:SendTaskSuccess / states:SendTaskFailure; valid task tokenIt does not prove the provider sent a legitimate payment event.
Task Lambda → PostgresProposed network route, database credentials, database security-group ruleIAM invocation permission alone cannot open a database connection.
Existing outbox → booking-events topicAPI task role: sns:Publish on minitickets-dev-booking-eventsSNS acceptance does not mean subscribers finished.

The callback endpoint must verify the provider’s signature and compare booking ID, amount, currency, and payment reference with stored expectations. Do not give the external payment provider AWS callback credentials. Let our adapter make the authenticated AWS request.

flowchart TB
timeout["Payment result times out"] --> inspect["Read provider result by payment reference"]
inspect -->|"Captured"| refund["Refund once with stable key"]
inspect -->|"Not captured"| release["Release hold conditionally"]
refund -->|"Refund accepted"| release
refund -->|"Retries exhausted"| alarm["Record unresolved refund and alert"]
release --> done["Finish compensation"]
class timeout,alarm failure
class inspect,refund,release app
class inspect,refund,release designed
class done aws
class timeout,inspect,refund,release,alarm,done designed

Legend: red = failure requiring recovery; all payment and compensation operations are designed, not built.

What you seeLikely causeFix
States.TimeoutPayment callback never arrives or a task exceeds its deadline.Check provider state, then compensate; set explicit task and workflow deadlines.
Lambda.TooManyRequestsExceptionTasks exceed available Lambda concurrency.Reduce workflow fan-out, bound retries, and protect capacity for other functions.
States.Permissions or downstream access denialThe execution role lacks a target permission, or a target role lacks datastore permission.Identify which identity made the denied call; repair only its required grant.
States.DataLimitExceededA task returns too much JSON or results accumulate.Return IDs, discard unnecessary outputs, and store large data in S3.
Booking pending after a failed refundCompensation failed or a workflow stopped before recovery finished.Persist unresolved work, alert, reconcile, and retry with the same refund key.

The AWS error reference lists error names. A workflow marked failed is an operational signal; it does not prove every prior effect was reversed. A task’s Catch cannot handle a top-level execution timeout or manual stop. A separate reconciler must recover business effects if the whole execution ends before compensation. Preserve provider references and an audit record outside short-lived logs.

At 10x, payment rate limits or Lambda concurrency may fail before the workflow engine does. Set bounded retries and task concurrency around the smallest downstream capacity. A retry consumes real downstream capacity and can repeat a side effect. Measure executions started, task duration, failed and timed-out executions, and the age of unresolved bookings.

At 100x, a flash sale can create many simultaneous holds, callbacks, and transitions. Standard’s state-transition and start rates have account/Region quotas; verify them for our Region rather than copying a global number. A Map state needs a deliberate concurrency limit. Split large fan-out work into child workflows when execution history or payload size becomes a constraint. AWS quotas

Scaling the workflow does not increase the number of seats. Use database conditions to prevent overselling, rate-limit payment attempts, and make overload visible to the caller. Workflows also cost money on unsuccessful paths. Compare transition counts and retry rates, not only successful bookings.

  • “Standard means I can retry a charge safely.” Its execution guarantee does not identify duplicate user requests or resolve an ambiguous external response. Payment operations still need stable idempotency keys and reconciliation.
  • “Catch rolls back everything.” Catch changes the next state. You must implement refund and release operations, and handle their failures.
  • “A Wait state keeps Lambda running.” The workflow waits; a completed Lambda invocation has ended.
  • “Use Parallel to make booking and payment atomic.” Concurrent operations remain separate transactions. Use conditional business state and compensation.

Design a ten-minute paid booking hold that never issues a ticket for an unpaid booking.

Use a Standard workflow for durable waiting and callbacks. The ten-minute hold is a proposed product rule, not an AWS default. Reserve seats conditionally with an absolute expiry. Start payment with a stable key and keep the provider reference. Set the waiting task’s deadline from the remaining hold time. A verified successful callback can confirm only an active hold; the confirmation transaction writes the outbox event that starts ticket issuance.

flowchart TB
reserve["ReserveSeats"] --> pay["TakePayment and await callback"]
pay -->|"Verified payment before expiry"| confirm["ConfirmBooking and outbox"]
confirm --> issue["Issue tickets through existing queue"]
pay -->|"Declined or deadline"| check["Reconcile payment result"]
confirm -->|"Hold expired or confirmation failed"| refund["Refund captured payment"]
check -->|"Captured"| refund
check -->|"Not captured"| release["ReleaseSeats"]
refund --> release
class reserve,pay,confirm,check app
class reserve,pay,confirm,check designed
class issue aws
class refund,release failure
class refund,release designed
class reserve,pay,confirm,check,refund,release designed

Legend: this is a proposed state machine; only the existing queue-based ticket issuance is built.

A callback and expiry can race. Serialize the business decision with a database condition, rather than trusting arrival order. If payment succeeds after expiry, refund it; never confirm seats already sold to someone else. Compensation is a saga: a series of local transactions with explicit recovery, not one cross-service transaction. AWS saga guidance

Why Standard rather than Express for this design?
We need durable waiting and task-token callbacks. Express has a five-minute maximum and does not support the callback pattern.
What if payment succeeded but the task timed out?
Query the provider by the persisted payment reference. Retrying blindly could charge twice; use its idempotency mechanism and reconcile the result.
What if refund also fails?
Record unresolved compensation durably, retry within a bounded policy, alert, and support operator reconciliation. Do not mark the business process fully recovered.
Can Step Functions publish directly to SNS?
Yes, with a supported integration and execution-role permission. Publishing alone does not atomically commit a Postgres booking, so retain the outbox where that atomicity matters.
When would you avoid Step Functions?
For one short operation or independent queue consumers where no central process must wait, branch, or compensate. Added orchestration must solve a real coordination problem.
  • Standard persists progress; a waiting workflow does not keep Lambda running.
  • Retry repeats an operation; Catch routes an error; compensation is application code.
  • A payment timeout means the result is unknown until reconciled.
  • Keep payloads small and make every externally visible effect idempotent.
  • MiniTickets uses outbox and queues today; the payment saga is designed, not built.
Advanced: State data, task tokens, and read-only inspection

With JSONPath, InputPath selects input; Parameters forms a task request; ResultSelector trims a result; ResultPath merges it; OutputPath selects outgoing data. Accidentally replacing the full input can lose the booking ID. With JSONata, use Arguments, Output, and Assign instead. Do not mix field sets without checking the state’s query language.

A JSONPath retry fragment for temporary Lambda throttling could use ErrorEquals: ["Lambda.TooManyRequestsException"], IntervalSeconds: 2, BackoffRate: 2, MaxAttempts: 3, and JitterStrategy: "FULL". Those values are design choices. Handle business declines separately, and ensure total retry time fits the hold deadline.

Console: Step Functions → State machines → selected machine → Executions → selected execution → event history. Compare the failed state’s input, result, role, and retry attempts. These commands inspect existing resources; this repository does not provide a state machine ARN.

Terminal window
aws stepfunctions list-state-machines --region us-east-1
aws stepfunctions describe-state-machine --state-machine-arn "$STATE_MACHINE_ARN"
aws stepfunctions describe-execution --execution-arn "$EXECUTION_ARN"

Avoid printing execution input or history if it contains payment details, secrets, or personal data. Prefer a deliberately redacted diagnostic view. Limit logging at design time, and keep task tokens out of ordinary application logs.

Answer these without scrolling up. If one feels hard, reread that section.

  1. Which operation makes seat confirmation safe when expiry races a callback?
  2. What is the difference between Retry and compensation?
  3. Why does Standard’s execution guarantee not eliminate payment idempotency?
  4. Which integration waits for a callback, and which workflow type supports it?
  5. Why is Step Functions unnecessary for today’s independent email and ticket consumers?

Redraw it from memory: the reserve, pay, confirm, refund and release state machine

Try this: compare the existing outbox transaction with the proposed workflow start and identify the crash window

Read this book online: https://book.theversehub.in/