| name | event-driven-architect |
| description | Event-driven architecture expertise covering event sourcing, CQRS, message brokers (Kafka, RabbitMQ), saga patterns, eventual consistency, idempotency, event schema design, dead letter queues, and building resilient distributed systems.
Use when the user asks about event driven architect, event driven architect best practices, or needs guidance on event driven architect implementation.
Do NOT use when the user needs a different specialized skill or is asking about an unrelated technology domain.
|
| license | Apache-2.0 |
| metadata | {"author":"foundry-skills","version":"1.0.0","tags":"backend api-design architecture","category":"backend-systems","subcategory":"server-infrastructure","depends":"","disclaimer":"none","difficulty":"advanced"} |
Event-Driven Architect
You are an expert in designing event-driven distributed systems. Event-driven architecture decouples producers from consumers, enables asynchronous processing, and makes systems resilient to partial failures. It also introduces complexity that you must manage deliberately: eventual consistency, message ordering, idempotency, and debugging distributed workflows. The goal is not to use events everywhere, but to use them where they genuinely solve problems that synchronous communication cannot.
When to Use Event-Driven Architecture
Decision Framework
USE EVENTS WHEN:
✓ Multiple systems need to react to the same occurrence
✓ The producer should not wait for the consumer (async is acceptable)
✓ You need temporal decoupling (producer and consumer need not be online simultaneously)
✓ You need to replay historical events (event sourcing, audit trail)
✓ Load leveling: smooth out traffic spikes via queue buffering
USE SYNCHRONOUS (HTTP/gRPC) WHEN:
✗ The caller needs an immediate response (e.g., "is this username available?")
✗ The operation must be atomic (all-or-nothing in one step)
✗ Simple request-response with one consumer
✗ Latency matters more than decoupling
Event-Driven vs Request-Driven
| Aspect | Event-Driven | Request-Driven (HTTP/gRPC) |
|---|
| Coupling | Loose (producer doesn't know consumers) | Tight (caller knows the callee) |
| Latency | Higher (async, eventual) | Lower (synchronous) |
| Reliability | Higher (messages persisted in broker) | Lower (if callee is down, call fails) |
| Debugging | Harder (distributed traces needed) | Easier (single call stack) |
| Scaling | Independent (consumer scales separately) | Coupled (both must handle load) |
| Ordering | Needs explicit handling | Natural (sequential calls) |
Event Design
Event Types
| Type | Description | Example | Mutability |
|---|
| Domain event | Something that happened in the business | OrderPlaced, PaymentReceived | Immutable fact |
| Integration event | Cross-service communication | UserCreated (published for other services) | Immutable fact |
| Command | Request for action | ProcessPayment, SendEmail | Can be rejected |
| Notification | Fire-and-skip alert | LowInventoryWarning | No response expected |
Event Schema Best Practices
interface OrderPlacedEvent {
eventId: string;
eventType: 'OrderPlaced';
timestamp: string;
version: 1;
source: 'order-service';
correlationId: string;
causationId: string;
data: {
orderId: string;
customerId: string;
items: Array<{
productId: string;
quantity: number;
priceAtOrder: number;
}>;
totalAmount: number;
currency: string;
};
}
interface BadEvent {
type: ;
: {
: ;
};
}
Schema Evolution
RULES FOR BACKWARDS-COMPATIBLE EVOLUTION:
SAFE CHANGES:
✓ Add new optional fields
✓ Add new event types
✓ Add documentation
UNSAFE CHANGES (require versioning):
✗ Remove fields
✗ Rename fields
✗ Change field types
✗ Change semantics of existing fields
VERSIONING STRATEGY:
Option A: Version in event type ("OrderPlaced.v2")
Option B: Version field in metadata (version: 2)
Option C: Separate topic per version (orders.v1, orders.v2)
MIGRATION:
1. Deploy consumers that handle BOTH v1 and v2
2. Deploy producers that emit v2
3. Eventually remove v1 handling from consumers
Message Brokers
Broker Comparison
| Feature | Kafka | RabbitMQ | AWS SQS/SNS | Redis Streams |
|---|
| Model | Log-based | Queue-based | Cloud-managed queue | In-memory log |
| Ordering | Per partition | Per queue | Best-effort (FIFO available) | Per stream |
| Retention | Configurable (days/forever) | Until consumed | 14 days max | Configurable |
| Throughput | Very high (millions/sec) | High (10K-50K/sec) | High (auto-scaled) | Very high |
| Replay | Yes (seek to offset) | No (consumed = gone) | No | Yes (seek to ID) |
| Consumer groups | Built-in | Plugin | Built-in | Built-in |
| Best for | High-volume event streaming | Task queues, RPC | Serverless, AWS-native | Simple streaming, caching layer |
Kafka Architecture
TOPIC: order-events (3 partitions)
Producer → ┌─────────────────────────────────────────┐
│ Partition 0: [msg1] [msg4] [msg7] │
│ Partition 1: [msg2] [msg5] [msg8] │
│ Partition 2: [msg3] [msg6] [msg9] │
└─────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
Consumer Group "payment-service"
Consumer A Consumer B Consumer C
(reads P0) (reads P1) (reads P2)
KEY CONCEPTS:
- Partitions enable parallel consumption
- Partition key determines which partition (e.g., orderId)
- Messages with same key always go to same partition (ordering guarantee)
- Consumer group: each partition consumed by exactly one consumer
- Multiple consumer groups can independently read the same topic
Kafka Producer Configuration
import { Kafka, Partitioners } from 'kafkajs';
const kafka = new Kafka({
clientId: 'order-service',
brokers: ['kafka1:9092', 'kafka2:9092', 'kafka3:9092'],
});
const producer = kafka.producer({
createPartitioner: Partitioners.DefaultPartitioner,
idempotent: true,
maxInFlightRequests: 5,
retry: { retries: 5 },
});
await producer.connect();
await producer.send({
topic: 'order-events',
messages: [{
key: order.id,
value: JSON.stringify({
eventId: uuid(),
eventType: 'OrderPlaced',
timestamp: new Date().toISOString(),
data: { orderId: order., : order. },
}),
: {
: correlationId,
: ,
},
}],
});
RabbitMQ Patterns
DIRECT EXCHANGE (point-to-point):
Producer → Exchange → Queue → Consumer
Use for: Task distribution, work queues
FANOUT EXCHANGE (publish-subscribe):
Producer → Exchange → Queue A → Consumer A
→ Queue B → Consumer B
→ Queue C → Consumer C
Use for: Broadcasting events to multiple consumers
TOPIC EXCHANGE (pattern-based routing):
Producer → Exchange → "order.created" → Queue A (bound to "order.*")
→ "order.shipped" → Queue A (bound to "order.*")
→ "order.created" → Queue B (bound to "*.created")
Use for: Selective subscription based on routing patterns
Event Sourcing
Concept
TRADITIONAL: Store current state
users table: { id: 1, name: "Jane", email: "jane@new.com", role: "admin" }
(Previous states are lost)
EVENT SOURCING: Store the sequence of events
Event 1: UserCreated { id: 1, name: "Jane", email: "jane@old.com" }
Event 2: EmailChanged { id: 1, newEmail: "jane@new.com" }
Event 3: RoleChanged { id: 1, newRole: "admin" }
Current state = replay(Event 1, Event 2, Event 3)
State at any point in time = replay events up to that point
Event Store Implementation
interface EventStore {
append(streamId: string, events: DomainEvent[], expectedVersion: number): Promise<void>;
load(streamId: string): Promise<DomainEvent[]>;
loadFrom(streamId: string, fromVersion: number): Promise<DomainEvent[]>;
}
class Order {
private state: OrderState;
private version: number = 0;
private uncommittedEvents: DomainEvent[] = [];
static fromEvents(events: DomainEvent[]): Order {
const order = new Order();
events.forEach(() => order.(event, ));
order;
}
(: [], : ): {
(.. !== ) {
();
}
.( (., items, customerId), );
}
(: , : ): {
(event.) {
:
. = { : , : event.. };
;
:
. = { ...., : };
;
}
.++;
(isNew) ..(event);
}
}
CQRS (Command Query Responsibility Segregation)
WRITE SIDE (Commands): READ SIDE (Queries):
┌─────────────────────┐ ┌─────────────────────┐
│ POST /orders │ │ GET /orders │
│ (validate, apply) │ │ (optimized reads) │
│ │ │ │
│ ┌───────────────┐ │ Events │ ┌───────────────┐ │
│ │ Event Store │──┼──────────────>│ │ Read Database │ │
│ │ (append-only) │ │ (project) │ │ (denormalized) │ │
│ └───────────────┘ │ │ └───────────────┘ │
└─────────────────────┘ └─────────────────────┘
BENEFITS:
- Write model optimized for validation and consistency
- Read model optimized for query patterns (denormalized, materialized views)
- Independent scaling (reads scale differently from writes)
- Multiple read models for different query needs
COST:
- Eventual consistency between write and read sides
- More infrastructure (two databases, projection process)
- Only justified when read and write patterns differ significantly
Saga Patterns
Choreography vs Orchestration
CHOREOGRAPHY (event chain):
OrderService PaymentService InventoryService ShippingService
│ OrderPlaced ────> │ │ │
│ │ PaymentCharged ──> │ │
│ │ │ InventoryReserved > │
│ │ │ │ ShipmentCreated
│ <──────────────────────────────────────────────────────────┘
PROS: Simple, no central coordinator
CONS: Hard to track overall progress, hard to debug, circular events possible
ORCHESTRATION (central coordinator):
Saga Orchestrator
│
├── 1. Command: ChargePayment → PaymentService
│ Response: PaymentCharged ✓
│
├── 2. Command: ReserveInventory → InventoryService
│ Response: InventoryReserved ✓
│
├── 3. Command: CreateShipment → ShippingService
│ Response: ShipmentCreated ✓
│
└── 4. Complete saga
PROS: Clear flow, easy to debug, centralized error handling
CONS: Single point of failure (the orchestrator), more coupling
Compensating Transactions
class OrderSaga {
private steps: SagaStep[] = [
{
name: 'chargePayment',
execute: (ctx) => this.paymentService.charge(ctx.orderId, ctx.amount),
compensate: (ctx) => this.paymentService.refund(ctx.orderId, ctx.paymentId),
},
{
name: 'reserveInventory',
execute: (ctx) => this.inventoryService.reserve(ctx.orderId, ctx.items),
compensate: (ctx) => this.inventoryService.release(ctx.orderId, ctx.reservationId),
},
{
name: 'createShipment',
execute: (ctx) => this.shippingService.create(ctx.orderId, ctx.address),
: ..(ctx., ctx.),
},
];
(: ): <> {
: [] = [];
( step .) {
{
result = step.(context);
.(context, result);
completedSteps.(step);
} (error) {
( completed completedSteps.()) {
{
completed.(context);
} (compensateError) {
logger.(, { : completed., : compensateError });
}
}
(step., error);
}
}
}
}
Idempotency
Why Idempotency is Non-Negotiable
In distributed systems, messages can be delivered:
- AT MOST ONCE: May lose messages (fast, no duplicates)
- AT LEAST ONCE: May duplicate messages (reliable, needs idempotency)
- EXACTLY ONCE: Extremely expensive (requires distributed transactions)
Most systems use AT LEAST ONCE + IDEMPOTENT CONSUMERS
Idempotency Implementation
class IdempotentEventHandler {
constructor(private processedEventStore: ProcessedEventStore) {}
async handle(event: DomainEvent): Promise<void> {
const alreadyProcessed = await this.processedEventStore.exists(event.eventId);
if (alreadyProcessed) {
logger.info('Duplicate event, skipping', { eventId: event.eventId });
return;
}
await this.processEvent(event);
await this.processedEventStore.markProcessed(event.eventId);
}
}
Dead Letter Queues
FLOW:
Main Queue → Consumer attempts processing
├── Success → Acknowledge message
└── Failure → Retry (with backoff)
├── Retry 1 (1 sec delay)
├── Retry 2 (5 sec delay)
├── Retry 3 (30 sec delay)
└── Max retries exceeded → Dead Letter Queue (DLQ)
DLQ HANDLING:
1. Alert on-call engineer when DLQ depth > 0
2. Investigate: Why did processing fail?
3. Fix the bug or data issue
4. Replay messages from DLQ back to main queue
5. Monitor: ensure messages process successfully
Common Anti-Patterns
-
Event-driven everything: Not all communication needs events. Simple request-response between two services is fine. Events add complexity; use them when the decoupling is worth it.
-
Fat events: Including the entire entity in every event. Events should contain only what happened, not the complete state. Consumers that need full state should maintain their own read model.
-
Missing idempotency: Assuming messages are delivered exactly once. They are not. Every consumer must be idempotent.
-
No dead letter queue: Failed messages disappear silently. Always configure a DLQ and monitor its depth.
-
Synchronous over events: Calling HTTP endpoints in event handlers and waiting for responses. This defeats the purpose of async processing and creates coupling.
Event-Driven Architecture Checklist
Output Format
# Event Driven Architect Analysis
## Context Assessment
[Situation summary and constraints]
## Recommended Approach
[Primary recommendation with rationale]
## Implementation Steps
1. [Step with specific details]
2. [Step with specific details]
3. [Step with specific details]
## Trade-offs and Considerations
- [Key trade-off 1]
- [Key trade-off 2]
## Next Steps
- [Immediate action item]
- [Follow-up action item]
Example
Input: "Help me implement event driven architect for a medium-scale production application"
Output: A structured analysis covering current state assessment, recommended event driven architect approach with specific patterns, implementation roadmap with milestones, and risk mitigation strategies tailored to the application scale and constraints.
Edge Cases
- Legacy system integration: When event driven architect must coexist with legacy approaches, provide a gradual migration path rather than a complete rewrite
- Scale mismatch: When the solution complexity exceeds the project scale, recommend a simpler approach and note when to revisit
- Team skill gaps: When the team lacks experience with the recommended approach, include learning resources and simpler alternatives
- Conflicting requirements: When constraints conflict (e.g., performance vs. maintainability), explicitly state the trade-off and recommend based on stated priorities