Zstem

Event-Driven Pipeline

by Zstem · published 2026-07-16 · 0 forks

APIDatabaseEventsArchitectureDocsWorkflowevent-drivenserverless

SNS + SQS + Lambda fan-out reference: one producer publishes domain events to an SNS topic that fans out to per-consumer SQS queues (each with a DLQ), processed

Open & fork this on Zstem →
Order ClientEvents APIPublisherLambdaEvent AuditTableorder-eventsTopicFulfillmentQueueFulfillmentDLQFulfillmentWorkerOrderProjectionsNotificationsQueueNotificationsDLQNotificationWorkerSESAnalyticsQueueAnalyticsDLQAnalyticsWorkerEvents DataLakeDLQ AlarmsEvent-Driven Pipelinezstem.design

Architecture

Workflow

Event flow

API contract

Database

event_audit
event_idULID; also the idempotency key carried in every message
event_typeorder.created | order.updated | order.cancelled
order_idGSI: by_order (order_id, published_at)
customer_id
payloadFull event envelope exactly as published to SNS
published_at
sourcePublishing service, e.g. checkout-service
sns_message_id
order_projection
order_id
statuspending | paid | packed | shipped | delivered | cancelled
total_cents
currency
item_count
last_event_idConditional-write guard: reject events older than this
updated_at
dlq_redrive_log
id
queue_nameWhich DLQ the message was found in
message_id
event_id
receive_count
error_summaryLast handler error before dead-lettering
resolutionredriven | discarded
resolved_by
resolved_at

About this design

About this design

What this is

A reference implementation of the SNS + SQS + Lambda fan-out pattern — the workhorse of event-driven architectures on AWS. One producer publishes order lifecycle events (order.created, order.updated, order.cancelled) to a single SNS topic; the topic fans out to one SQS queue per consumer; independent worker Lambdas drain their queues into their own sinks (DynamoDB projections, SES emails, an S3 data lake). Every queue has a dead-letter queue and an alarm, so failure handling is designed in, not bolted on.

Start with the event flow view — it is the clearest picture of what happens here. The workflow view covers the retry/DLQ lifecycle of a single message, and the architecture view maps everything onto concrete AWS services.

How it works

A client calls POST /events (see the API contract). The Publisher Lambda validates the envelope against the schema for its eventType, writes an append-only record to the event_audit table, then publishes to SNS with eventType as a message attribute.

SNS delivers to each subscribed queue. Subscription filter policies do the routing — the fulfillment queue only receives order.created and order.cancelled; the other queues take everything. Consumers never filter in code.

Each worker is idempotent on eventId: fulfillment uses a conditional write guarded by lasteventid, notifications dedupe before sending, analytics writes are naturally re-runnable. This matters because SQS is at-least-once.

On handler failure the message becomes visible again and is retried; after 5 failed receives the redrive policy moves it to the DLQ, a CloudWatch alarm pages on-call, and the workflow view describes the triage: fix-and-redrive (via StartMessageMoveTask) for transient issues, archive-and-discard for poison payloads. Every resolution is recorded in dlqredrivelog.

How to extend

Add a consumer: create a queue + DLQ pair, subscribe the queue to the topic with a filter policy, attach a worker Lambda, and alarm the DLQ. Nothing upstream changes — that is the point of the pattern.

Add an event type: register its JSON Schema (see the event flow view for the existing three), extend the eventType enum in the API contract, and update any filter policies that should receive it.

Need strict ordering per order? Swap the topic and queues for their FIFO variants and use orderId as the message group id.

Higher throughput sinks: replace the analytics worker with Kinesis Firehose if batching logic outgrows Lambda.

Replay: the event_audit table is your event store — a small script can republish any slice of history to the topic.

Open & fork this on Zstem →