00:00

Event-Driven Architecture (EDA) in System Design

Last updated:

Event-driven architecture (EDA) is a way of designing software in which parts of a system talk to each other by sending and reacting to events. An event is a short message saying that something has happened, such as "an order was placed", "a payment succeeded" or "a user signed up". Instead of one service calling another and waiting for an answer, a service announces what happened, and every interested service reacts in its own time.

This tutorial explains EDA step by step: what events are, the building blocks, how it compares with request-driven design, the main patterns, how to deal with duplicates, ordering and failures, and when EDA is the right choice. It also includes formatted Java examples, from a tiny in-memory event bus to Spring Boot with Apache Kafka.

1. What Is Event-Driven Architecture?

Event-driven architecture is a design style where the flow of a system is driven by events. A component that notices a change publishes an event, and any component that cares about that change subscribes to it and reacts.

Think of a school bell. The bell does not walk into every classroom to tell each teacher that the period has ended. It simply rings, and every class reacts on its own: one class packs up, another starts a test, and the canteen opens its counter. The bell does not know or care who is listening. Event-driven systems work the same way.

The key idea is loose coupling: the service that produces an event does not need to know which services consume it, how many there are, or whether they are running right now.

2. What Is an Event?

An event is a record of something that has already happened in the system. Because it describes the past, it is named in the past tense, such as OrderPlaced, PaymentFailed or StockReserved, and it is never changed after it is created.

A typical event contains:

  • Event ID: a unique identifier, used to detect duplicates.
  • Event type: what happened, for example OrderPlaced.
  • Timestamp: when it happened.
  • Source: which service produced it.
  • Payload: the useful data, such as the order ID and amount.

Here is what an OrderPlaced event might look like in JSON:

OrderPlaced event
{
  "eventId": "7f3c2a1e-9b4d-4c8e-a6f1-2d5e8b0c4a91",
  "eventType": "OrderPlaced",
  "version": 1,
  "occurredAt": "2026-10-19T10:15:30Z",
  "source": "order-service",
  "data": {
    "orderId": "ORD-1001",
    "customerEmail": "asha@example.com",
    "amount": 2499.00,
    "currency": "INR"
  }
}

Events can be business events (an order was placed), system events (a file was uploaded, a server became unhealthy) or user activity events (a product page was viewed). EDA works with all of them.

3. Events vs Commands vs Queries

Messages in a system usually fall into three groups. Knowing the difference helps you name and design them correctly.

  • Event: a fact about the past, such as OrderPlaced. The sender does not expect a reply and does not decide who reacts. Any number of consumers can receive it.
  • Command: a request to do something in the future, such as ChargeCard or SendInvoice. It is aimed at one specific receiver, which may accept or reject it.
  • Query: a question asking for data, such as "get order 1001". It expects an answer and changes nothing.

A simple naming test: an event reads naturally as "this has happened", while a command reads as "please do this".

4. Key Components of Event-Driven Architecture

  • Event producer (publisher): the component that detects a change and publishes an event. For example, the Order Service publishes OrderPlaced after saving an order.
  • Event broker (event bus or message broker): the middle layer that receives events, stores them safely and delivers them to subscribers. Examples include Apache Kafka, RabbitMQ and cloud services such as Amazon SNS and SQS.
  • Event channel (topic or queue): a named stream inside the broker that groups related events, for example a topic called order-events.
  • Event consumer (subscriber): a component that listens to a channel and reacts to each event, for example by charging a card or sending an email.
Core components of event-driven architecture An Order Service producer publishes events to an event broker. Inside the broker, events wait in a topic called order-events. The broker delivers each event to three consumers: Payment Service, Inventory Service and Notification Service. PRODUCER EVENT BROKER CONSUMERS Order Service publishes events Event Broker topic: order-events E1 E2 E3 stores and delivers events publish Payment Service Inventory Service Notification Service The producer and the consumers never call each other. They only share the broker and the event format.
Figure 1: The producer publishes an event once. The broker stores it in a topic and delivers a copy to every consumer that has subscribed.

