01.The Synchronous Bottleneck
When Service A calls Service B synchronously, you've created a temporal coupling. If B is slow, A is slow. If B is down, A fails. This is fine for simple CRUD — it's catastrophic for document processing, payment workflows, or anything with unpredictable latency. Event-driven architecture decouples producers from consumers entirely.
02.SQS vs EventBridge — When to Use Which
SQS is for task queues — one consumer, guaranteed processing, retry logic baked in. EventBridge is for event routing — many consumers, fan-out patterns, rule-based filtering. Use SQS when you need guaranteed delivery and exactly-once semantics. Use EventBridge when multiple services need to react to the same event.
- ▸SQS: document processing, email sending, async jobs
- ▸EventBridge: system-wide events like 'deal.created', 'user.signup'
- ▸SQS visibility timeout: set to 6x your Lambda timeout
- ▸EventBridge rules: filter by detail-type to route events cleanly
03.Dead Letter Queues Are Not Optional
Every SQS queue must have a DLQ. Set maxReceiveCount to 3 — after 3 failed processing attempts, the message moves to the DLQ. Set up a CloudWatch alarm on DLQ depth. When the alarm fires, you get a PagerDuty/SNS notification. Without this, messages silently disappear and you never know what failed.
// CDK example
const dlq = new sqs.Queue(this, 'ProcessingDLQ', {
retentionPeriod: Duration.days(14),
});
const queue = new sqs.Queue(this, 'ProcessingQueue', {
visibilityTimeout: Duration.seconds(300),
deadLetterQueue: { queue: dlq, maxReceiveCount: 3 },
});04.Idempotency — Handle Duplicate Messages
SQS guarantees at-least-once delivery, meaning duplicates are possible. Your Lambda handlers must be idempotent. Use a DynamoDB or Redis idempotency key (typically the messageId) to detect and skip already-processed messages. This prevents double-charging, double-processing, and data corruption.
05.Structured Event Schema
Every event should follow a consistent envelope schema. This makes debugging, logging, and routing predictable across all services. Never publish raw data blobs — always include metadata about the event's origin, version, and timestamp.
interface DomainEvent<T> {
eventId: string; // UUID for idempotency
eventType: string; // 'document.classified'
version: string; // '1.0'
timestamp: string; // ISO 8601
source: string; // 'extraction-service'
payload: T;
}