Event-Driven Architecture (EDA) in System Design
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:
{
"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
ChargeCardorSendInvoice. 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
OrderPlacedafter 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.
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.
| Aspect | Request-driven | Event-driven |
|---|---|---|
| Communication | Direct call, caller waits for a reply | Publish an event, no waiting |
| Coupling | Caller knows every service it calls | Producer does not know its consumers |
| Failure impact | One failed service can fail the request | Failed consumers catch up later |
| Adding a new feature | Change the caller to make another call | Add a new consumer, producer unchanged |
| Consistency | Immediate, all steps finish together | Eventual, steps finish at different times |
| Debugging | Easy to follow one call chain | Needs tracing across services |
| Best for | Reads and actions needing an instant answer | Background 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.
- A customer clicks Buy Now. The Order Service saves the order with status
PENDINGand immediately shows "Order received". - The Order Service publishes an
OrderPlacedevent to theorder-eventstopic. - 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.
- Each service can publish its own events, for example
PaymentCompletedorStockReserved, which other services react to in turn.
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
OrderPlacedand publishesPaymentCompleted; 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:
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:
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:
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);
}
}
[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):
<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:
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):
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:
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:
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> { }
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
orderIdas 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:
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.
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:
@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, notCreateOrderorOrderTableUpdated. - 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
OrderPlacedversion 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.
Frequently Asked Questions (FAQ)
Q1. What is event-driven architecture in simple words?
Event-driven architecture is a way of building software where services communicate by announcing what has happened, called events, instead of calling each other directly.
One service publishes an event such as OrderPlaced, and any service that cares about it reacts on its own, for example by charging a card or sending an email.
Q2. What is an event in software?
An event is a record of something that has already happened, such as a user signing up or a payment failing. It is named in the past tense and never changed after it is created.
An event usually contains:
- A unique event ID
- The event type
- A timestamp and the source service
- A payload with the important data
Q3. What is the difference between an event and a command?
- An event states a fact about the past, such as
OrderPlaced. The sender does not know or choose who reacts, and any number of consumers can receive it. - A command asks one specific receiver to do something, such as
ChargeCard. The receiver may accept or reject it.
In short: an event says "this happened", a command says "please do this".
Q4. How is event-driven architecture different from REST calls?
With REST, a service calls another directly and waits for the answer, so both must be running at the same time. With events, a service publishes a message to a broker and carries on; consumers process it whenever they are ready.
REST is better when the user needs an instant answer. Events are better for background work, fan-out to many services and absorbing traffic spikes. Most systems use both.
Q5. What is an event broker?
An event broker is the middle layer that receives events from producers, stores them safely and delivers them to consumers. It means producers and consumers never need to know about each other.
Popular brokers include Apache Kafka, RabbitMQ, Apache Pulsar, and cloud services such as Amazon SNS/SQS, Azure Service Bus and Google Cloud Pub/Sub.
Q6. What is the difference between a message queue and pub/sub?
- In a message queue, each message is processed by only one consumer. Several workers share the load.
- In publish/subscribe, each event is delivered to every subscriber, so several services can react to the same event.
Use a queue to spread work; use pub/sub to notify many services.
Q7. Is Apache Kafka required for event-driven architecture?
No. EDA is a design style, not a product. You can build it with RabbitMQ, cloud services such as Amazon SNS and SQS or Google Cloud Pub/Sub, or even an in-memory event bus inside one application.
Kafka is popular because it handles very high volumes, keeps events durably and lets consumers replay old events, but it also needs more effort to operate.
Q8. Should I choose Kafka or RabbitMQ?
- Choose Kafka for high-volume event streams, when you need to keep events and replay them, or when many consumer groups read the same data, such as analytics pipelines.
- Choose RabbitMQ for task queues, flexible routing rules, request/reply messaging and smaller systems where simple setup matters.
Both are reliable; pick based on volume, retention and routing needs.
Q9. What is eventual consistency?
Eventual consistency means that different services may hold slightly different data for a short time, but they all become consistent once every event has been processed.
For example, right after an order is placed, the order page may show "Payment pending" for a second until the Payment Service handles the event. Systems must be designed and screens worded so this short delay is acceptable.
Q10. What is event sourcing, and is it the same as EDA?
Event sourcing stores every change to an object as an event, such as Deposited 500 and Withdrew 200, instead of storing only the current state. The current state is rebuilt by replaying the events, which gives a full history.
It is not the same as EDA. EDA is about services communicating through events; event sourcing is about how one service stores its data. You can use EDA without event sourcing.
Q11. What is CQRS?
CQRS (Command Query Responsibility Segregation) separates the model used to change data (commands) from the model used to read data (queries).
In event-driven systems, the write side publishes events, and those events update one or more read models built for fast queries, such as a search index or a reporting table. It is useful when reads and writes have very different needs, but it adds complexity.
Q12. What is the difference between choreography and orchestration?
- Choreography: no central controller. Each service listens for events and decides what to do next. Very loosely coupled, but the full flow is hard to see.
- Orchestration: a central orchestrator tells each service what to do and tracks progress. The flow is easy to follow and change, but the orchestrator is a central component that must be reliable.
Q13. What is a saga in event-driven systems?
A saga is a business process that spans several services, made of a series of local steps. If a later step fails, earlier steps are undone with compensating actions instead of a single big database transaction.
For example, if payment fails after stock was reserved, a PaymentFailed event triggers the Inventory Service to release the stock and the Order Service to cancel the order.
Q14. How do I handle duplicate events?
Most brokers deliver events at least once, so duplicates can happen. Make consumers idempotent, meaning that processing the same event twice gives the same result as processing it once:
- Give every event a unique ID.
- Store the IDs of processed events, and skip any event already seen.
- Save the processed ID in the same transaction as the business change.
- Prefer operations that are naturally safe to repeat, such as "set status to PAID".
Q15. How can I keep events in the right order?
Most brokers do not guarantee a single global order, but they can keep order for related events:
- In Kafka, use a message key such as
orderId. Events with the same key go to the same partition and are read in order. - Include a version number or timestamp in each event, so consumers can ignore older updates.
- Avoid keys that send most traffic to one partition.
Q16. What is a dead-letter queue or dead-letter topic?
A dead-letter queue (or dead-letter topic) is a separate place where events go when they cannot be processed even after several retries, for example because their data is invalid.
Moving them aside keeps other events flowing. The team can then inspect the failed events, fix the cause and replay them. Always monitor it, because each event there is unfinished work.
Q17. What is the transactional outbox pattern?
The outbox pattern solves the problem of saving data and publishing an event reliably. The service writes the event into an outbox table in the same database transaction as the business data.
A separate relay, such as a scheduled job or a change data capture tool like Debezium, then reads the outbox and publishes the events to the broker. Events are never lost, and never sent for data that was not saved.
Q18. How should events be versioned?
- Prefer backward-compatible changes, such as adding new optional fields.
- Never remove or rename fields that consumers use without a migration plan.
- Put a
versionfield in every event. - For breaking changes, publish a new version alongside the old one until all consumers have moved.
- Use a schema format and a schema registry, such as Avro, Protobuf or JSON Schema, to check compatibility automatically.
Q19. How do you monitor and debug an event-driven system?
- Add a correlation ID to every event, so one business action can be followed across all services.
- Use distributed tracing, for example OpenTelemetry, to see the path of each request and event.
- Use structured logs that include the event ID and type.
- Track consumer lag, which shows how far behind each consumer is.
- Alert on errors and on events arriving in dead-letter topics.
Q20. When should I not use event-driven architecture?
- Small, simple applications where direct calls are easier to build and understand.
- Operations where the user needs an immediate, consistent answer, such as checking a balance before a transfer.
- Processes that need strict, step-by-step transactions where eventual consistency is not acceptable.
- Teams without the time or skills to run and monitor a broker.
EDA is a tool for the right problems, not a default for everything.