5. Request-Driven vs Event-Driven Communication

In a request-driven (synchronous) design, a service calls other services directly, for example over REST, and waits for each answer. In an event-driven (asynchronous) design, the service publishes an event and carries on, and other services react later.

Request-driven versus event-driven communication On the left, the Order Service calls the Payment, Inventory and Email services directly and waits for all of them. The Email service is down, so the whole order fails. On the right, the Order Service publishes one OrderPlaced event to an event broker and responds immediately. The broker delivers the event to Payment and Inventory, and keeps it for Email, which catches up when it recovers. REQUEST-DRIVEN (SYNCHRONOUS) EVENT-DRIVEN (ASYNCHRONOUS) Order Service calls and waits Payment Inventory Email down The Order Service waits for every call. If Email is down, the whole order fails. Order Service OrderPlaced Event Broker Payment Inventory Email down The Order Service publishes once and responds. Email catches up when it recovers.
Figure 2: With direct calls, one slow or failed service breaks the whole request. With events, the broker keeps the event until each consumer is ready.
AspectRequest-drivenEvent-driven
CommunicationDirect call, caller waits for a replyPublish an event, no waiting
CouplingCaller knows every service it callsProducer does not know its consumers
Failure impactOne failed service can fail the requestFailed consumers catch up later
Adding a new featureChange the caller to make another callAdd a new consumer, producer unchanged
ConsistencyImmediate, all steps finish togetherEventual, steps finish at different times
DebuggingEasy to follow one call chainNeeds tracing across services
Best forReads and actions needing an instant answerBackground work, fan-out and long workflows

Most real systems use both: synchronous calls when the user needs an immediate answer, and events for everything that can happen in the background.

6. Simple Example: An Online Shopping Application

Let's follow a single order through an event-driven online shop.

  1. A customer clicks Buy Now. The Order Service saves the order with status PENDING and immediately shows "Order received".
  2. The Order Service publishes an OrderPlaced event to the order-events topic.
  3. Three services receive the event at the same time and work independently:
    • The Payment Service charges the customer's card.
    • The Inventory Service reserves the items in stock.
    • The Notification Service sends an order confirmation email.
  4. Each service can publish its own events, for example PaymentCompleted or StockReserved, which other services react to in turn.
Online shopping order flow with events A customer clicks Buy Now, the Order Service publishes an OrderPlaced event to the event broker's order-events topic, and the broker delivers it to the Payment Service, which charges the card, the Inventory Service, which reserves stock, and the Notification Service, which sends an email. A Loyalty Points Service, drawn with a dashed border, was added later and subscribes to the same event without any change to the Order Service. Customer Buy Now Order Service OrderPlaced Event Broker order-events Payment Servicecharges the card Inventory Servicereserves stock Notification Servicesends a confirmation email Loyalty Points Serviceadded later, no code change
Figure 3: One OrderPlaced event fans out to several services. A new Loyalty Points Service simply subscribes to the same topic, and the Order Service does not change at all.

This is where EDA shines. When the business later asks for loyalty points, the team builds a Loyalty Points Service that subscribes to OrderPlaced. Nobody touches the Order Service, so there is no risk of breaking checkout.

7. Communication Models: Queues, Pub/Sub and Event Streams

Brokers deliver events in three common ways:

  • Point-to-point queue: each message is taken by one consumer and then removed. Several workers can share a queue to split the work, like cashiers serving one line of customers. Good for background jobs such as resizing images.
  • Publish/subscribe (pub/sub): each event is copied to every subscriber. Payment, Inventory and Notification each get their own copy of OrderPlaced. Good for fan-out.
  • Event streaming: events are written to a durable, ordered log and kept for a set time, even after they are read. Each consumer tracks its own position, so a new consumer can replay old events from the beginning. Apache Kafka works this way.

Kafka combines the last two ideas: every consumer group gets every event (pub/sub), while the consumers inside one group share the work (like a queue).

