Event-Driven Architecture
Broker vs Log, Choreography vs Orchestration.
Event-Driven Architecture (EDA)
In standard REST, System A tells System B to do something ("Command"). In EDA, System A shouts "Something happened!" ("Event") and doesn't care who is listening. This creates loose coupling.
The Topologies: Broker vs. Log
Not all message queues are the same. Know the difference.
| Feature | Message Broker (RabbitMQ, SQS) | Event Log (Kafka, Kinesis) |
|---|---|---|
| Concept | "Smart Broker, Dumb Consumer" | "Dumb Broker, Smart Consumer" |
| State | Ephemeral. Deleted once consumed. | Persistent. Retained for days/years. |
| Scale | Good. | Massive. |
| Pattern | Task distribution, Job queues. | Data streaming, Event Sourcing. |
Choreography vs. Orchestration
Who manages the business process?
1. Choreography (The Dance)
Services react to events. No central coordinator.
- Flow: Order Service emits
OrderPlaced-> Inventory Service listens and emitsStockReserved-> Payment Service listens and emitsPaymentProcessed. - Pros: Highly decoupled.
- Cons: Hard to visualize. "Who actually owns the Order process?"
Choreography Implementation Example
Order Service - emits events:
- Creates order and saves to database
- Publishes
OrderPlacedevent with order details - Doesn't know or care who processes the event
- Simple, focused responsibility
Inventory Service - reacts to events:
- Listens for
OrderPlacedevents - Reserves stock for ordered items
- Publishes
StockReservedorStockReservationFailedevent - Operates independently, no direct coupling
Key Benefits:
- Services can be developed and deployed independently
- New services can be added without changing existing ones
- Natural resilience - if one service fails, others continue operating
2. Orchestration (The Conductor)
A central service commands others.
- Flow: Order Service tells Inventory "Reserve Stock". Waits for reply. Tells Payment "Charge Card".
- Pros: Clear flow, easy error handling.
- Cons: Tighter coupling, single point of failure logic.
Orchestration Implementation Example
Central orchestrator controls the flow:
- Coordinates sequential calls to each service
- Handles business logic and error conditions
- Manages compensation transactions when steps fail
- Provides single point of visibility for the entire process
Typical Flow:
- Create order in Order Service
- Reserve inventory in Inventory Service
- Process payment in Payment Service
- If any step fails, execute compensations in reverse
- Return final result to client
Key Benefits:
- Clear, visible business process
- Centralized error handling and logging
- Complex branching logic managed in one place
- Easier to implement transactional behavior
Event Payloads
What goes inside the JSON?
Notification (Thin)
Contains only IDs. `{'orderId': 123}`. The consumer must call the API to get details. Ensures data freshness but hammers the API.
Carried State (Fat)
Contains changed data. `{'orderId': 123, 'status': 'PAID'}`. Consumer needs no API call. Fast, but data might be stale.
Payload Examples
Thin Payload (Notification Style)
Structure: Minimal data - just identifiers and metadata
- Fields:
orderId,customerId,timestamp - Consumer must make additional API calls to get details
- Ensures data freshness but increases API load
- Better when data changes frequently
Consumer Implementation:
- Listen for
OrderPlacedevents - Extract
orderIdfrom event - Call Order Service API to get full order details
- Process order items and other data
- Handle API failures gracefully
Fat Payload (Carried State Style)
Structure: Includes all relevant data at time of event
- Fields:
orderId,customerId,timestamp, plus:- Full
itemsarray with product details totalAmountfor convenienceshippingAddressfor processing
- Full
- Consumer can process immediately without additional calls
- Faster processing but data might become stale
Consumer Implementation:
- Listen for
OrderPlacedevents - Extract all needed data from event payload
- Process items directly without API calls
- Handle potential data staleness issues
- Validate data integrity before processing
Real-World Message Patterns
1. Request-Reply with Correlation ID
Pattern Overview:
- Request message includes unique
requestId - Response message includes same
requestIdfor matching - Requester maintains map of pending requests
- Timeout handling prevents hanging requests
Request Message Structure:
- Unique correlation identifier
- Request payload (items to check, query parameters)
- Timestamp for request tracking
- Optional priority level
Response Message Structure:
- Matching correlation identifier
- Response data (availability status, results)
- Processing time and status codes
- Error details if applicable
Requester Implementation Logic:
- Generate unique request ID
- Store request promise with resolver in pending map
- Publish request message to event bus
- Set timeout for request expiration
- Wait for response or timeout
Response Handler Logic:
- Extract correlation ID from response
- Find pending request in map
- Resolve or reject promise with response data
- Clean up pending request entry
- Handle unmatched responses
Use Cases:
- Inventory availability checks
- Price calculations
- Fraud scoring
- Any synchronous-appearing operation in async system
Idempotency is Mandatory
In EDA, you usually get At-Least-Once delivery. Your consumers will see the same message twice. You must handle this (e.g., checking a processed_message_ids table) or data will be corrupted.
Idempotency Implementation Example
Key Components:
- Unique message ID for each event
- Processed message tracking table/cache
- Check-before-process logic
- Mark-as-successful only after completion
- Retry mechanism for failed messages
Implementation Strategy:
- Extract unique message ID from event
- Check if message already processed
- If processed, skip and acknowledge
- If not processed, execute business logic
- On success, mark message as processed
- On failure, don't mark (allows retry)
Storage Options:
- Database table for persistence
- Redis cache for speed
- DynamoDB for serverless applications
- Message broker's built-in deduplication features
Error Handling:
- Don't mark failed events as processed
- Implement retry logic with exponential backoff
- Move repeated failures to dead-letter queue
- Monitor and alert on high failure rates
Performance Considerations:
- Use efficient lookups (indexed queries)
- Implement TTL for old processed message records
- Batch insert processed message IDs
- Consider memory vs. persistence trade-offs