Serverless event-driven architecture: Lambda / Step Functions / EventBridge / Saga

puml.online

Serverless isn’t just “no servers to manage” — it’s event-driven + async orchestration. This is the production combination of AWS Lambda + Step Functions + EventBridge + SQS/SNS, plus Saga compensation, event sourcing, cold start optimization, PlantUML sequence diagrams for async chains.

Serverless landscape (AWS)

1
2
3
4
5
6
7
8
9
Event source                   Compute                     Storage/downstream
────────── ───── ─────────
API Gateway → Lambda → DynamoDB
S3 events → Lambda → S3 (processed file)
EventBridge schedule → Step Functions → ECS / Fargate
DynamoDB Streams → Lambda → Kinesis
SNS notifications → Lambda → SES (email)
SQS queues → Lambda → Step Functions
ALB → Lambda → RDS Proxy → RDS

Lambda function architecture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@startuml
title "Lambda Function Internal Architecture"

actor "Caller" as C
participant "Lambda Runtime" as Runtime
participant "Handler Function" as Handler
participant "Init Code" as Init
database "DynamoDB" as DB
participant "External API" as API
participant "CloudWatch Logs" as Logs

C -> Runtime : ① invoke {payload}

alt Cold Start
Runtime -> Init : ② initialize (VPC ENI, SDK clients, ORM)
Init -> DB : ③ create connection pool
Init -> Logs : ④ log init duration
end

Runtime -> Handler : ⑤ handler(event, context)
Handler -> Handler : ⑥ business logic
Handler -> DB : ⑦ query / write
Handler -> API : ⑧ HTTP call
Handler -> Logs : ⑨ structured log

Handler --> Runtime : ⑩ return / throw error
Runtime --> C : ⑪ response (success / fail)

note right of Handler
Warm Start:
Init doesn't run, directly handler
Cold Start: 100ms - 5s
Warm Start: 1ms - 100ms
end note

@enduml

Cold start optimization:

  • Provisioned Concurrency — pre-warm instances, no cold start
  • Smaller deployment package — Lambda Layer separates dependencies
  • SnapStart (Java) — cold start < 200ms
  • Avoid VPC — VPC ENI is slow, don’t put Lambda in VPC unless necessary
  • Connection reuse — RDS Proxy / DynamoDB SDK both support connection pool

API Gateway + Lambda synchronous

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@startuml
title "API Gateway + Lambda Sync"

actor "Client" as C
participant "API Gateway" as GW
participant "Lambda" as Fn
database "DynamoDB" as DB

C -> GW : ① GET /api/users/123
GW -> GW : ② verify JWT / API Key
GW -> Fn : ③ invoke (sync)
Fn -> DB : ④ GetItem
DB --> Fn : ⑤ user record
Fn --> GW : ⑥ 200 {user: {...}}
GW --> C : ⑦ 200

note right of Fn
Timeout limits:
- Lambda: 15 min
- API Gateway: 29 sec
- ALB: same
end note

@enduml

API Gateway timeout is shorter than Lambda — long tasks use async.

SQS + Lambda async decoupling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@startuml
title "SQS + Lambda Async Decoupling"

actor "Client" as C
participant "API Gateway" as GW
participant "Lambda (Producer)" as Producer
queue "SQS Queue" as Q
participant "Lambda (Consumer)" as Consumer
database "DynamoDB" as DB

C -> GW : ① POST /api/orders {items: [...]}
GW -> Producer : ② invoke
Producer -> Producer : ③ validate request
Producer -> Q : ④ SendMessage {order_id, payload}
Producer --> GW : ⑤ 202 Accepted {order_id}
GW --> C : ⑥ 202 Accepted

note over Q : SQS persists, Lambda failure auto retries

Q -> Consumer : ⑦ poll (batch)
Consumer -> DB : ⑧ PutItem order
Consumer -> Q : ⑨ DeleteMessage (after success)

@enduml