8. Common Event-Driven Patterns

  • Event notification: the event carries only a small amount of data, such as orderId. A consumer that needs more details calls the producer's API. Events stay small, but consumers depend on the producer being available.
  • Event-carried state transfer: the event carries all the data consumers need, such as the customer, items and amount. Consumers can keep their own local copy and do not have to call back. Events are bigger, and the data is duplicated across services.
  • Event sourcing: instead of storing only the current state, a service stores every event that changed it, for example Deposited 500, Withdrew 200. The current balance is rebuilt by replaying the events, which gives a complete audit history.
  • CQRS (Command Query Responsibility Segregation): writes and reads use separate models. Writes produce events, and those events update one or more read models that are designed for fast queries, such as a search index or a dashboard.

You can use EDA without event sourcing or CQRS. They are optional patterns that build on top of events.

9. Choreography vs Orchestration

When a business process spans several services, such as "place order, take payment, reserve stock, ship", there are two ways to coordinate it:

  • Choreography: there is no central controller. Each service listens for events and decides what to do next, like dancers who each know their own steps. The Payment Service hears OrderPlaced and publishes PaymentCompleted; the Shipping Service hears that and starts shipping. It is simple and very loosely coupled, but the full flow is hard to see in one place.
  • Orchestration: a central orchestrator tells each service what to do, step by step, often using commands, and tracks the progress. The flow is easy to see and change, but the orchestrator becomes an important central part that must be kept reliable.

Long processes that must be undone if a later step fails are called sagas. For example, if payment fails after stock was reserved, a PaymentFailed event triggers a compensating action that releases the stock. Sagas can use either style. A common rule of thumb: choreography for short flows with few steps, orchestration for long or complex flows.

10. Java Example: A Simple In-Memory Event Bus

Before using a real broker, it helps to see the idea in plain Java. This small example has no external libraries. First, the event is an immutable Java record, named in the past tense:

OrderPlacedEvent.java
package com.javabytechie.eda;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.UUID;

public record OrderPlacedEvent(
        String eventId,
        String orderId,
        String customerEmail,
        BigDecimal amount,
        Instant occurredAt) {

    // Creates a new event with a unique ID and the current time
    public static OrderPlacedEvent of(String orderId, String customerEmail, BigDecimal amount) {
        return new OrderPlacedEvent(
                UUID.randomUUID().toString(),
                orderId,
                customerEmail,
                amount,
                Instant.now());
    }
}

Next, a tiny event bus lets handlers subscribe to an event type and lets producers publish events without knowing who is listening:

SimpleEventBus.java
package com.javabytechie.eda;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;

public class SimpleEventBus {

    // Event type -> list of handlers subscribed to that type
    private final Map<Class<?>, List<Consumer<Object>>> handlers = new ConcurrentHashMap<>();

    public <T> void subscribe(Class<T> eventType, Consumer<T> handler) {
        handlers.computeIfAbsent(eventType, type -> new CopyOnWriteArrayList<>())
                .add(event -> handler.accept(eventType.cast(event)));
    }

    public void publish(Object event) {
        List<Consumer<Object>> subscribers = handlers.getOrDefault(event.getClass(), List.of());
        for (Consumer<Object> subscriber : subscribers) {
            try {
                subscriber.accept(event);
            } catch (RuntimeException e) {
                // One failing handler must not stop the others
                System.err.println("Handler failed: " + e.getMessage());
            }
        }
    }
}

Finally, three independent "services" subscribe to OrderPlacedEvent, and the order code publishes a single event:

ShopApplication.java
package com.javabytechie.eda;

import java.math.BigDecimal;

public class ShopApplication {

