Step Functions
After this you can answer: How would you design a booking and payment saga without pretending distributed operations are one transaction?
Picture it
Section titled “Picture it”Where it sits
Section titled “Where it sits”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 awsLegend: 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.
How it works
Section titled “How it works”- 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.
ReserveSeatscreates 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.TakePaymentstarts a payment attempt using a stable payment idempotency key. An idempotency key identifies one intended operation so repeated requests return the same result.- 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.
- 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. - 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 confirmedLegend: 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.
Under the hood
Section titled “Under the hood”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
| State | What happens |
|---|---|
Task | Call a service, Lambda function, or worker. |
Choice | Evaluate conditions and select the next state. |
Parallel | Run branches concurrently, then combine results. |
Map | Apply a workflow to each item in a collection. |
Wait | Pause until a duration or timestamp is reached. |
Pass | Pass or transform data without calling another service. |
Succeed | Finish successfully. |
Fail | Finish 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
Limits and numbers
Section titled “Limits and numbers”At the time of writing; check the AWS docs before choosing capacity or pricing. These are service facts, not MiniTickets measurements.
| Property | Standard | Express | Source |
|---|---|---|---|
| Maximum execution duration | 1 year | 5 minutes | AWS quotas |
| Execution semantics | Exactly once, except configured retries | Async: at least once; sync: at most once | AWS workflow types |
| Billing basis | State transitions | Executions, duration, memory | AWS workflow types |
| Task, state, execution input/output | 256 KiB | 256 KiB | AWS quotas |
| Execution history events | 25,000 per execution | Use configured CloudWatch Logs | AWS quotas |
.sync and callback patterns | Supported for compatible integrations | Unsupported | AWS 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.
In MiniTickets
Section titled “In MiniTickets”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.
Connections
Section titled “Connections”These are proposed connections, unless marked existing.
| Connection | What allows it | What the permission does not do |
|---|---|---|
| API → Step Functions | API task role: states:StartExecution on the proposed state machine ARN | It does not authorize its downstream tasks. |
| Step Functions → task Lambda | Execution-role trust for states.amazonaws.com; lambda:InvokeFunction on selected function ARNs | It does not authorize that Lambda’s database access. |
| Payment callback adapter → waiting task | Adapter role: states:SendTaskSuccess / states:SendTaskFailure; valid task token | It does not prove the provider sent a legitimate payment event. |
| Task Lambda → Postgres | Proposed network route, database credentials, database security-group rule | IAM invocation permission alone cannot open a database connection. |
| Existing outbox → booking-events topic | API task role: sns:Publish on minitickets-dev-booking-events | SNS 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.
When it fails
Section titled “When it fails”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 designedLegend: red = failure requiring recovery; all payment and compensation operations are designed, not built.
| What you see | Likely cause | Fix |
|---|---|---|
States.Timeout | Payment callback never arrives or a task exceeds its deadline. | Check provider state, then compensate; set explicit task and workflow deadlines. |
Lambda.TooManyRequestsException | Tasks exceed available Lambda concurrency. | Reduce workflow fan-out, bound retries, and protect capacity for other functions. |
States.Permissions or downstream access denial | The 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.DataLimitExceeded | A task returns too much JSON or results accumulate. | Return IDs, discard unnecessary outputs, and store large data in S3. |
| Booking pending after a failed refund | Compensation 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 / 100x
Section titled “At 10x / 100x”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.
Common interview traps
Section titled “Common interview traps”- “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 question
Section titled “Design question”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 designedLegend: 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
Say it in an interview
Section titled “Say it in an interview”Follow-ups
Section titled “Follow-ups”Why Standard rather than Express for this design?
What if payment succeeded but the task timed out?
What if refund also fails?
Can Step Functions publish directly to SNS?
When would you avoid Step Functions?
Remember it
Section titled “Remember it”- 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
Section titled “Advanced”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.
aws stepfunctions list-state-machines --region us-east-1aws 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.
Checkpoint
Section titled “Checkpoint”Answer these without scrolling up. If one feels hard, reread that section.
- Which operation makes seat confirmation safe when expiry races a callback?
- What is the difference between Retry and compensation?
- Why does Standard’s execution guarantee not eliminate payment idempotency?
- Which integration waits for a callback, and which workflow type supports it?
- 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/