ReviseAlgo Logo

Architecture & Communication

Event-Driven Architecture (EDA)

Producing, detecting, and reacting to events asynchronously to build loosely coupled, scalable systems.

In short

Producing, detecting, and reacting to events asynchronously to build loosely coupled, scalable systems.

Last Updated: June 26, 2026 30 min read

In traditional web architecture, services communicate primarily through synchronous Request-Response patterns (e.g., HTTP REST). While easy to model, synchronous calls create tight temporal coupling: if Service B is offline or running slow, Service A's thread is blocked, leading to cascading failures. Event-Driven Architecture (EDA) breaks this paradigm by utilizing asynchronous events—immutable records of something that has already occurred—as the primary mechanism of communication. This approach allows producers and consumers to scale, fail, and evolve completely independently.

1. Learning Objectives

  • Compare synchronous Request-Response against asynchronous Event-Driven systems.
  • Distinguish between Event Notification, Event-Carried State Transfer, and Event Sourcing.
  • Understand the structural trade-offs between Orchestration (centralized) and Choreography (decentralized) patterns.
  • Analyze the mechanics and utility of the Transactional Outbox Pattern in guaranteeing event publishing reliability.
  • Evaluate handling policies for Backpressure, partition-key-based Event Ordering, and Idempotency.
  • Implement a thread-safe, dynamically registered Event Dispatcher in Java, Python, and C++.

2. Prerequisites

To get the most out of this lesson, you should be familiar with:

3. Why This Topic Matters

Modern high-scale applications require absolute decoupling to survive massive surges in traffic. If a checkout service has to talk synchronously to inventory, loyalty points, email notifications, shipping, and billing services during a transaction, the entire system is only as fast and reliable as its slowest dependency.

EDA resolves this dependency problem. By routing state updates as events through an asynchronous broker (like Apache Kafka or AWS EventBridge), the checkout system can complete the customer request in milliseconds. The downstream services consume and process the event at their own pace, entirely isolated from the main customer thread. This design is critical for scale, reliability, and real-time processing.

4. Real-world Analogy

Think of an Airport Control System:

Synchronous System (Direct Phone Calling): Every pilot must call every other pilot landing at the same airport to ask where they are. If Pilot A's phone line is busy, Pilot B must circle in the air waiting for a response. A single dropped call or slow pilot halts all airport operations.

Event-Driven System (Radio Broadcasting / Flight Board): The airport has a central radio channel and a flight status board. The control tower broadcasts: "Flight 456 has landed on Runway A." (Event). The cleaning crew, baggage handlers, and refueling trucks are listening to this channel. They hear the broadcast and react automatically. The pilot doesn't know who is listening or how they react. The pilot simply broadcasts the event and continues taxiing.

5. Core Concepts

  • Event: A record indicating that a state change has occurred. Events are immutable and always named in the past tense (e.g., OrderPlaced, UserUpgraded).
  • Event Producer: The service that detects a state change and publishes the corresponding event to the router or broker.
  • Event Consumer: The service that subscribes to event channels, listens for events, and executes processing logic when they arrive.
  • Event Channel / Topic: A logical path or category in an event broker where events are published and consumed.
  • Event Router / Broker: The middleware (e.g., Kafka, EventBridge, RabbitMQ) responsible for routing events from producers to the correct consumers.
  • Idempotency: The property of a consumer where processing the exact same event multiple times produces the same system state as processing it once.

6. Visualizations

Event Notification vs. Event-Carried State Transfer

Choreography vs. Orchestration Patterns

The Transactional Outbox Pattern

This pattern guarantees that data is committed to the application database and published to the event broker reliably, even in the event of crash failures:

