25.8 Event-Driven Architecture
Table of Contents
Core Idea of Event-Driven Architecture
Event-driven architecture (EDA) is a way to build systems where events drive what happens, instead of direct calls between components.
In a classic request-response flow, Service A calls Service B and waits for the result. In event-driven architecture, Service A publishes an event such as "order.created", and any interested service subscribes to that event and reacts when it arrives.
You still use HTTP and REST, but many internal workflows become "when X happens, do Y" instead of "call Y from X".
At a high level, EDA is about:
- Events: Facts that happened in the system, like
"user.registered","payment.failed". - Producers: Components that emit events.
- Consumers: Components that listen and react to events.
- Event broker: A messaging system in the middle, such as Kafka, RabbitMQ, or Redis streams.
This style is very common in modern backends, especially in microservices, but it can also be used inside a single application or modular monolith.
Events: Facts, Not Commands
An event is a record of something that already happened, typically in the past tense.
Examples:
order.createdorder.paidorder.shippeduser.registeredpassword.changedproduct.out_of_stock
An event usually contains:
- Name / type:
"order.created" - ID: a unique event ID, such as a UUID
- Timestamp: when it happened
- Payload: data needed by consumers
Example JSON event:
{
"id": "b11f0f1c-2b59-4a0e-b6b5-03fd863a9e1c",
"type": "order.created",
"occurred_at": "2026-08-28T10:15:00Z",
"data": {
"order_id": "o_123",
"user_id": "u_45",
"total_amount": 99.50,
"currency": "USD",
"items": [
{"product_id": "p_10", "quantity": 1},
{"product_id": "p_11", "quantity": 2}
]
}
}Important rule: An event describes what happened, not what should happen. Use past tense, and avoid putting decisions into events.
This is different from a command, such as "send_invoice", which is a request to do something in the future. Commands are about intent, events are about facts.
Producers, Consumers, and the Message Broker
In an event-driven system, components have clear roles.
Producers
A producer is any component that emits events.
Typical producer examples:
- Order service publishes
"order.created"and"order.cancelled". - Payment service publishes
"payment.succeeded"and"payment.failed". - User service publishes
"user.registered"and"user.deleted".
Pseudocode example in Python:
def create_order(order_data, message_broker):
order = save_order_to_db(order_data)
event = {
"id": str(uuid4()),
"type": "order.created",
"occurred_at": datetime.utcnow().isoformat() + "Z",
"data": {
"order_id": order.id,
"user_id": order.user_id,
"total_amount": order.total_amount,
},
}
message_broker.publish("order.events", event)
return orderThe producer does not know who will consume the event.
Consumers
A consumer subscribes to events and reacts.
Typical consumer examples:
- Email service listens to
"user.registered"and sends welcome emails. - Inventory service listens to
"order.created"and reserves products. - Analytics service listens to many events and updates reports.
Pseudocode example:
def handle_order_created(event):
data = event["data"]
order_id = data["order_id"]
user_id = data["user_id"]
total = data["total_amount"]
send_invoice_to_user(user_id=user_id, order_id=order_id, total=total)
message_broker.subscribe("order.events", "order.created", handle_order_created)Here, the consumer also does not know who produced the event.
Message Broker
A message broker sits in the middle:
- Receives messages from producers.
- Delivers them to consumers.
- Often stores messages temporarily or longer.
- Handles queues, topics, offsets, and routing.
Common broker technologies:
| Broker | Typical style | Notes |
|---|---|---|
| RabbitMQ | Queues, routing | Popular for task queues and workers |
| Apache Kafka | Event streams | Great for high volume and replay |
| Redis | Pub/Sub, streams | Simple, often already in backend stack |
| AWS SNS/SQS | Topics + queues | Cloud managed, integrates with AWS services |
For this course you do not need broker internals yet, just the idea that your backend talks to a separate messaging system.
Event-Driven Flow vs Direct Calls
To understand EDA, compare a simple business flow written in two styles.
Direct, synchronous flow
Imagine an e-commerce store where:
- User places an order.
- System charges the credit card.
- If payment succeeds, emails a receipt and updates inventory.
Without events, your code might chain calls:
def place_order(order_data):
order = create_order_in_db(order_data)
payment_result = charge_payment(order)
if not payment_result.success:
mark_order_as_failed(order.id)
return
send_receipt_email(order)
update_inventory(order)
return order
The place_order function knows and controls every step.
Event-driven flow
With events, this may look like:
# Order service
def place_order(order_data, broker):
order = create_order_in_db(order_data)
event = {
"id": str(uuid4()),
"type": "order.created",
"occurred_at": datetime.utcnow().isoformat() + "Z",
"data": {"order_id": order.id, "user_id": order.user_id},
}
broker.publish("order.events", event)
return orderOther services own the next steps:
- Payment service consumes
"order.created", charges the card, and publishes"payment.succeeded"or"payment.failed". - Email service listens to
"payment.succeeded"and sends the receipt. - Inventory service listens to
"payment.succeeded"to reduce stock, and to"payment.failed"to release any reservation.
No single place has the whole flow hardcoded.
Publish/Subscribe Pattern
Event-driven architecture almost always uses the publish/subscribe pattern, often shortened to pub/sub.
- Publisher: sends a message to a topic such as
"order.events". - Subscriber: registers interest in one or more topics or event types.
The publisher does not list recipients. The broker does the routing.
A simple analogy:
- A news website posts an article on the site (publisher).
- Readers subscribe to categories like "Sports" or "Tech" (subscribers).
- The website does not need a separate list for each reader.
Pub/sub decouples:
- The number of consumers.
- The consumer implementation.
- The consumer deployment.
You can add a new analytics service that listens to "order.created" without touching the order service at all.
Event-Driven Architecture vs Synchronous APIs
Backend systems rarely choose only one style. You usually combine synchronous APIs and events.
Synchronous APIs
Use synchronous request-response when:
- The user is waiting for an answer.
- You need an immediate result, such as "is this password correct".
- The operation is small, fast, and local.
Examples:
POST /loginGET /productsGET /orders/{id}
Events
Use events when:
- Something happened and other parts of the system may react.
- Work can happen later or in the background.
- Different parts of the system should react independently.
Examples:
"user.registered"triggers:- Send welcome email.
- Add user to CRM.
- Start onboarding checklist.
"order.shipped"triggers:- Send tracking email.
- Update analytics.
Table comparison:
| Aspect | Request-response API | Event-driven |
|---|---|---|
| Direction | Client calls server | Producer publishes, consumers subscribe |
| Time | Synchronous, blocking | Asynchronous, non-blocking |
| Dependencies | Caller knows callee | Producers and consumers do not know each other |
| Typical protocol | HTTP, gRPC | Broker protocols (Kafka, AMQP, Redis) |
| Use cases | Read data, immediate actions | Reactions, notifications, background workflows |
In a FastAPI backend, for example, the public API is HTTP, but internally you may publish events to Redis or a queue for background workers.
Benefits of Event-Driven Architecture
EDA is popular because it solves several problems that appear as systems grow.
Loose coupling
Components do not call each other directly. They only:
- Emit events describing what they did.
- React to events they care about.
This means:
- You can change or replace a consumer without touching the producer.
- A producer can be written in a different language than the consumer.
- Services can be deployed separately.
Scalability
You can scale different consumers independently:
- If your email sending is slow, scale email consumer workers.
- If analytics is heavy, scale analytics consumers.
- The order service does not need to know or care.
Resilience
When you decouple with a broker, you can:
- Continue to accept orders even if email service is temporarily down. Events wait in a queue.
- Retry operations from events in case of transient errors.
- Sometimes replay events to rebuild a state.
Extensibility
You can add new behavior easily:
- Existing events like
"user.registered"already flow through the system. - You can build a new "referral rewards" service that listens to that event.
- No change is needed in the user registration logic.
Challenges and Trade-offs
EDA is not free. It brings complexity.
Eventual consistency
With events, not everything happens instantly in one transaction. Different parts of the system may be temporarily inconsistent.
Example:
- User places an order and gets back an order ID.
- A separate inventory service will reduce stock when it receives
"order.created"or"payment.succeeded". - For a small window of time, the product page might still show old stock numbers.
This is called eventual consistency.
Key idea: In event-driven systems, data is often eventually consistent, not instantly consistent across all services. Design your UX and workflows to accept short delays.
Debugging
When many services react to events, it can be hard to answer:
- "What exactly happened when this order was placed?"
- "Which service failed?"
You need good logging, correlation IDs, and tracing to follow events through the system.
Ordering and duplication
Many brokers cannot guarantee perfect ordering for all messages. Also, messages can sometimes be delivered more than once.
As a result, consumers should:
- Be idempotent: process the same event twice without bad side effects.
- Use event IDs or business keys to detect duplicates.
Example of idempotent consumer:
def handle_payment_succeeded(event):
event_id = event["id"]
if has_already_processed(event_id):
return # skip duplicate
data = event["data"]
order_id = data["order_id"]
if order_already_marked_as_paid(order_id):
return # another duplicate check
mark_order_as_paid(order_id)
record_processed_event(event_id)Schema evolution
Event payloads are a kind of API contract. Over time, you may need to:
- Add new fields.
- Deprecate old fields.
- Support multiple versions.
Changing event shape can break consumers, so you must be careful, similar to versioning REST APIs.
Common Event-Driven Patterns
Several patterns are frequently used in event-driven backends. You do not need to implement them now, but it is useful to recognize them.
Event notification
The simplest pattern: a service emits an event, but each consumer stores its own data and manages its own side effects.
Example:
"order.created"has basic info.- Listeners:
- Email service just sends emails.
- Analytics service updates stats.
- Logging service persists the raw event.
This is often enough for many workflows.
Event-carried state transfer
An event contains not only the ID of an entity, but also a snapshot of its important data. Consumers can update their own copies.
Example "product.price_changed":
{
"id": "e1",
"type": "product.price_changed",
"occurred_at": "2026-08-28T10:00:00Z",
"data": {
"product_id": "p_100",
"old_price": 19.99,
"new_price": 24.99,
"currency": "USD"
}
}A "search index" service can read this and update its own index without querying the product service.
Event sourcing (briefly)
In event sourcing, a service stores all events that affect an entity, and rebuilds the current state by replaying them. This is an advanced topic that overlaps with event-driven architecture, but you do not need to implement it for basic backend work.
The main idea is:
- Store events such as
"account.opened","money.deposited","money.withdrawn". - Derive current balance from the sequence of events.
For now, it is enough to know that some systems use events as the primary source of truth.
Practical Examples in a Backend
To make this concrete, imagine you are building the e-commerce backend from the later project chapters. Here is how EDA might show up.
User registration flow
When a user signs up:
- HTTP request hits
POST /users. - The user service:
- Validates and saves the user.
- Publishes
"user.registered"event. - Listeners:
- Email service sends welcome email.
- Analytics service logs a "new user" metric.
- Recommendation service prepares personalized content.
You can add or remove these listeners without changing the POST /users handler.
Order and payment flow
- Frontend calls
POST /orders. - Order service:
- Saves order in "pending" state.
- Publishes
"order.created". - Payment service:
- Listens to
"order.created". - Contacts payment gateway.
- Publishes
"payment.succeeded"or"payment.failed". - Order service listens to payment events and updates order status.
- Email and notification services listen to payment events and inform the user.
Here, the order service plays both producer and consumer roles for different events.
When to Use Event-Driven Architecture
You rarely start a very small project fully event-driven. EDA makes more sense when your system needs:
- Multiple independent reactions to one business event.
- Background processing that should not block user requests.
- Scalable and decoupled internal services.
- Extensibility without editing core workflows.
Examples of good fits:
- Sending emails, SMS, or push notifications in reaction to many actions.
- Updating analytics and reporting from live events.
- Processing long-running tasks such as:
- Video encoding.
- Document generation.
- Image processing.
- Synchronizing data to external systems or data warehouses.
Many systems start as a modular monolith with direct method calls and later introduce an event bus inside the monolith. Then, as parts are extracted into microservices, the events become network messages.
Summary
Event-driven architecture organizes your backend around events that describe what happened, not direct calls that tell others what to do.
You use:
- Producers to publish events.
- Consumers to react.
- A message broker to move events between them.
- Pub/sub to decouple who emits events from who listens.
The benefits are loose coupling, scalability, resilience, and easier extensibility, at the cost of eventual consistency and increased complexity. You still use synchronous HTTP APIs for many things, but internally, events can make your system more robust and flexible as it grows.
Views: 6
KAHIBARO