Benefits:

  • Burst absorption — SQS buffers bursts
  • Retry — Lambda failure, SQS auto retry (visibility timeout)
  • DLQ — multiple failures go to Dead Letter Queue
  • Decoupling — Producer / Consumer deploy independently

EventBridge bus pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@startuml
title "EventBridge Event Bus Pattern"

participant "Order Service" as Order
participant "EventBridge Bus" as EB
participant "Payment Lambda" as Pay
participant "Inventory Lambda" as Inv
participant "Email Lambda" as Email
participant "Analytics Lambda" as Analytics

Order -> EB : ① PutEvents {OrderPlaced, order_id, items}

EB -> EB : ② route matching rule

par parallel triggers
EB -> Pay : ③ OrderPlaced → process payment
EB -> Inv : ④ OrderPlaced → reserve inventory
EB -> Email : ⑤ OrderPlaced → send confirmation
EB -> Analytics : ⑥ OrderPlaced → record analytics
end

note right of EB
EventBridge rule:
- source = "com.orders"
- detail-type = "OrderPlaced"
- filter (e.g. region)
- multiple targets parallel
end note

@enduml

EventBridge benefits:

  • Decoupled — sender doesn’t know who’s subscribed
  • Multi-target parallel — one event triggers multiple downstreams
  • Cross-account — events route across AWS accounts
  • SaaS integration — direct to Datadog / Zendesk / PagerDuty

Step Functions workflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@startuml
title "Step Functions State Machine"

state "OrderReceived" as S1
state "ValidateOrder" as S2
state "ChargePayment" as S3
state "ReserveInventory" as S4
state "ShipOrder" as S5
state "NotifyCustomer" as S6
state "OrderCompleted" as S7
state "OrderFailed" as SF
state "RefundPayment" as SR

S1 --> S2
S2 --> S3 : valid
S2 --> SF : invalid
S3 --> S4 : payment_ok
S3 --> SR : payment_failed
S4 --> S5 : inventory_ok
S4 --> SR : inventory_failed\n→ refund first
S5 --> S6 : shipping_ok
S6 --> S7

SR --> SF
SF --> [*]
S7 --> [*]

@enduml

Step Functions state machine file (ASL):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
"Comment": "Order processing workflow",
"StartAt": "ValidateOrder",
"States": {
"ValidateOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:validateOrder",
"Next": "ChargePayment",
"Catch": [{
"ErrorEquals": ["ValidationError"],
"Next": "OrderFailed"
}]
},
"ChargePayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:chargePayment",
"Next": "ReserveInventory",
"Retry": [{
"ErrorEquals": ["ThrottlingException"],
"IntervalSeconds": 2,
"MaxAttempts": 3
}],
"Catch": [{
"ErrorEquals": ["PaymentFailed"],
"Next": "RefundPayment"
}]
},
"ReserveInventory": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:reserveInventory",
"Next": "ShipOrder"
},
"ShipOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:shipOrder",
"Next": "NotifyCustomer"
},
"NotifyCustomer": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:notifyCustomer",
"End": true
},
"RefundPayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:refundPayment",
"Next": "OrderFailed"
},
"OrderFailed": {
"Type": "Fail",
"Cause": "Order processing failed"
}
}
}

Benefits:

  • Visualizable — state machine diagram directly shows flow
  • Auto retry — built-in retry policy
  • Error handling — Catch block explicit failure handling
  • Long tasks — max 1 year, supports wait state

Saga pattern (distributed transaction compensation)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@startuml
title "Saga Pattern - Distributed Transaction with Compensation"

actor "User" as U
participant "Order Service" as Order
participant "Payment Service" as Payment
participant "Inventory Service" as Inventory
participant "Shipping Service" as Shipping

U -> Order : ① place order
Order -> Order : ② create order (status=pending)
Order -> Payment : ③ charge
Payment --> Order : ④ success / fail

alt Payment success
Order -> Inventory : ⑤ reserve stock
Inventory --> Order : ⑥ success / fail

alt Inventory success
Order -> Shipping : ⑦ create shipment
Shipping --> Order : ⑧ success / fail

