Event-driven architecture (EDA) has become the default recommendation for microservices systems — and is often over-applied as a result. EDA solves real problems: decoupling producers from consumers, enabling scale through async processing, and providing natural audit trails. But it also adds complexity: eventual consistency, debugging difficulty, and operational overhead that synchronous systems don't have.
This guide provides the decision framework for when to use EDA on AWS, and the implementation patterns that work in production.
When Event-Driven Architecture Is Worth the Complexity
EDA is the right choice when:
Producers don't need immediate consumer responses: Order placed → process payment → notify warehouse → send confirmation email. Each step doesn't need the previous step to complete synchronously. Async processing through an event bus is ideal.
Multiple consumers need to react to the same event: A new user registration might need to: update a CRM, send a welcome email, provision a Stripe customer, create an audit log entry, and trigger an onboarding workflow. EDA fans these out naturally without the producer knowing about each consumer.
Workloads have variable throughput: An SQS queue buffers traffic spikes — a sudden surge of events doesn't overwhelm consumers. Consumers process at their own rate.
Services should be deployable independently: EDA creates loose coupling. A consumer can be deployed, scaled, or replaced without the producer changing.
You need reliable delivery: SQS and EventBridge provide durability guarantees. Events survive consumer failures and are retried automatically.
EDA is NOT worth the complexity when:
- You need synchronous response (user is waiting for a result)
- The operation is simple with one obvious consumer
- Team is small (under 5-10 engineers) and the overhead isn't justified
- Eventual consistency is unacceptable for your business requirements
AWS Event Services: Which to Use When
SQS (Simple Queue Service)
Point-to-point messaging. One producer → one queue → one consumer (or consumer group processing the same queue).
Use SQS when: - You have one consumer per event type - You need dead-letter queue handling for failed messages - You need FIFO ordering (SQS FIFO queues) - Consumers process at their own rate (decoupled throughput)
The visibility timeout gotcha: When a Lambda function picks up an SQS message, the message is hidden (not deleted) for the duration of the visibility timeout. If the Lambda fails or times out, the message becomes visible again and is retried. Set the visibility timeout to at least 6× your Lambda function's timeout to prevent duplicate processing.
SNS (Simple Notification Service)
Pub-sub. One producer → one topic → multiple consumers (SQS queues, Lambda functions, HTTP endpoints).
Use SNS when: - Multiple consumers need the same event - You need fan-out from a single event to multiple queues
SNS + SQS pattern (standard): For reliable multi-consumer delivery: SNS topic → multiple SQS queues (one per consumer). Each consumer processes from its own queue at its own rate. SNS handles the fan-out; SQS handles the delivery guarantee and rate buffering.
EventBridge
Event bus with routing rules. Producer → event bus → rule-filtered → multiple targets.
Use EventBridge when: - You need content-based routing (route events to different targets based on event fields) - You're integrating with AWS services or third-party SaaS (EventBridge has 90+ SaaS integrations) - You want schema registry and event validation - Cross-account event routing
EventBridge event pattern matching:
{
"source": ["com.company.orders"],
"detail-type": ["OrderPlaced"],
"detail": {
"order_value": [{"numeric": [">", 1000]}]
}
}
This routes only high-value orders to a specific target. Fine-grained routing without application-level if/else logic in consumers.
Kinesis Data Streams
Ordered, partitioned event streaming. For high-throughput, ordered event processing.
Use Kinesis when: - Events must be processed in order (per partition key) - Very high throughput (millions of events/second) - You need replay capability (re-process historical events up to 7 days) - Multiple consumers each need to process all events (fan-out with independent cursors)
Kinesis vs SQS: SQS is easier to operate; Kinesis handles ordered, high-throughput streaming. For most business event processing, SQS is sufficient. Kinesis is the right choice for analytics pipelines, activity streams, and IoT data ingestion.
The Event Schema: Your API Contract for Events
Event schemas are the API contracts of EDA systems. Poorly designed event schemas cause consumer breakage when producers evolve. Define schemas explicitly from the start:
{
"schema": "https://schema.company.com/events/order-placed/v1",
"event_id": "uuid-v4",
"event_time": "ISO-8601",
"source": "order-service",
"version": "1.0",
"data": {
"order_id": "string",
"customer_id": "string",
"order_value": "number",
"currency": "string",
"items": [...]
}
}
Key schema design rules:
- Include a version field in every event
- Use an event_id for idempotency (consumers should be able to safely process the same event twice)
- Include event_time in UTC ISO-8601 format
- Never remove or rename existing fields (add new fields instead — backwards compatible)
Register schemas in EventBridge Schema Registry or a custom schema registry. Consumers can use generated code from the schema registry to deserialize events with type safety.
Idempotency: The Non-Negotiable
In EDA systems, consumers must be idempotent — processing the same event multiple times should have the same effect as processing it once. Events are delivered "at least once" by all AWS queuing services. Duplicate delivery is guaranteed to happen eventually.
Implement idempotency at the consumer level:
def process_order_placed(event):
order_id = event['data']['order_id']
# Check if we already processed this event
if already_processed(event['event_id']):
logger.info(f"Duplicate event {event['event_id']}, skipping")
return
# Process the event
create_warehouse_pick_order(order_id)
# Mark as processed
mark_processed(event['event_id'])
Use a DynamoDB table with event_id as the primary key and a TTL for the idempotency store. Events older than your deduplication window (typically 24-72 hours) are eligible for cleanup.
Dead Letter Queues and Failed Event Handling
Every SQS queue processing events should have a dead-letter queue (DLQ) configured. Messages that fail processing N times (maxReceiveCount) go to the DLQ for investigation.
The DLQ processing workflow: 1. Alert fires when DLQ message count exceeds threshold 2. Engineer reviews DLQ messages to understand failure pattern 3. Fix the bug in the consumer 4. Move messages from DLQ back to the source queue for reprocessing
The SQS Dead Letter Queue Redrive Policy (added 2022) automates moving messages from DLQ back to the source queue after fixes — configure with appropriate delay and monitoring.
Rutagon designs and implements event-driven systems for engineering teams building at scale on AWS. Contact us to discuss your event architecture.
Frequently Asked Questions
How do you debug event-driven systems in production?
Observability is the answer. Structured logging with event IDs in every consumer allows you to trace an event through all consumers. AWS X-Ray with EventBridge and Lambda provides distributed tracing across the event chain. Building a correlation ID into events and logging it throughout lets you reconstruct the full processing history of any individual event.
What's the difference between event sourcing and event-driven architecture?
Event-driven architecture uses events for asynchronous communication between services. Event sourcing uses events as the source of truth for state — instead of storing current state in a database, you store the sequence of events that led to current state and derive state by replaying events. EDA doesn't require event sourcing; event sourcing systems are typically event-driven. Event sourcing adds significant complexity and is rarely justified outside specific use cases (CQRS, audit-critical systems).
How do you handle event ordering when multiple consumers have dependencies?
Consumer dependencies indicate that your event decomposition may be wrong. If consumer B must wait for consumer A, they probably shouldn't be separate consumers — they're part of a sequential process that should be an orchestrated workflow (Step Functions) rather than parallel event consumers. Restructure to eliminate cross-consumer ordering dependencies where possible.
What's an appropriate retry strategy for failed events?
SQS visibility timeout and maxReceiveCount together form the retry policy. For transient failures (network timeouts, downstream service unavailable), 3-5 retries with exponential backoff is standard. For permanent failures (invalid event data, downstream service returning a permanent error), you want the event in the DLQ after 1-2 retries, not 5. Use different DLQ policies for different event types based on expected failure modes.
How does event-driven architecture affect transaction boundaries?
EDA introduces eventual consistency: two services processing related events may be in inconsistent states briefly. For operations that require atomic guarantees (like payment processing), synchronous operations within a bounded context are safer than distributed events. The saga pattern addresses distributed transactions in EDA systems — using compensation events to undo partial work when a step fails.