7. How It Works Step-by-Step

  1. Action Occurs: A client performs an operation (e.g., clicks "Buy Now"). The target microservice processes the HTTP request and updates its local database.
  2. Event Generation: The microservice creates an immutable event object capturing the state transition, including a unique event ID, timestamp, entity ID, and context data.
  3. Publishing to Broker: The microservice pushes the event to an event broker. This is done either inline or asynchronously using a transactional outbox.
  4. Broker Routing: The event broker matches the event payload properties or the topic name to active subscription rules.
  5. Consumer Pull/Push: Subscribed consumers receive the event (either pushed by the broker or pulled at their own pace).
  6. Execution and Acknowledgement: The consumer processes the event, updates its local store, and sends an acknowledgement back to the broker to commit the consumer offset.

8. Internal Architecture

An Event-Driven Architecture relies on three main architectural layers:

  • Ingestion and Routing (The Broker): Ingests high-throughput streams of events, stores them durably (often in a distributed, append-only log), and manages partition offsets.
  • Schema Registry: A central repository storing the precise JSON, Avro, or Protobuf schemas for every event type. This ensures compatibility checks and prevents corrupted events from entering the stream.
  • Consumer Groups: Pools of consumers sharing the work of reading from partitioned topics. This allows horizontal scaling: if a topic has 10 partitions, up to 10 consumer instances can read in parallel.

9. Event Lifecycle

Let's trace the lifecycle of a purchase transaction:

  • t0: User submits order. OrderService inserts an order row in orders table and writes an OrderPlaced event to the outbox table in a single local database transaction.
  • t1: The local transaction commits. The user immediately receives a success screen.
  • t2: An Outbox Poller reads the outbox table and publishes the event to the Kafka topic orders-v1.
  • t3: The event broker persists the event inside partition 4 based on the partition key (customerId).
  • t4: PaymentService and InventoryService consume the event from partition 4.
  • t5: PaymentService successfully processes the credit card payment and emits a PaymentCompleted event.
  • t6: NotificationService consumes the PaymentCompleted event and triggers the receipt email.

10. Deep Dive

Event Notification vs. Event-Carried State Transfer vs. Event Sourcing

  • Event Notification: A tiny event payload (e.g., {"orderId": "123", "action": "created"}). The consumer receives it but does not have enough context to perform work. The consumer must call back to the source system via a synchronous API. This maintains loose data coupling but increases network traffic and runtime dependency.
  • Event-Carried State Transfer (ECST): The event payload contains all the data needed by downstream consumers (e.g., products, quantity, customer billing details). Consumers store this data locally. This eliminates call-backs to the producer, maximizing autonomy and performance, but results in duplicated data storage across services.
  • Event Sourcing: Instead of storing only the *current* state of an entity, the system stores the entire chronological sequence of mutation events. The current state is calculated dynamically by replaying the event log from the beginning. (Detailed in the next lesson).

Choreography vs. Orchestration

  • Orchestration: A central orchestrator control unit acts as a manager. It sends commands to services, tracks completion, and manages failure recovery. This makes the execution state clear and easy to monitor, but makes the orchestrator a central bottleneck.
  • Choreography: There is no central manager. Each service listens for events and reacts independently, publishing its own events in response. This is highly decoupled, but makes tracing and debugging complex workflows difficult.

Transactional Outbox Pattern

In a distributed system, a service cannot update its local database and publish to an external broker in a single atomic transaction. If the database update succeeds but the event publishing fails, the rest of the system remains unaware of the update.

The Transactional Outbox Pattern solves this. The service writes the event payload into a dedicated outbox table in its local database as part of the *same* database transaction as the business update. A background polling process or log tailer (like Debezium) reads new rows from the outbox table and publishes them to the message broker. This guarantees At-Least-Once delivery.

Ordering Guarantees & Backpressure

  • Ordering via Partition Keys: Standard brokers do not guarantee order across the entire cluster. To ensure related events (e.g., OrderCreated and OrderCancelled for order 123) are processed in order, they must be sent to the same broker partition using a consistent partition key (e.g., hashing orderId).
  • Backpressure Handling: If consumers cannot keep up with high event volumes, they must use a Pull-based consumption model. This allows consumers to fetch messages at a manageable rate, preventing memory overload.