alt Shipping success
Order -> Order : ⑨ status=confirmed
else Shipping failure
Order -> Inventory : ⑩ compensate: release stock
Order -> Payment : ⑪ compensate: refund
Order -> Order : ⑫ status=cancelled
end

else Inventory failure
Order -> Payment : ⑬ compensate: refund
Order -> Order : ⑭ status=cancelled
end

else Payment failure
Order -> Order : ⑮ status=cancelled
end

@enduml

Two Saga implementations:

  • Orchestration — central coordinator (Step Functions) calls each service
  • Choreography — each service listens to events, decides next step

Event sourcing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@startuml
title "Event Sourcing Pattern"

actor "User" as U
participant "Command Handler" as Cmd
participant "Aggregate" as Agg
queue "Event Store\n(DynamoDB Stream / Kinesis)" as Store
participant "Projection" as Proj
database "Read Model\n(DynamoDB)" as Read
participant "Query Handler" as Q

U -> Cmd : ① PlaceOrder {items}
Cmd -> Agg : ② load aggregate
Agg -> Agg : ③ business rule validation
Agg -> Store : ④ append OrderPlaced {order_id, ...}
Store -> Proj : ⑤ stream event
Proj -> Read : ⑥ update read model

U -> Q : ⑦ GET /orders/{id}
Q -> Read : ⑧ query
Read --> Q : ⑨ order info

note right of Store
Event Store features:
- immutable (append-only)
- time-ordered
- replayable
end note

@enduml

Event sourcing vs traditional CRUD:

Traditional CRUD Event Sourcing
Storage current state state-change event sequence
History lost complete
Audit hard built-in
Debug current + log replay events

SNS Fan-Out + SQS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@startuml
title "SNS Fan-Out to Multiple SQS"

participant "Producer Service" as P
participant "SNS Topic" as SNS
queue "SQS - Email Queue" as Q1
queue "SQS - SMS Queue" as Q2
queue "SQS - Analytics Queue" as Q3
participant "Lambda Email" as Email
participant "Lambda SMS" as SMS
participant "Lambda Analytics" as Analytics

P -> SNS : ① Publish {message}
SNS -> Q1 : ② Fan-out
SNS -> Q2 : ③ Fan-out
SNS -> Q3 : ④ Fan-out

Q1 -> Email : ⑤ poll
Q2 -> SMS : ⑥ poll
Q3 -> Analytics : ⑦ poll

note right of SNS
SNS Fan-Out:
- one-to-many
- each subscription independent
- no impact between subscriptions
end note

@enduml

Fan-Out benefit: message goes to multiple downstreams, each consumes independently, no mutual impact.

DynamoDB Streams trigger Lambda

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@startuml
title "DynamoDB Streams + Lambda"

database "DynamoDB Table" as Table
participant "DynamoDB Stream" as Stream
participant "Lambda (CDC)" as Lambda
participant "OpenSearch" as ES
participant "S3 (Data Lake)" as S3

Table -> Stream : ① INSERT/UPDATE/DELETE auto-captured
Stream -> Lambda : ② trigger (new image, old image)
Lambda -> ES : ③ index update
Lambda -> S3 : ④ write data lake Parquet
Lambda -> Stream : ⑤ checkpoint (processed)

note right of Stream
DynamoDB Streams:
- 24 hour retention
- ordered by partition key
- trigger batch size configurable
end note

@enduml

Typical uses:

  • Real-time index to OpenSearch — search
  • Data lake ETL — analytics
  • Cross-region replication — DR
  • Cache invalidation — sync Redis

Cold start optimization field guide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@startuml
title "Lambda Cold Start Optimization"

start

:choose runtime;
note right: Node.js/Python faster than Java

:control deployment package size;
note right: <50MB ideal
:use Lambda Layer to separate deps

:VPC configuration;
note right: avoid VPC unless necessary

:Provisioned Concurrency;
note right: pre-warm N instances
:eliminate cold start

