KAHIBARO
Discord Login Register

25.8 Event-Driven Architecture

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:

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:

An event usually contains:

Example JSON event:

json
{
  "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:

Pseudocode example in Python:

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 order

The producer does not know who will consume the event.

Consumers

A consumer subscribes to events and reacts.

Typical consumer examples:

Pseudocode example:

python
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:

Common broker technologies:

BrokerTypical styleNotes
RabbitMQQueues, routingPopular for task queues and workers
Apache KafkaEvent streamsGreat for high volume and replay
RedisPub/Sub, streamsSimple, often already in backend stack
AWS SNS/SQSTopics + queuesCloud 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:

  1. User places an order.
  2. System charges the credit card.
  3. If payment succeeds, emails a receipt and updates inventory.

Without events, your code might chain calls:

python
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:

python
# 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 order

Other services own the next steps:

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.

The publisher does not list recipients. The broker does the routing.

A simple analogy:

Pub/sub decouples:

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:

Examples:

Events

Use events when:

Examples:

Table comparison:

AspectRequest-response APIEvent-driven
DirectionClient calls serverProducer publishes, consumers subscribe
TimeSynchronous, blockingAsynchronous, non-blocking
DependenciesCaller knows calleeProducers and consumers do not know each other
Typical protocolHTTP, gRPCBroker protocols (Kafka, AMQP, Redis)
Use casesRead data, immediate actionsReactions, 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:

This means:

Scalability

You can scale different consumers independently:

Resilience

When you decouple with a broker, you can:

Extensibility

You can add new behavior easily:

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:

  1. User places an order and gets back an order ID.
  2. A separate inventory service will reduce stock when it receives "order.created" or "payment.succeeded".
  3. 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:

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:

Example of idempotent consumer:

python
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:

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:

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":

json
{
  "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:

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:

  1. HTTP request hits POST /users.
  2. The user service:
    • Validates and saves the user.
    • Publishes "user.registered" event.
  3. 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

  1. Frontend calls POST /orders.
  2. Order service:
    • Saves order in "pending" state.
    • Publishes "order.created".
  3. Payment service:
    • Listens to "order.created".
    • Contacts payment gateway.
    • Publishes "payment.succeeded" or "payment.failed".
  4. Order service listens to payment events and updates order status.
  5. 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:

Examples of good fits:

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:

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

Comments

Please login to add a comment.

Don't have an account? Register now!