    public static void main(String[] args) {
        SimpleEventBus eventBus = new SimpleEventBus();

        // Consumers: each one reacts to the same event independently
        eventBus.subscribe(OrderPlacedEvent.class, event ->
                System.out.println("[Payment]      Charging " + event.amount()
                        + " for order " + event.orderId()));

        eventBus.subscribe(OrderPlacedEvent.class, event ->
                System.out.println("[Inventory]    Reserving items for order " + event.orderId()));

        eventBus.subscribe(OrderPlacedEvent.class, event ->
                System.out.println("[Notification] Sending confirmation to " + event.customerEmail()));

        // Producer: publishes one event and does not know who is listening
        OrderPlacedEvent event = OrderPlacedEvent.of("ORD-1001", "asha@example.com",
                new BigDecimal("2499.00"));
        System.out.println("[Order]        Order " + event.orderId() + " placed");
        eventBus.publish(event);
    }
}
Output
[Order]        Order ORD-1001 placed
[Payment]      Charging 2499.00 for order ORD-1001
[Inventory]    Reserving items for order ORD-1001
[Notification] Sending confirmation to asha@example.com

Notice that the order code never calls the payment, inventory or notification logic directly. Adding a fourth handler would not change the publishing code. This bus runs in one process and delivers events immediately, so it is only for learning: if the program stops, events are lost. Real systems use a broker that stores events durably across machines.

11. Java Example: Spring Boot with Apache Kafka

In a real system, the Order Service and the Inventory Service are separate applications connected through a broker such as Apache Kafka. Add the Spring for Apache Kafka starter (Spring Boot manages the version):

pom.xml
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

Configure the broker address and tell Spring to send and receive events as JSON:

application.yml
spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
    consumer:
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "com.javabytechie.eda"

The producer saves the order and then publishes an OrderPlacedEvent. The order ID is used as the message key, which keeps all events for the same order in order (see section 13):

OrderService.java
package com.javabytechie.eda.order;

import com.javabytechie.eda.OrderPlacedEvent;
import java.math.BigDecimal;
import java.util.UUID;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private static final String TOPIC = "order-placed";

    private final KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate;

    public OrderService(KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public String placeOrder(String customerEmail, BigDecimal amount) {
        String orderId = "ORD-" + UUID.randomUUID();

        // 1. Save the order in this service's own database (omitted for brevity)

        // 2. Publish the event; the order ID is the key, so events of one order stay in order
        OrderPlacedEvent event = OrderPlacedEvent.of(orderId, customerEmail, amount);
        kafkaTemplate.send(TOPIC, orderId, event)
                .whenComplete((result, ex) -> {
                    if (ex != null) {
                        System.err.println("Could not publish event for " + orderId + ": " + ex.getMessage());
                    }
                });

        return orderId;
    }
}

The consumer lives in the Inventory Service. Spring calls the method for every event on the topic:

InventoryEventListener.java
package com.javabytechie.eda.inventory;

import com.javabytechie.eda.OrderPlacedEvent;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@Component
public class InventoryEventListener {

    @KafkaListener(topics = "order-placed", groupId = "inventory-service")
    public void onOrderPlaced(OrderPlacedEvent event) {
        System.out.println("Reserving stock for order " + event.orderId());
        // Reserve the items, then optionally publish a StockReserved event
    }
}

To add the Notification Service, you would create another listener with its own groupId, such as notification-service. Kafka gives every consumer group its own copy of each event, so both services receive every order. This example assumes both applications share the OrderPlacedEvent class; in larger systems, teams often share a schema instead (see section 16).

12. Delivery Guarantees and Idempotent Consumers

Brokers offer different promises about how many times an event is delivered:

  • At most once: an event is delivered once or not at all. Fast, but events can be lost.
  • At least once: an event is never lost, but may occasionally be delivered more than once, for example when a consumer crashes after processing but before confirming. This is the most common setting.
  • Exactly once: each event affects the result exactly once. Some platforms, such as Kafka, support this within their own system, but it is harder to achieve across databases and external services.

Because duplicates can happen, consumers should be idempotent: handling the same event twice must give the same result as handling it once. A simple way is to remember the IDs of processed events in the consumer's own database:

ProcessedEvent.java
package com.javabytechie.eda.inventory;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;

@Entity
@Table(name = "processed_events")
public class ProcessedEvent {

    @Id
    private String eventId;

    private Instant processedAt;

    protected ProcessedEvent() {
        // Required by JPA
    }

    public ProcessedEvent(String eventId) {
        this.eventId = eventId;
        this.processedAt = Instant.now();
    }
}

// ProcessedEventRepository.java
// public interface ProcessedEventRepository extends JpaRepository<ProcessedEvent, String> { }
IdempotentInventoryListener.java
package com.javabytechie.eda.inventory;

import com.javabytechie.eda.OrderPlacedEvent;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class IdempotentInventoryListener {

    private final ProcessedEventRepository processedEvents;
    private final InventoryService inventoryService;

    public IdempotentInventoryListener(ProcessedEventRepository processedEvents,
                                       InventoryService inventoryService) {
        this.processedEvents = processedEvents;
        this.inventoryService = inventoryService;
    }

    @Transactional
    @KafkaListener(topics = "order-placed", groupId = "inventory-service")
    public void onOrderPlaced(OrderPlacedEvent event) {
        if (processedEvents.existsById(event.eventId())) {
            return; // Duplicate delivery: this event was already handled
        }

        inventoryService.reserveItems(event.orderId());
        processedEvents.save(new ProcessedEvent(event.eventId()));
    }
}

Saving the event ID and reserving stock happen in the same database transaction, so either both are saved or neither is. If the same event arrives again, it is skipped. Make sure the inventory work uses the same database as the processed_events table.

13. Event Ordering and Partitioning

Order matters for some events. For example, OrderPlaced must be handled before OrderCancelled for the same order. But brokers spread events across many partitions and consumers to scale, so global ordering of all events is usually not guaranteed.

  • In Kafka, events with the same key always go to the same partition, and a partition is read in order. Using orderId as the key keeps all events of one order in sequence, while different orders are processed in parallel.
  • Choose keys carefully. A key that is too broad, such as a country code, can send most traffic to one partition and create a hot spot.
  • Consumers should still be defensive: include a version number or timestamp in events, and ignore an event that is older than the data already stored.

14. Handling Failures: Retries and Dead-Letter Topics

Sometimes a consumer cannot process an event: a database is briefly unavailable, or the event contains bad data. A good design separates these two cases:

  • Temporary errors, such as timeouts, are retried a few times with a delay between attempts.
  • Permanent errors, such as invalid data, should not block other events. After the retries are used up, the event is moved to a dead-letter topic (DLT) or dead-letter queue, where the team can inspect it, fix the problem and replay it.

In Spring Boot, declaring one error-handler bean is enough. This handler retries a failed event three times, one second apart, and then publishes it to the order-placed.DLT topic:

KafkaErrorConfig.java
package com.javabytechie.eda.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.DeadLetterPublishingRecoverer;
import org.springframework.kafka.listener.DefaultErrorHandler;
import org.springframework.util.backoff.FixedBackOff;

@Configuration
public class KafkaErrorConfig {

    @Bean
    public DefaultErrorHandler errorHandler(KafkaTemplate<?, ?> kafkaTemplate) {
        // After the retries fail, send the event to "<topic>.DLT", e.g. order-placed.DLT
        DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(kafkaTemplate);

        // Retry 3 more times, waiting 1 second between attempts
        DefaultErrorHandler handler = new DefaultErrorHandler(recoverer, new FixedBackOff(1000L, 3));

        // Bad data will never succeed, so skip the retries for it
        handler.addNotRetryableExceptions(IllegalArgumentException.class);
        return handler;
    }
}

Always monitor the dead-letter topic. Events sitting there are real business actions, such as orders, that have not been completed.

15. The Transactional Outbox Pattern

A classic problem: the Order Service must save the order in its database and publish an event. These are two separate systems. If the database save succeeds but publishing fails, the order exists but nobody hears about it. If publishing succeeds but the save fails, other services react to an order that does not exist.

The transactional outbox pattern solves this. The service writes the event into an outbox table in the same database transaction as the order. A separate relay process reads new outbox rows, publishes them to the broker and marks them as sent.

