Lambda + Step Functions — Serverless Workflow Orchestration
In this tutorial, you will learn about Lambda + Step Functions. We cover key concepts, practical examples, and best practices to help you master this topic.
AWS Step Functions coordinate multiple Lambda functions and AWS services into visual workflows, handling sequencing, error handling, retries, and parallel execution for complex business processes.
What You'll Learn
By the end of this lesson you will understand how to create Step Functions state machines, chain Lambda functions, handle errors with retries, execute parallel branches, and integrate with other AWS services.
Why It Matters
Chaining Lambda functions directly (Function A calls Function B) creates tight coupling and makes error handling complex. Step Functions provides a visual Orchestration layer with built-in retry, error handling, and execution history -- making complex workflows observable and reliable.
Real-World Use
DodaTech's file processing pipeline uses Step Functions to orchestrate: validate the upload, scan for malware, convert format, generate thumbnails, store metadata, and notify the user -- with compensation actions if any step fails.
flowchart TD
S[Start] --> V[Validate File]
V --> M[Malware Scan]
M --> C{Clean?}
C -->|Yes| F[Convert Format]
C -->|No| Q[Quarantine]
F --> T[Generate Thumbnails]
T --> D[Store Metadata]
D --> N[Notify User]
N --> E[End]
Q --> E
style S fill:#f90,color:#fff
Sequential Workflow
The simplest state machine executes Lambda functions in sequence, passing the output of each as input to the next.
# sequential_workflow.py
# Simulating a sequential Step Functions workflow
class StepFunctionExecution:
def __init__(self):
self.history = []
def add_step(self, name, function, input_data):
print(f"[{name}] Starting with: {input_data}")
try:
result = function(input_data)
self.history.append({"step": name, "status": "success", "result": result})
print(f"[{name}] Completed: {result}")
return result
except Exception as e:
self.history.append({"step": name, "status": "failed", "error": str(e)})
raise
def validate(order):
if order.get("amount", 0) <= 0:
raise ValueError("Invalid amount")
return {**order, "validated": True}
def charge(order):
print(f" Charging ${order['amount']} to card {order['card'][-4:]}")
return {**order, "charged": True, "charge_id": "ch_123"}
def fulfill(order):
print(f" Creating shipment for order {order['order_id']}")
return {**order, "fulfilled": True}
workflow = StepFunctionExecution()
order = {"order_id": "ORD-001", "amount": 49.99, "card": "4242424242424242"}
result = workflow.add_step("Validate", validate, order)
result = workflow.add_step("Charge", charge, result)
result = workflow.add_step("Fulfill", fulfill, result)
print(f"\nFinal result: {result}")
print(f"Execution history: {len(workflow.history)} steps")
Expected output:
[Validate] Starting with: {'order_id': 'ORD-001', 'amount': 49.99, 'card': '4242424242424242'}
[Validate] Completed: {'order_id': 'ORD-001', 'amount': 49.99, 'card': '4242424242424242', 'validated': True}
[Charge] Starting with: {'order_id': 'ORD-001', 'amount': 49.99, 'card': '4242424242424242', 'validated': True}
Charging $49.99 to card 4242
[Charge] Completed: {'order_id': 'ORD-001', 'amount': 49.99, 'card': '4242424242424242', 'validated': True, 'charged': True, 'charge_id': 'ch_123'}
[Fulfill] Starting with: {'order_id': 'ORD-001', 'amount': 49.99, 'card': '4242424242424242', 'validated': True, 'charged': True, 'charge_id': 'ch_123'}
Creating shipment for order ORD-001
[Fulfill] Completed: {'order_id': 'ORD-001', 'amount': 49.99, 'card': '4242424242424242', 'validated': True, 'charged': True, 'charge_id': 'ch_123', 'fulfilled': True}
Final result: {'order_id': 'ORD-001', 'amount': 49.99, ...}
Execution history: 3 steps
Error Handling and Retries
Step Functions supports automatic retries with exponential backoff and custom error handling.
# error_handling.py
# Step Functions error handling and retries
def simulate_step_function_task(task_name, max_attempts=3):
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
print(f"[{task_name}] Attempt {attempt}/{max_attempts}")
return func(*args, **kwargs)
except Exception as e:
print(f" -> Failed: {e}")
if attempt == max_attempts:
print(f" -> Exhausted retries, moving to catch")
raise
wait = 2 ** attempt
print(f" -> Retrying in {wait}s")
return None
return wrapper
return decorator
@simulate_step_function_task("ChargeCard", max_attempts=3)
def charge_card(order):
import random
if random.choice([True, False, False]):
raise Exception("Payment provider unavailable")
return {**order, "charged": True}
try:
result = charge_card({"order_id": "ORD-001", "amount": 49.99})
print(f"Final: {result}")
except:
print("Falling back to alternative payment method")
Parallel Execution
Step Functions can execute multiple branches in parallel and aggregate results.
# parallel_execution.py
# Parallel branch execution
def simulate_parallel(tasks):
import concurrent.futures
results = {}
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {executor.submit(func, data): name for name, func, data in tasks}
for future in concurrent.futures.as_completed(futures):
name = futures[future]
try:
results[name] = future.result()
print(f"[Parallel] {name} completed")
except Exception as e:
results[name] = f"Failed: {e}"
print(f"[Parallel] {name} failed: {e}")
return results
tasks = [
("CheckInventory", lambda d: {"available": True, "qty": 10}, {}),
("FraudCheck", lambda d: {"risk": "low", "score": 15}, {}),
("ShippingQuote", lambda d: {"carrier": "UPS", "cost": 5.99}, {}),
]
results = simulate_parallel(tasks)
print(f"\nAll parallel results: {results}")
Expected output:
[Parallel] CheckInventory completed
[Parallel] FraudCheck completed
[Parallel] ShippingQuote completed
All parallel results: {'CheckInventory': {'available': True, 'qty': 10}, 'FraudCheck': {'risk': 'low', 'score': 15}, 'ShippingQuote': {'carrier': 'UPS', 'cost': 5.99}}
Common Mistakes
Not using Task tokens for human approval: Long-running workflows with manual approval steps need Task tokens to pause execution until confirmed.
Overloading single state machines: A state machine should represent one business Process. Multiple unrelated steps belong in separate state machines.
Ignoring execution history retention: Execution histories are retained for 90 days but cost money. Set appropriate retention periods.
Not using ResultPath: By default, step output replaces the entire input. Use ResultPath to merge step results with existing state.
Missing timeout configurations: Without timeouts, a stuck Lambda holds the workflow indefinitely. Set timeout and heartbeat for each task.
Practice Questions
What is the difference between Standard and Express Step Functions? Standard is for long-running workflows (up to 1 year) with exactly-once execution. Express is for high-volume, short-duration workflows.
How does Step Functions handle Lambda failures? Configure retry policies with max attempts and backoff rate. Use catch for non-retryable errors.
What is parallel execution in Step Functions? Multiple branches execute simultaneously and their outputs are aggregated before proceeding.
How do you handle human approval in Step Functions? Use a Task token that pauses the workflow until an external process (human approval) resumes it via the SendTaskSuccess API.
Challenge: Design a Step Functions workflow for an e-commerce order that validates inventory, processes payment in parallel with fraud check, and handles fulfillment.
FAQ
Mini Project
Create a Step Functions workflow template for a document processing pipeline: upload to S3, extract text with Lambda, analyze sentiment, store results, and notify user.
def simulate_step_function_execution():
steps = ["UploadDocument", "ExtractText", "AnalyzeSentiment", "StoreResults", "NotifyUser"]
payload = {"document_id": "DOC-001", "s3_key": "uploads/report.pdf"}
for step in steps:
print(f"[{step}] Starting")
print(f" Input: {payload}")
if step == "ExtractText":
payload["text"] = "This is the extracted document content"
elif step == "AnalyzeSentiment":
payload["sentiment"] = "positive"
payload["confidence"] = 0.92
elif step == "StoreResults":
payload["stored"] = True
print(f" Output: {{key: value, ...}}")
print(f"\nWorkflow complete. Document {payload['document_id']} processed.")
simulate_step_function_execution()
What's Next
Next: Lambda Cold Start for performance optimization.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro