Based on the DVA-C02 exam guideReport an error
SQS, SNS, and Kinesis, which we've covered so far, were tools for separating components. But real business processes require weaving back together in order the pieces you've separated. "Validate the order → check inventory → process payment → refund if it fails → send a notification if it succeeds." If you write this into a Lambda with if-else and try-catch, that function soon becomes bloated, and the state hides deep in the code so you can't tell "which step it stopped at right now." AWS Step Functions takes this flow (workflow) out of the code and turns it into an explicit state machine. AppSync applies a similar spirit to the data layer, letting you declare the "flow" of gathering data from multiple backends in a single GraphQL query.
In DVA-C02, Step Functions tends to show up in forms like "how do you orchestrate Lambda," "the difference between Standard and Express," and "how do you wait for a response from an external system." AppSync is tested as "real-time subscriptions," "multiple data sources," and "the difference from API Gateway." This article covers why separating the workflow from the code is valuable, the theoretical foundation of state machines, the problems solved by service integration patterns (.sync, .waitForTaskToken), and where GraphQL is fundamentally different from REST.
Consider a case where 5 Lambda functions must run in order. The simplest approach is a chain, where function A invokes function B directly when it finishes, and B invokes C. This is called choreography. Without a central conductor, each one knows what comes next. This approach falls apart quickly. If an error occurs at C, how do you roll back A and B? Which step is the workflow at now? What if you want to retry B? This logic is scattered across the functions and the code becomes tangled.
Orchestration is the opposite. A central conductor (Step Functions) knows the whole flow, calls each step, and manages errors, retries, branching, and parallelism. Each Lambda does only its own job and doesn't know "what to do next." The flow logic is gathered in one place not as code but as a declarative definition (Amazon States Language, JSON), so it is visualized and leaves an execution history.
{
"StartAt": "ValidateOrder",
"States": {
"ValidateOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:ValidateOrder",
"Next": "CheckInventory",
"Catch": [{"ErrorEquals": ["ValidationError"], "Next": "OrderFailed"}]
},
"CheckInventory": {
"Type": "Choice",
"Choices": [{"Variable": "$.inStock", "BooleanEquals": false, "Next": "Refund"}],
"Default": "ProcessPayment"
},
"ProcessPayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:ProcessPayment",
"Retry": [{"ErrorEquals": ["Lambda.ServiceException"],
"IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2.0}],
"Next": "OrderSuccess"
},
"OrderSuccess": {"Type": "Succeed"},
"OrderFailed": {"Type": "Fail", "Error": "OrderError"},
"Refund": {"Type": "Task", "Resource": "arn:aws:lambda:...:Refund", "Next": "OrderFailed"}
}
}What Step Functions implements is the fundamental computer science concept of a Finite State Machine (FSM). It is a model in which a system is in one of a finite set of states, and transitions between states occur according to input. FSMs have been the foundation of automata theory since the 1950s (Mealy and Moore machines), and regular expression engines, network protocols, and UI state management are all FSMs. Step Functions applies this abstraction to distributed workflows, where each "State" expresses a task, branch, parallel run, or wait, and transitions are declared with Next. When you model a flow as an FSM, you can visually catch defects such as "unreachable states," "missing transitions," and "infinite loops." These are things you couldn't see when they were buried in code.
Orchestration vs. choreography is a long-standing debate in microservice design.
Step Functions has two workflow types, and the exam includes questions that turn on which of the two you choose. Both use the same state language, but the execution engine and the guarantees differ.
| Item | Standard | Express |
|---|---|---|
| Maximum execution time | 1 year | 5 minutes |
| Execution model | Exactly-once | At-least-once |
| Throughput | 2,000 starts/s | 100,000 starts/s |
| Execution history | Console + CloudWatch (90-day lookup) | CloudWatch Logs only |
| Billing | Per state transition | Execution time + memory |
| Use | Long/critical workflows (human approval, deployments, orders) | Short, frequent event processing (IoT, streams) |
The core intuition is this. Use Standard for flows that "take a long time and where getting it right once matters." Examples are a deployment pipeline that waits days for human approval, or an order that must be paid for exactly once. Use Express for flows that are "short, bursty, and idempotent." An example is quickly transforming tens of thousands of events per second. Express being at-least-once means duplicate executions are possible, so the consumer must be idempotent (the same principle we saw with SQS Standard).
⚠️ Trap: If you choose Standard for "processing tens of thousands of short events per second," you get blocked on throughput (2,000/s) and on cost (per-state-transition billing accumulates). The answer is Express. Conversely, for "pay exactly once, waiting days for human approval," Express is unsuitable because of its 5-minute limit and at-least-once, and Standard is the answer. Execution time (1 year vs. 5 minutes) and execution guarantee (exactly-once vs. at-least-once) are the two deciding axes.
When a Task state calls an external service, "when does it move on to the next state" differs by pattern. These are the three patterns where the exam is likely to ask you to tell them apart.
| Pattern | URI format | Behavior |
|---|---|---|
| Request-Response | arn:aws:states:::lambda:invoke | Calls and gets an immediate response, then moves on |
Run a Job (.sync) | arn:aws:states:::ecs:runTask.sync | Waits until the job completes |
Wait for Callback (.waitForTaskToken) | arn:aws:states:::sqs:sendMessage.waitForTaskToken | Waits until an external party responds with the token |
Request-Response is the simplest. You call, and when the response arrives, you're done. .sync is the pattern of "start a long-running job and pause the workflow, waiting until it finishes." ECS tasks or Glue jobs take minutes to hours, and with .sync, Step Functions polls for their completion, waits, and then moves on.
.waitForTaskToken is for cases of waiting for the response of a person or an external system rather than a machine. Step Functions issues a task token along with the task and pauses the workflow. When an external party (a button pressed by an approver, a callback from an external payment provider, etc.) calls SendTaskSuccess/SendTaskFailure with that token, the workflow resumes. It doesn't matter if it takes days (within Standard's 1-year limit).
The classic use of .waitForTaskToken is the human-in-the-loop workflow. When a refund request exceeds a certain amount, the workflow sends an approval email/Slack message (including the token) to a manager and stops. When the manager clicks the approval link, a callback arrives with that token, the workflow resumes, and the refund proceeds. This pattern, which naturally integrates human response time (minutes to days) into the workflow, is hard to implement with a code-based chain, but in Step Functions it is expressed as a single state. The same pattern is used to wait for asynchronous callbacks from external payment gateways or third-party APIs.
The Map state for large-scale parallel processing is also worth knowing.
If Step Functions made the flow of work declarative, AppSync makes the flow of data gathering declarative. Consider a case where a single mobile screen has to show "user info + recent orders + recommended products." With REST, the client has to call three endpoints separately (over-fetching/under-fetching, multiple round trips) and combine the results. With GraphQL, when the client declares the shape of the data it needs in a single query, the server gathers it from multiple data sources and returns exactly that shape.
type Query {
getOrder(orderId: ID!): Order # → DynamoDB resolver
}
type Mutation {
updateOrderStatus(orderId: ID!, status: String!): Order # → Lambda resolver
}
type Subscription {
onOrderStatusUpdate(orderId: ID!): Order
@aws_subscribe(mutations: ["updateOrderStatus"]) # Real-time subscription
}The core concept of AppSync is the resolver. It is a function that maps a single GraphQL field to a specific data source (DynamoDB, Lambda, RDS, HTTP, OpenSearch). getOrder reads DynamoDB directly, while updateOrderStatus routes business logic through Lambda, so within one GraphQL schema, each field can use a different backend. It is the AWS implementation of GraphQL's strength of "multiple data sources in one query."
| Item | AppSync (GraphQL) | API Gateway (REST) |
|---|---|---|
| Data fetching | Exactly as much as needed in one query | Fixed response per endpoint |
| Multiple sources | Multiple backends in one query | Usually a single backend |
| Real-time | Built-in subscriptions (WebSocket) | WebSocket API configured separately |
| Caching | Response level | Method level |
| Suited for | Mobile, complex graph queries | General REST APIs |
The motivation for GraphQL (published by Facebook in 2015) was the constraints of mobile. On mobile, network round trips are expensive and data costs money, so REST's approach of "calling a fixed, large response per endpoint, multiple times" was inefficient.
AppSync's real-time subscription (Subscription) is bound to a specific mutation with the @aws_subscribe directive. When the updateOrderStatus mutation runs, AppSync pushes the change over WebSocket to all clients subscribed to that order. This model, where the server pushes changes without polling, is the foundation of chat, real-time dashboards, and collaboration apps. Polling has the waste of "asking continuously even when nothing changes" and the "delay between a change and the next poll"; a subscription pushes exactly once at the moment of change, eliminating both. It flips the problem that SQS long polling, which we saw on Day 1, tried to solve, turning it into push entirely. Authentication supports API_KEY/IAM/Cognito User Pool/OIDC/Lambda Authorizer.
The one sentence that runs through this article is "take the flow out of the code and make it declarative." Step Functions makes the flow of work explicit as a finite state machine, taking visibility, retries, error handling, and human approval out of the code, and Standard and Express split into "long and exactly-once" versus "short, frequent, idempotent." .sync waits for job completion and .waitForTaskToken waits for an external/human response inside the workflow, while Distributed Map handles massive parallelism. AppSync applies the same spirit to the data layer, declaring the flow of gathering data from multiple backends in a single GraphQL query and pushing changes with real-time subscriptions. Both orchestration and GraphQL are two applications of the same idea: "weave scattered pieces back together, but gather the weaving logic in one place as a declaration rather than as code."
In the next article, we do a scenario-centered comprehensive review of everything covered in Week 11: SQS, SNS, Kinesis, Step Functions, and AppSync.
Click a choice to reveal the answer and explanation.
Question 1
You need to run 5 Lambda functions in order, compensate (roll back) earlier work if an intermediate step fails, and track the whole flow visually. What is the most appropriate approach?
Question 2
You need a workflow that transforms about 50,000 short IoT events per second. Each execution finishes within a few seconds, and duplicate executions are acceptable through idempotent handling. What is the appropriate Step Functions type?
Question 3
In a refund workflow, manager approval is required above a certain amount, and approval can take days. How do you resume the workflow after approval?
Question 4
You need massively parallel processing that transforms each of hundreds of thousands of objects in an S3 bucket. What is the appropriate Step Functions state?
Question 5
A mobile app needs to efficiently fetch user info (DynamoDB), order history (via Lambda), and recommendations (OpenSearch) for one screen in a single request, and must receive order status changes in real time. What is the appropriate service?
Question 6
Which statement about the difference between Step Functions Standard and Express is correct?
Question 7
What is the core reason GraphQL is more efficient than REST in a mobile environment?