:SnapStart (Java);
note right: start < 200ms

:connection reuse;
note right: RDS Proxy / SDK connection pool

:code optimization;
note right: init clients at module top
:only business logic in handler

:monitor;
note right: CloudWatch Init Duration metric

stop

@enduml

Cold start benchmarks (typical):

  • Node.js: 100-300ms
  • Python: 200-500ms
  • Java: 1-3s (no SnapStart); 200ms (with SnapStart)
  • .NET: 1-2s

Lambda call chain visualization (PlantUML)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@startuml
title "Lambda Distributed Trace (X-Ray)"

actor "User" as U
participant "API Gateway" as GW
participant "Lambda: OrderAPI" as L1
queue "SQS" as Q
participant "Lambda: OrderProcessor" as L2
participant "Lambda: PaymentProcessor" as L3
database "DynamoDB" as DB

U -> GW : ① GET /api/orders
GW -> L1 : ② invoke
L1 -> DB : ③ Query
L1 -> Q : ④ SendMessage (async)
L1 --> GW : ⑤ 200

Q -> L2 : ⑥ async trigger
L2 -> L3 : ⑦ call another Lambda
L3 -> DB : ⑧ UpdateItem
L3 --> L2 : ⑨ success
L2 -> DB : ⑩ UpdateItem order.status

note over GW
X-Ray Service Map:
API Gateway → OrderAPI → SQS → OrderProcessor → PaymentProcessor → DynamoDB

Each segment shows latency proportion
Find the slowest link
end note

@enduml

X-Ray / CloudWatch ServiceLens auto-draw this trace, PlantUML for documentation.

Field foot-guns

  • Lambda concurrency limit — account default 1000, throttling under burst. Request quota increase or SQS buffering.
  • Lambda timeout — sync API Gateway 29s, async max 15min. Long tasks use Step Functions.
  • Step Functions state machine too large — over 200 states slow compile. Split into multiple state machines, nested execution.
  • EventBridge rule quota — 300 rules per bus default. Cross-bus routing or merge rules.
  • DLQ not configured — failed messages retry infinitely. Every SQS / EventBridge target has a DLQ.
  • DynamoDB Stream 24h retention — long lag data lost. Use Kinesis Data Stream instead.
  • Cold start spikes — daytime cold start latency high, Provisioned Concurrency pre-warm.
  • VPC Lambda network cost — NAT Gateway data egress charged per GB, expensive. VPC Endpoint for internal traffic, or don’t put Lambda in VPC.
  • Async invoke no response — Lambda async invoke returns no result. Use Destination for success/failure handling.

Decision tree

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
What's the task?
├─ HTTP sync API (< 30s) → API Gateway + Lambda
├─ HTTP sync long task → API Gateway → SQS → Lambda (async)
├─ Async processing (image/video) → S3 event → Lambda
├─ Scheduled task → EventBridge schedule → Lambda
├─ Multi-step workflow → Step Functions
├─ DB change capture → DynamoDB Stream → Lambda
├─ One-to-many push → SNS Fan-Out
└─ Cross-service event bus → EventBridge

Need transaction consistency?
├─ No → EventBridge + Lambda (eventual consistency)
├─ Strong local transaction → RDS + Lambda
├─ Cross-service transaction → Saga (compensation)
└─ Complex state machine → Step Functions + Saga

Minimum Serverless start: API Gateway + Lambda + DynamoDB. Add async: add SQS. Add workflow: add Step Functions. Add decoupling: add EventBridge.

Remember: Serverless isn’t silver bullet — long tasks use Fargate/EC2, local dev uses docker-compose. Lambda is glue, not application server.

  • Title: Serverless event-driven architecture: Lambda / Step Functions / EventBridge / Saga
  • Author: puml.online
  • Created at : 2026-07-30 17:50:00
  • Updated at : 2026-08-14 21:34:29
  • Link: https://puml.online/blog/plantuml-serverless-event-driven-en/
  • License: This work is licensed under CC BY-NC-SA 4.0.