11. Production Examples

  • Apache Kafka at LinkedIn/Uber: Ingests billions of events per hour, serving as the central nervous system. Partitioning allows linear scaling across compute instances, while disk persistence allows events to be replayed.
  • AWS EventBridge Serverless Flow: Applications publish events to a managed EventBridge bus. Serverless rules match event shapes to target Lambda functions or Step Functions, eliminating the need to manage server infrastructure.

12. Advantages

  • Logical Decoupling: Producers do not need to know who the consumers are, where they are located, or what technologies they use.
  • Temporal Decoupling: Consumers do not need to be online when the event is produced. They will process the event once they start up.
  • High Extensibility: New features or services can be added by subscribing to existing event channels without modifying the producer's code.
  • Resiliency: If a downstream service fails, events are queued in the broker. The producer continues working normally, and the failing service catches up when it recovers.

13. Limitations

  • Debugging Complexity: Tracing requests across asynchronous boundaries is difficult, requiring correlation IDs and distributed tracing tools (e.g., OpenTelemetry).
  • Eventual Consistency: The system cannot guarantee immediate read consistency across services, which can confuse users if not handled properly.
  • Out-of-Order Execution: Network issues can cause events to arrive out of order, requiring consumers to implement reordering buffers or logic to handle out-of-order state.
  • Broker Maintenance Overhead: Operating a distributed event platform like Kafka requires significant configuration, monitoring, and scaling overhead.

14. Trade-offs

  • Event-Carried State Transfer vs. Event Notification: Event Notification keeps payloads small and requires less data sync, but places a heavy API query load on the producer. ECST eliminates callbacks, but results in duplicated data storage and complex schema management.
  • Choreography vs. Orchestration: Choreography offers high performance and decoupling, but makes tracking complex workflow states difficult. Orchestration provides clear execution flows and transaction management, but introduces a single point of failure and bottleneck.

15. Performance Considerations

  • Serialization Efficiency: Use compact binary serialization formats (e.g., Avro, Protobuf) instead of JSON for high-throughput pipelines to reduce network bandwidth and CPU overhead.
  • Broker Batching: Configure producers to batch events before writing to the broker. This increases throughput significantly, at the expense of a slight increase in latency.
  • Consumer Partitioning: Ensure the number of partitions in a topic matches the required consumption concurrency. A single partition can only be read by one consumer in a consumer group.

16. Failure Scenarios

  • The "Poison Pill" Event: A corrupted or invalid event payload causes a consumer to throw an exception repeatedly. The consumer fails to commit its offset, blocking all subsequent events in that partition.
    Mitigation: Catch parsing errors and route the invalid events to a Dead Letter Queue (DLQ) for manual inspection, allowing normal processing to continue.
  • Consumer Lag Spike: A heavy processing load or a database slowdown causes consumers to fall behind producers, leading to stale reads and out-of-date states.
    Mitigation: Set up alerts for consumer lag and scale the consumer group up dynamically up to the number of partitions.

17. Best Practices

  • Make Consumers Idempotent: Always verify if an event ID was already processed using an idempotency table before executing business logic.
  • Use a Schema Registry: Enforce strict backward-compatible schemas (e.g., only append optional fields) to prevent breaking downstream consumers.
  • Define Clear Event Boundaries: Avoid leaking internal database schemas inside public event payloads. Model events around business domain transitions, not raw database updates.

18. Common Mistakes

  • Using Events as Commands: Naming events in the imperative mood (e.g., CancelOrderEvent instead of OrderCancellationRequested). Commands imply a specific action and coupling, whereas events simply state that something occurred.
  • Ignoring Broker Partition Key Hashing: Sending events without keys or using random keys when order of operations matters. This causes related events to land in different partitions, leading to out-of-order execution errors.

19. Implementation (Event Dispatcher / Event Bus)

