A real, runnable system — five Java services trading events through Kafka to fulfil an order — built so you learn EDA by breaking things and watching them recover, not just by reading definitions.
Start here
The short version: instead of services calling each other and waiting for an answer, services publish facts about things that already happened — and whoever's interested reacts, on their own time.
In a typical request/response system, Service A calls Service B directly and blocks until it gets an answer. If B is slow or down, A's request fails right then — the caller has to decide what to do about it.
In an event-driven system, Service A publishes an event — "OrderCreated" — to a durable, ordered log (that's what Kafka is) and moves on. It doesn't know or care who's listening. Service B reacts whenever it gets to it, even if that's 20 seconds later because it was temporarily offline. Nothing is lost — the message just waits.
That one shift — from "call and wait" to "publish and move on" — is what gives event-driven systems their defining properties: services fail and recover independently, scale independently, and can be added or removed without anyone else's code changing.
What you'll be running
No central coordinator. Each service reacts only to events on the topics it subscribes to, and publishes its own — including two compensating paths when something downstream fails.
This is choreography, not orchestration — there's no coordinator telling services what to do next, just each one reacting to the events it subscribes to.
Get running in 5 steps
Everything runs in Docker. No IDE required to see it working, though you'll want one for the labs that ask you to edit code.
Java 25+, Maven, and Docker with Docker Compose. That's the whole list.
The project doesn't bundle its own broker — it joins one over an external Docker network. A minimal single-node KRaft broker (no ZooKeeper) plus a UI, in its own folder:
# kafka-local/docker-compose.yml
services:
kafka:
image: apache/kafka:latest
container_name: kafka
ports: ["29092:29092"]
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,PLAINTEXT_HOST://:29092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
CLUSTER_ID: 4L6g3nShT-eMCtK--X86sw
kafka-ui:
image: provectuslabs/kafka-ui:latest
ports: ["8080:8080"]
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
cd kafka-local && docker compose up -d
git clone https://github.com/nitinramachandran/EventDrivenArchitecture.git cd EventDrivenArchitecture
Joins the Kafka network from step 2 and brings up all four always-on services.
docker compose build
docker compose up -d
docker compose logs -f # watch it happen — a new order every ~4s
Open localhost:8080 (Kafka UI) to browse topics, partitions, and consumer groups alongside the logs — then head into the labs below.
Your learning path
Each level has a clear "you'll walk away able to…" outcome and builds on the vocabulary
from the one before it. Work through them in order the first time — after that, the repo's
HANDS_ON_LABS.md doubles as a reference you can jump into anywhere.
Explain what makes EDA different from request/response, read a raw Kafka event envelope, and prove a consumer being offline doesn't lose data.
Explain at-least-once vs. exactly-once precisely, make a handler safe against duplicates, and use Kafka transactions correctly.
Design a choreographed saga with compensating actions, and isolate a poison message without blocking the partition behind it.
Tell event sourcing apart from event notification, see how partitions bound scaling, and watch the system survive a broker outage.
What you'll master
The six ideas this lab is built to make you not just recognize, but have actually watched happen.
One style signals a change; the other is the system of record, with state derived purely by replaying the log.
At-least-once delivery is the default, not an edge case — see a duplicate arrive live, and the dedupe pattern that makes it safe.
What Kafka transactions actually guarantee (the Kafka-to-Kafka hop), and why that's not the same as "exactly once end to end."
How a distributed "transaction" works without two-phase commit — each step commits independently, failures trigger explicit undo actions.
Why a poison message needs its own topic instead of blocking everything behind it — and how to inspect and safely replay one.
Parallelism inside a consumer group is capped at partition count — watch a 4th instance sit completely idle to see why.
Free, open, and built to be broken on purpose — that's how the labs work.
git clone https://github.com/nitinramachandran/EventDrivenArchitecture.git cd EventDrivenArchitecture docker compose build && docker compose up -d docker compose logs -f
Get in touch
Found a bug, have a question, or just want to say hi? Leave a note below — 60 words max.