Architecture
Advanced 16 min read Architecture

Event-Driven Architecture

Event-driven architecture decouples producers from consumers through an event backbone: services publish facts about what happened, and interested parties react asynchronously. It enables loose coupling, elastic scaling and replayable history at the cost of new failure modes and eventual consistency.

eventsmessage brokerevent sourcingoutboxeventual consistencykafkapub/sub
Introduction

What is this?

Event-driven architecture (EDA) coordinates services by exchanging immutable facts about what happened — order.created, payment.received — rather than by making synchronous requests. Producers publish events to a broker (Kafka, RabbitMQ, SNS, EventBridge) and consumers subscribe to the ones they care about. The producer does not know who, if anyone, will react.

Motivation

Why do you need it?

  • Loose coupling: producers and consumers evolve independently because neither knows the other's interface or availability.
  • Elastic, decoupled scaling: you can scale consumers independently to absorb traffic spikes without touching producers.
  • Replayable history: a log of events can rebuild read models, audit trails or test fixtures at any point in time.
  • Better resilience: a downstream outage never blocks the primary flow — events buffer and process later.
  • Domain clarity: the event stream becomes a shared, precise record of everything the system did.
Think about it

Mental model

Imagine

A town crier shouting news

Think of

A publish/subscribe event bus

The crier does not walk door to door; each household decides whether to listen and what to do. Producers publish facts, brokers deliver them at least once, and consumers react at their own pace — no one waits for a specific listener to answer.

Mechanics

How it works

  • A producer records that something happened by publishing an event to a topic on a broker.
  • The broker persists the event and delivers it to every subscribed consumer, usually with at-least-once semantics.
  • Each consumer processes events independently and tracks its own position (offset) in the stream.
  • Downstream state is derived from events rather than written by the producer, so consumers can rebuild or replay state.
  • Outbox and saga patterns keep the local database and the event stream consistent across the system.

A producer publishes once; the broker fans out to many independent consumers.

Order service
Transactional outbox
Relay to broker
Topic: order.created
Consumers
Derived state
Reference

Key commands

docker run -d --name kafka -p 9092:9092 apache/kafka-native

Start a Kafka broker locally for development.

kafka-topics.sh --create --topic order.created --bootstrap-server localhost:9092

Create the topic that producers publish to.

Created topic order.created.
kafka-console-producer.sh --topic order.created --bootstrap-server localhost:9092

Manually publish a test event to the topic.

kafka-console-consumer.sh --topic order.created --from-beginning --bootstrap-server localhost:9092

Consume and print every event on the topic from the start.

curl -X POST localhost:8080/orders -d '{"customer":"ada"}'

Trigger a real event by invoking the order service.

kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group payments --describe

Inspect a consumer group's lag to find slow or stuck consumers.

GROUP  TOPIC         PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
Hands-on

Examples

Publishing an event through a transactional outbox

Publishing an event through a transactional outbox
// order-service/placeOrder.js
import { pool } from "./db"
export async function placeOrder(db, publish, input) {
const client = await pool.connect()
try {
await client.query("BEGIN")
const order = await client.query(
"INSERT INTO orders (customer, total) VALUES ($1, $2) RETURNING id",
[input.customer, input.total]
)
await client.query(
"INSERT INTO outbox (aggregate_type, aggregate_id, type, payload) VALUES ('order', $1, 'order.created', $2)",
[order.rows[0].id, JSON.stringify(order.rows[0])]
)
await client.query("COMMIT")
await publish(outboxRow) // relayed async by the outbox poller
} catch (err) {
await client.query("ROLLBACK")
throw err
} finally {
client.release()
}
}

The order and the outbox row are written in one database transaction, so the event cannot be lost if the publish fails. A separate relay reads the outbox, publishes to the broker, and marks the row dispatched — that is what makes the database and the event stream agree.

Real world

In production

  • Payment providers react to order.placed events instead of holding the request open waiting for an answer.
  • Analytics teams subscribe to the same stream to build data pipelines without the producers knowing.
  • Inventory projections rebuild read models from event history after a schema migration.
  • IoT fleets push telemetry events; downstream alerting and storage scale independently of the devices.
Guidelines

Best practices

  • Name events in the past tense with a shared schema (event.aggregate.action) and version them from day one.
  • Use the transactional outbox pattern so the database and the event stream cannot diverge.
  • Design consumers to be idempotent: at-least-once delivery means the same event may arrive twice.
  • Keep events immutable and compactable, and treat the topic as the system of record, not a cache.
  • Put a timeout budget and a DLQ (dead-letter queue) around every consumer so poison messages cannot stall the group.
Watch out

Common mistakes

  • Sending events as afterthought HTTP calls from business logic, losing the transactional guarantee of the outbox.
  • Designing synchronous event flows where a consumer blocks waiting for another event that may never come.
  • Treating events as commands (order.ship it) instead of facts (order.shipped), which re-couples the services.
  • Ignoring idempotency and replaying events into duplicates in downstream databases.
Root cause analysis

Common issues

Error
Consumer group stuck: lag never decreases
Root cause

A poison message or a blocking operation in the consumer loop.

Fix

Enable dead-lettering, add a per-message timeout, and inspect the failed payload's schema.

Error
Duplicate events in the database
Root cause

At-least-once delivery combined with a non-idempotent consumer.

Fix

Make the consumer idempotent with a unique event_id constraint or an upsert keyed by the event.

Error
Event lost after a deploy
Root cause

The producer committed the DB transaction but the outbox relay never dispatched the row.

Fix

Retry the outbox relay with exponential backoff and a dispatcher that re-attempts undispatched rows.

When things break

Debugging tips

  • Check consumer lag first — a growing lag means the consumer is slower than the producer or is stuck.
  • Capture the event schema, producer and timestamp on the wire; most bugs are version mismatches.
  • Replay a topic from the beginning in a staging environment to reproduce state drift.
  • Look at the DLQ for poison events and inspect the exact payload that failed deserialization.
Interviews

Interview checkpoint

Practice

Hands-on lab

Publish, Consume and Replay Events
50 min · advanced
In progress
Objectives
  • Run a Kafka broker locally with Docker
  • Publish events from a producer script
  • Consume events with a consumer group
  • Inspect and replay history and consumer lag

Publish, Consume and Replay Events

50 min · advanced

0/5 steps
0%
sandbox · Linux sandbox
$
Try these commands
Test yourself

Quick quiz

0/3
1

Which pattern guarantees that a database transaction and its event publication cannot diverge?

2

An event-driven producer must know exactly which consumers will react to its events.

3

Your consumer processes each event by sending an email. The broker redelivers an event after a timeout, and customers receive duplicate emails. What is the correct fix?

Quick reference

Cheat sheet

Kafka operations
kafka-topics.sh --create --topic <t>Create a topic
kafka-console-producer.shPublish a test event
kafka-console-consumer.sh --from-beginningRead full history
consumer-groups --describe --group <g>Check consumer lag
kafka-configs.sh --alter --entity-type topicsChange retention
Reliability rules
Idempotent consumerDedupe by event_id
OutboxDB and stream agree
DLQPoison events never stall
Versioned schemaEvolve without breaking
Go deeper

Official documentation