Below are complete, production-grade implementations of an Event Bus in Java, Python, and C++. The design allows services to subscribe dynamically to specific event types, supports multi-handler registration, and includes robust exception handling to ensure a single failed handler does not crash the system.

20. Interview Questions

Easy

Q: What is the difference between an event and a command?

A: An event is a record of a state transition that has already occurred (e.g., OrderPlaced). It is immutable and named in the past tense. The producer has no expectation of how or if it will be processed. A command is an instruction to perform an action (e.g., PlaceOrder). It can be rejected and is sent to a specific target handler expecting a result.

Medium

Q: How do you guarantee at-least-once delivery without using distributed two-phase commit transactions?

A: By using the Transactional Outbox Pattern. In this design, the business database update and the event log generation are committed as a single transaction in the local database. A separate polling mechanism or database transaction log tailer (like CDC) reads the outbox table and publishes events to the broker. If the broker publish fails, the poller retries, ensuring the event is eventually delivered.

Hard

Q: Explain how to maintain strict ordering of events for a single customer while scaling consumption horizontally across multiple server nodes.

A: Strict ordering across an entire multi-partition queue is not possible without limiting consumption to a single thread. To scale horizontally while maintaining order *per customer*, the topic must be partitioned. All events for a specific customer must include a consistent partitioning key (like customerId). The producer hashes this key to assign the events to a specific partition. Since each partition is consumed by only one consumer thread within a consumer group, the events for that customer are guaranteed to be processed in order.

21. Practice Exercises

  • Easy: Modify the Java/Python implementation of the EventBus to measure and log the total processing execution duration of all combined handlers for a dispatched event.
  • Medium: Extend the EventBus to include a Dead Letter Queue (DLQ). If a handler fails with an exception, catch it, log it, wrap the failed event in a diagnostic envelope, and push it to a mock DLQ handler instead of halting the program.
  • Hard: Implement a simulated retry logic inside the EventBus that retries failed handler executions 3 times with exponential backoff before routing the event to the Dead Letter Queue.

22. Challenge Problem

Problem Statement: Design an architectural workflow for a ticket booking application. The system must process orders, reserve tickets in inventory, process payments via a third-party gateway, and send customer receipt emails.

Write out a detailed system diagram and sequence of event payloads using the Transactional Outbox Pattern and Event-Carried State Transfer. Explain how you handle a ticket booking cancellation if the payment fails, ensuring the inventory reservations are released automatically without using distributed transactions.

23. Summary

  • EDA decouples systems by using asynchronous events instead of synchronous request-response API calls.
  • Event Notification uses minimal payloads, whereas Event-Carried State Transfer provides full payloads to eliminate callbacks.
  • The Transactional Outbox Pattern solves the atomicity problem between database updates and event publishing.
  • Idempotency and partition-key based routing are critical to handle duplicate and out-of-order events.

24. Cheat Sheet

Dimension Request-Response Event Notification Event-Carried State Transfer
Coupling High (Temporal & Spatial) Medium (Calls back to source) Low (Completely decoupled)
Payload Size Variable Minimal (IDs only) Large (Full record updates)
Consistency Immediate (Strong) Eventual Eventual
Use Case User logins, payment gateway calls Alerts, simple cache invalidation High-throughput analytics, microservice sync

25. Quiz

1. Which of the following best defines an "Event" in EDA?

  • A command indicating what a downstream service should do immediately.
  • A synchronous request sent via HTTP REST.
  • An immutable statement indicating that a state change has occurred in the past. (Correct)
  • A temporary database lock held until consumers reply.

Explanation: Events are records of historical facts. They are immutable and represent state changes that have already occurred, which is why they are named in the past tense.

2. What is a key disadvantage of the Event Notification pattern compared to Event-Carried State Transfer?

  • Payload sizes are much larger.
  • Consumers are forced to make synchronous API callbacks to the producer to get required details. (Correct)
  • Consumers must manage large local database replicas.
  • Event ordering guarantees are impossible.