Transactional outbox pattern The Order Service writes a new order to the orders table and an event row to the outbox table inside one database transaction. An outbox relay reads unsent rows from the outbox table and publishes them to the event broker, which delivers them to consumers. Order Service placeOrder() ONE DATABASE TRANSACTION ordersINSERT the order outboxINSERT the event Outbox Relayreads new rows Event Broker publish Consumers
Figure 4: The order and its event are saved together in one transaction. The relay publishes the event afterwards, so an event is never lost and never sent for an order that was not saved.

Here is the Order Service using an outbox. Order and OutboxMessage are JPA entities with Spring Data repositories, and imports are left out to keep the example short:

OrderService.java (with outbox)
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final OutboxRepository outboxRepository;
    private final ObjectMapper objectMapper;

    public OrderService(OrderRepository orderRepository,
                        OutboxRepository outboxRepository,
                        ObjectMapper objectMapper) {
        this.orderRepository = orderRepository;
        this.outboxRepository = outboxRepository;
        this.objectMapper = objectMapper;
    }

    @Transactional
    public String placeOrder(String customerEmail, BigDecimal amount) {
        String orderId = "ORD-" + UUID.randomUUID();

        // Both rows are saved in the same transaction: all or nothing
        orderRepository.save(new Order(orderId, customerEmail, amount));

        OrderPlacedEvent event = OrderPlacedEvent.of(orderId, customerEmail, amount);
        outboxRepository.save(new OutboxMessage("order-placed", orderId, toJson(event)));

        return orderId;
    }

    private String toJson(Object event) {
        try {
            return objectMapper.writeValueAsString(event);
        } catch (JsonProcessingException e) {
            // An unchecked exception rolls back the whole transaction
            throw new IllegalStateException("Could not serialize event", e);
        }
    }
}

The relay can be a simple scheduled job that polls the outbox table, or a change data capture (CDC) tool such as Debezium that reads the database log. Because the relay may publish an event twice if it crashes at the wrong moment, consumers must still be idempotent.

16. Designing and Versioning Events

Events are a contract between teams. Once other services depend on an event, changing it carelessly can break them. Some simple rules:

  • Name events in the past tense after business facts: OrderPlaced, not CreateOrder or OrderTableUpdated.
  • Always include an event ID, type, timestamp, source and version.
  • Make changes backward compatible: adding a new optional field is safe; removing or renaming a field is not.
  • Version breaking changes, for example by publishing OrderPlaced version 2 alongside version 1 until all consumers have moved.
  • Use a schema format and registry, such as Avro, Protobuf or JSON Schema with a schema registry, to check compatibility automatically.
  • Do not leak internal details: publish what other teams need, not your database columns.

17. Benefits of Event-Driven Architecture

  • Loose coupling: producers and consumers can be developed, deployed and scaled independently.
  • Easy to extend: new features are added as new consumers without changing existing services.
  • Better resilience: if a consumer is down, events wait in the broker and are processed when it returns.
  • Independent scaling: a busy consumer can run more instances without scaling the whole system.
  • Faster responses: the user gets an answer quickly while slow work happens in the background.
  • Handles traffic spikes: the broker acts as a buffer, so consumers process events at a steady pace.
  • Real-time reactions and history: events allow live dashboards, alerts and, with streaming, a replayable record of what happened.

18. Challenges of Event-Driven Architecture

  • Harder to follow the flow: a single business action may involve many services reacting at different times.
  • Eventual consistency: data in different services is updated at slightly different times, so screens may briefly show old data.
  • Duplicates and ordering: consumers must handle repeated and out-of-order events.
  • Error handling: failures happen away from the user, so retries, dead-letter topics and compensating actions are needed.
  • Testing and debugging: end-to-end tests need a broker, and problems need distributed tracing to investigate.
  • Operational effort: the broker is critical infrastructure that must be run, monitored and secured.
  • Event contracts: changing an event's structure needs coordination between teams.

Most of these can be managed with good practices: unique event IDs, idempotent consumers, correlation IDs in every event, distributed tracing, and clear event contracts.

