⚡ Event-Driven Architecture
Services communicate by producing and consuming events — enabling loose coupling, scalability, and real-time processing.
Real-World Analogy
Think of a newspaper publishing model. The newspaper (event producer) publishes stories without knowing who reads them. Subscribers (event consumers) choose which sections to read. If a new subscriber joins, the newspaper doesn't change anything. If a subscriber leaves, nothing breaks. This is event-driven — producers and consumers are completely decoupled.
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
E(("⚡ Event Occurs<br/>'Order Placed'"))
E --> EB{{"📫 Event Bus / Broker"}}
EB --> C1[["📧 Email Service<br/>Send confirmation"]]
EB --> C2[["📊 Analytics Service<br/>Track conversion"]]
EB --> C3[["📦 Inventory Service<br/>Reserve stock"]]
EB --> C4[["🔔 Push Service<br/>Notify restaurant"]]
style E fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#000
style EB fill:#EDE9FE,stroke:#7C3AED,stroke-width:2px,color:#000
style C1 fill:#E8F5E9,stroke:#2E7D32,color:#000
style C2 fill:#E3F2FD,stroke:#1565C0,color:#000
style C3 fill:#FFF3E0,stroke:#E65100,color:#000
style C4 fill:#FCE4EC,stroke:#C62828,color:#000 🧩 Core Concepts
Domain Events
A domain event represents something that happened in the business domain:
public record OrderPlacedEvent(
String eventId,
String orderId,
String userId,
BigDecimal amount,
List<OrderItem> items,
Instant occurredAt
) {}
public record PaymentCompletedEvent(
String eventId,
String orderId,
String paymentId,
BigDecimal amount,
Instant occurredAt
) {}
Event Naming
Use past tense for events — they describe something that already happened: OrderPlaced, PaymentCompleted, UserRegistered. Never CreateOrder (that's a command).
📐 Event Sourcing
Instead of storing current state, store all events that led to the current state:
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
subgraph Traditional["Traditional (State Store)"]
DB["Account Balance: $750"]
end
subgraph ES["Event Sourcing (Event Store)"]
E1["AccountOpened: $0"]
E2["Deposited: +$1000"]
E3["Withdrawn: -$200"]
E4["Deposited: +$500"]
E5["Withdrawn: -$550"]
E1 --> E2 --> E3 --> E4 --> E5
end
style DB fill:#E3F2FD,stroke:#1565C0,color:#000
style E5 fill:#E8F5E9,stroke:#2E7D32,color:#000 Benefits: Complete audit trail, time travel (replay to any point), debugging, analytics.
// Event Store
public interface EventStore {
void append(String aggregateId, DomainEvent event);
List<DomainEvent> getEvents(String aggregateId);
List<DomainEvent> getEventsSince(String aggregateId, long version);
}
// Rebuild state from events
public class BankAccount {
private BigDecimal balance = BigDecimal.ZERO;
public static BankAccount fromEvents(List<DomainEvent> events) {
BankAccount account = new BankAccount();
events.forEach(account::apply);
return account;
}
private void apply(DomainEvent event) {
switch (event) {
case MoneyDeposited e -> balance = balance.add(e.amount());
case MoneyWithdrawn e -> balance = balance.subtract(e.amount());
// Fail loudly on unknown events — silent defaults hide bugs when new
// event types are added but replay logic isn't updated.
default -> throw new IllegalStateException(
"Unknown event type: " + event.getClass().getName());
}
}
}
🔄 CQRS with Events
Command Query Responsibility Segregation — separate the write model from the read model:
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
subgraph Write["✏️ Command Side"]
CMD[/"Commands"/] --> AS{{"Aggregate"}} --> ES[("Event Store")]
end
subgraph Projection["🔄 Projection"]
ES --> EP{{"Event Processor"}}
end
subgraph Read["📖 Query Side"]
EP --> RM[("Read Model<br/>(Optimized for queries)")]
Q[/"Queries"/] --> RM
end
style CMD fill:#FFF3E0,stroke:#E65100,color:#000
style ES fill:#FEF3C7,stroke:#D97706,color:#000
style RM fill:#E8F5E9,stroke:#2E7D32,color:#000 🛡️ Handling Failures
Idempotent Consumers
Events may be delivered more than once. Consumers must be idempotent:
@Service
public class PaymentConsumer {
@Autowired private ProcessedEventRepository processedEvents;
@KafkaListener(topics = "order-events")
@Transactional
public void handle(OrderPlacedEvent event) {
// Process the event
paymentService.charge(event.orderId(), event.amount());
// Record processing — relies on UNIQUE constraint on event_id column.
// If a duplicate event arrives, the constraint violation is our guard.
try {
processedEvents.save(new ProcessedEvent(event.eventId()));
} catch (DataIntegrityViolationException e) {
// Duplicate event — already processed, safe to ignore.
log.info("Duplicate event, skipping: {}", event.eventId());
return;
}
}
}
Why SELECT-then-INSERT is not enough
A naive if (existsByEventId(...)) return; check is racy — two concurrent consumers can both see "not exists" and both proceed. The real idempotency guard is a database unique constraint on event_id. Let the DB enforce uniqueness and catch DataIntegrityViolationException (or DuplicateKeyException). This is safe under concurrency without distributed locks.
Dead Letter Queue (DLQ)
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
T["📋 Main Topic"] --> C["Consumer"]
C -->|"❌ Failed 3x"| DLQ["💀 Dead Letter Queue"]
DLQ --> Alert["🚨 Alert Ops Team"]
DLQ --> Retry["🔄 Manual Retry"]
style DLQ fill:#FFCDD2,stroke:#C62828,color:#000 🎯 Interview Questions
1. What is event-driven architecture?
A design pattern where services communicate by producing and consuming events through a message broker. Producers don't know who consumes events. This enables loose coupling, independent scaling, and real-time processing.
2. What is Event Sourcing?
Instead of storing current state, you store all events that led to the current state. The current state is rebuilt by replaying events. Benefits: complete audit trail, time travel, debugging.
3. How do you handle duplicate events?
Make consumers idempotent — store processed event IDs and check before processing. Use database unique constraints on business keys. Design operations to be naturally idempotent (e.g., SET balance=X vs ADD X to balance).
4. What is eventual consistency?
In event-driven systems, different services may have temporarily inconsistent views. Eventually (usually milliseconds to seconds), all services will be consistent. Trade-off: you gain availability and partition tolerance (CAP theorem).
5. Event Sourcing vs Traditional CRUD — when to use which?
Event Sourcing: audit requirements, temporal queries, complex business logic, debugging needs. CRUD: simple domains, strong consistency needed, team unfamiliar with ES patterns.