Skip to main content

Event-Driven Design

Level: Concurrency & Architecture | Track: Low Level Design

Why it matters in interviews

Event-driven questions appear when interviewers ask you to design multi-actor workflows — "how does placing an order trigger an email, reduce inventory, and notify fulfillment without tight coupling?" Companies like Uber, DoorDash, and Stripe ask this to stress-test your ability to decouple side effects from core business logic. A clean answer signals you understand separation of concerns, extensibility, and the tradeoffs between synchronous and asynchronous execution.

Core concepts

  • Domain events: immutable data records describing something that already happened (OrderPlaced, TripCompleted, PaymentFailed) — past tense, never a command
  • EventPublisher: raises events to the bus; has no knowledge of which handlers (if any) will react
  • EventHandler / EventListener: subscribes to a specific event type and encapsulates the reaction logic
  • Synchronous dispatch: handlers are called inline in the same thread — simple to reason about, but failures in one handler can block or corrupt others
  • Asynchronous dispatch: events are queued and handlers run on a separate thread pool — better isolation and throughput, but harder to debug and test
  • EventBus: the registry and dispatcher; maps event types to their list of registered handlers
  • Distinguish from Observer pattern: Observer couples a concrete subject to its observers in the same process; Event-Driven is an architectural style where producers and consumers are fully decoupled through a shared bus or broker
  • Multi-actor workflow: a single state transition (order placed) fans out to multiple downstream reactions — confirmation email, inventory deduction, fulfillment notification — each independently extensible

Code example

// Immutable domain event
record OrderPlacedEvent(String orderId, String customerId, double totalAmount) {}

// Generic handler contract
interface EventHandler<T> {
void handle(T event);
}

// In-process synchronous EventBus
class EventBus {
private final Map<Class<?>, List<EventHandler<?>>> handlers = new HashMap<>();

public <T> void subscribe(Class<T> eventType, EventHandler<T> handler) {
handlers.computeIfAbsent(eventType, k -> new ArrayList<>()).add(handler);
}

@SuppressWarnings("unchecked")
public <T> void publish(T event) {
List<EventHandler<?>> registered = handlers.getOrDefault(event.getClass(), List.of());
for (EventHandler<?> handler : registered) {
((EventHandler<T>) handler).handle(event);
}
}
}

// Handler 1: sends confirmation email
class EmailNotificationHandler implements EventHandler<OrderPlacedEvent> {
@Override
public void handle(OrderPlacedEvent event) {
System.out.printf("Email sent to customer %s for order %s%n",
event.customerId(), event.orderId());
}
}

// Handler 2: reduces inventory
class InventoryHandler implements EventHandler<OrderPlacedEvent> {
@Override
public void handle(OrderPlacedEvent event) {
System.out.printf("Inventory reserved for order %s (amount: %.2f)%n",
event.orderId(), event.totalAmount());
}
}

// Wiring and usage
class OrderService {
private final EventBus eventBus;

OrderService(EventBus eventBus) {
this.eventBus = eventBus;
}

void placeOrder(String orderId, String customerId, double amount) {
// core business logic here ...
eventBus.publish(new OrderPlacedEvent(orderId, customerId, amount));
}
}

// Main
EventBus bus = new EventBus();
bus.subscribe(OrderPlacedEvent.class, new EmailNotificationHandler());
bus.subscribe(OrderPlacedEvent.class, new InventoryHandler());

OrderService service = new OrderService(bus);
service.placeOrder("ORD-001", "CUST-42", 149.99);

When to use

ScenarioUse this when...
Single caller, single reactionDirect method call — no indirection needed
One subject, multiple observers, tightly scopedObserver pattern — subject explicitly manages its observer list
Multiple producers, multiple consumers, loosely coupledEvent-Driven architecture — decouple via a shared EventBus
Side effects must not block the main flowAsynchronous event dispatch with a thread pool or queue
Adding new reactions without modifying existing codeEvent-Driven — new handlers register themselves; no existing code changes
Strong ordering or exactly-once guarantees neededConsider a durable message broker (Kafka, RabbitMQ) rather than in-process bus

Common interview mistakes

  • Mutating the event object inside a handler — events must be immutable records of the past; mutating them creates hidden coupling between handlers
  • Confusing Observer and Event-Driven — Observer ties a concrete subject to its observers; Event-Driven removes that coupling entirely through a bus; interviewers notice when candidates use the terms interchangeably
  • Forgetting error isolation in synchronous dispatch — one handler throwing an unchecked exception will abort all subsequent handlers; candidates often miss wrapping each dispatch in a try-catch
  • Not discussing async tradeoffs — jumping straight to async looks sophisticated but skips the conversation about ordering guarantees, error visibility, and testability that interviewers want to hear
  • Using Object or raw types for the event — losing type safety on the handler signature is a red flag; generic EventHandler<T> with a typed subscribe(Class<T>, EventHandler<T>) demonstrates design discipline
  • No deduplication or idempotency discussion — in real systems (and senior interviews) candidates are expected to note that async handlers may receive duplicate events and should be idempotent

Covered by Problems

ProblemLink
Food Delivery System
Notification Service

Resources