19. When to Use Event-Driven Architecture (and When Not To)

EDA is a good fit when:

  • One action should trigger several independent reactions, such as an order triggering payment, stock, email and analytics.
  • Work can happen in the background, and the user does not need the final result instantly.
  • Traffic comes in bursts, and you want a buffer between fast producers and slower consumers.
  • Many teams own separate services that should change independently.
  • You need real-time processing, such as fraud alerts, IoT readings or live tracking.

EDA may not be the right choice when:

  • The application is small and simple, where direct calls are easier to build and understand.
  • The user needs an immediate, consistent answer, such as checking an account balance before a transfer.
  • Strict, step-by-step transactions across components are required and eventual consistency is not acceptable.
  • The team has no experience running a broker and no time to learn the operational side.

20. Popular Event Brokers and Platforms

  • Apache Kafka: a distributed event streaming platform with durable, replayable logs and very high throughput. Popular for large-scale event pipelines.
  • RabbitMQ: a flexible message broker with queues and routing rules. Great for task queues and smaller pub/sub setups.
  • Apache Pulsar: a streaming and queuing platform with multi-tenancy and tiered storage.
  • Amazon SNS, SQS and EventBridge: managed AWS services for pub/sub, queues and event routing.
  • Azure Event Grid and Service Bus: managed Azure services for event routing and enterprise messaging.
  • Google Cloud Pub/Sub: a managed global pub/sub messaging service.

For Java developers, Spring for Apache Kafka, Spring AMQP (RabbitMQ) and Spring Cloud Stream make it easy to produce and consume events with little code. To see how EDA fits into a microservices system, read Event-Driven Architecture in Microservices.

21. Event-Driven Architecture in System Design Interviews

EDA appears in many interview problems, such as designing an e-commerce checkout, a food delivery app, a notification system or a ride-hailing platform. Interviewers usually look for:

  • Knowing when to use events: synchronous calls for what the user is waiting on, events for background work and fan-out.
  • Choosing a broker and explaining why, for example Kafka for high-volume, replayable streams.
  • Reliability: at-least-once delivery, idempotent consumers, retries and dead-letter topics.
  • Consistency: how the outbox pattern avoids lost events, and how sagas undo partial work.
  • Ordering and scaling: partition keys, consumer groups and handling hot partitions.
  • Observability: correlation IDs, tracing and monitoring consumer lag.

A good answer explains the trade-offs, especially eventual consistency, rather than claiming EDA is always better.

22. Best Practices

  • Name events after business facts in the past tense, and keep them immutable.
  • Give every event a unique ID, a timestamp, a version and a correlation ID.
  • Make every consumer idempotent, because at-least-once delivery can repeat events.
  • Choose partition keys that keep related events in order and spread load evenly.
  • Use the transactional outbox pattern when a service must save data and publish an event together.
  • Retry temporary errors with backoff, send permanent failures to a dead-letter topic, and monitor it.
  • Keep event contracts backward compatible, and manage them with a schema registry.
  • Add distributed tracing and track consumer lag to see how far behind each consumer is.
  • Do not turn every interaction into an event; use synchronous calls where an instant answer is needed.
  • Document which services produce and consume each event, so the flow stays understandable.

23. Summary

  • Event-driven architecture lets services communicate by publishing and reacting to events instead of calling each other directly.
  • An event is an immutable fact about the past, such as OrderPlaced, with an ID, type, timestamp and payload.
  • Producers publish events, brokers store and deliver them, and consumers react to them.
  • Queues share work between consumers, pub/sub copies each event to every subscriber, and event streams keep a replayable log.
  • Patterns such as event-carried state transfer, event sourcing, CQRS and sagas build on top of events.
  • Reliable EDA needs idempotent consumers, sensible partition keys, retries with dead-letter topics and the outbox pattern.
  • EDA gives loose coupling, resilience and easy extension, at the cost of eventual consistency and more complex debugging.

Used in the right places, events make systems easier to grow and more tolerant of failure. Open the FAQ tab for answers to common questions about event-driven architecture.