Explanation: Since Event Notification sends minimal payloads (e.g., just an ID), the consumer must query the producer's APIs to get details. This creates a temporal and network callback dependency back to the producer.

3. How does the Transactional Outbox Pattern ensure eventual consistency?

  • It implements distributed two-phase commit transactions.
  • It writes business updates and events to the same local database using a single transaction. (Correct)
  • It rejects client writes if the broker goes offline.
  • It runs a synchronous REST API call before committing to the DB.

Explanation: By writing the business update and event data within a single local transaction, the pattern guarantees that both are committed together. A separate poller ensures the event is eventually read and published to the broker.

4. What issue occurs if consumers fail to implement idempotency?

  • Producers will stop sending events.
  • Broker partitions will run out of space.
  • Duplicate events could lead to double processing (e.g., charging a customer twice). (Correct)
  • The schema registry will reject future payloads.

Explanation: Because brokers offer at-least-once delivery, network errors during acknowledgements can result in the same event being delivered multiple times. Consumers must be idempotent to avoid processing duplicates.

5. Which of the following names is most appropriate for a command instead of an event?

  • OrderShipped
  • ProcessPayment (Correct)
  • UserRegistered
  • InventoryChecked

Explanation: "ProcessPayment" is written in the imperative mood, indicating an action that must be taken (a command). Events use the past tense (e.g., OrderShipped).

6. What mechanism guarantees that related events are processed in order by consumers?

  • Global locking of the event broker.
  • Hashing a partition key (e.g., customer ID) so all related events land in the same partition. (Correct)
  • Configuring consumers to use a synchronous pull-push connection.
  • Increasing the number of brokers.

Explanation: Consistently hashing partition keys ensures all events for a specific key land in the same partition. Since each partition is consumed by a single thread, order is preserved.

7. What is a "Poison Pill" event?

  • An event designed to test the limits of the system.
  • An event that deletes all consumer schemas.
  • A corrupted or unparseable event that causes consumers to crash or fail repeatedly, blocking the partition. (Correct)
  • An event that duplicates other payloads.

Explanation: A poison pill event is an invalid message that cannot be processed successfully. It blocks the consumer from advancing its offset, halting processing on that partition until it is removed or bypassed.

8. How should a consumer group handle a poison pill event?

  • Delete the topic and recreate it.
  • Route the invalid event to a Dead Letter Queue (DLQ) and commit the offset to continue processing. (Correct)
  • Shutdown the broker nodes.
  • Increase the partition count.

Explanation: Routing the problematic event to a Dead Letter Queue (DLQ) allows engineers to inspect it later without halting the execution of other valid events in the queue.

9. In an event-driven context, what is "Choreography"?

  • A centralized manager service coordinating calls to other APIs.
  • Decentralized coordination where services listen for events and react independently. (Correct)
  • A script that checks the schema registry for errors.
  • A network protocol for routing multicast requests.

Explanation: Choreography relies on services listening to and publishing events to coordinate decentralized workflows without a central manager.

10. What is a schema registry used for in EDA?

  • To cache event payloads on the consumer's disk.
  • To define and validate the structure of event payloads to ensure compatibility. (Correct)
  • To auto-scale broker partitions.
  • To encrypt event payloads in transit.

Explanation: A schema registry acts as a central source of truth for event schemas, enforcing validation rules to prevent incompatible event structures from breaking consumers.

26. Further Reading

  • Designing Data-Intensive Applications by Martin Kleppmann (Chapters 11 and 12).
  • Enterprise Integration Patterns by Gregor Hohpe and Bobby Woolf.
  • Confluent Guide to Event-Driven Microservices.

27. Next Lesson Preview

In the next lesson, we will explore Event Sourcing, learning how to design databases that store the chronological history of mutation events rather than just the current state.

Key takeaways

  • Decouples producers and consumers temporally, logically, and spatially.
  • Relies on asynchronous event channels or message brokers for delivery.
  • Requires careful handling of out-of-order execution, idempotency, and eventual consistency.