Design an event-driven system with a message broker
Stop making your services phone each other. Put a message broker in the middle: one service announces that something happened, and any number of others react, on their own schedule. By the end you have a producer and several consumers talking through Redis Streams in Docker, with events fanned out to independent subscribers, a group of workers sharing the load, acknowledgements, and a dead worker's events redelivered to a live one.
- A starter kit: the broker's docker-compose.yml, a producer, two kinds of consumer, a recovery script and an inspector, all runnable with one dependency
- Docker, running. If containers are new to you, do Docker without fear first: this mission uses one small Redis container and the same
docker compose up -droutine. - Python and uv from Level 0 (Install anything from GitHub, safely has the one-line uv install). Check with
uv --version. The only package you add isredis. - The terminal basics, and the willingness to keep four or five terminal windows open at once: a broker, a producer and a few consumers. New to the terminal? Do the Terminal survival kit first.
- A little Python (functions, loops, dicts). You do not need to know Redis; every command is explained where it first appears.
Why events instead of direct calls
Before any code, the shift in thinking. Direct calls couple services together and fail together. Events let one service announce a fact and walk away, while others react in their own time.
Picture an online shop. A customer pays, and now the shop must take payment, email a receipt, reserve stock, and tell the analytics team. The obvious way is for the checkout service to call each of the others in turn:
POST /payments, thenPOST /email, thenPOST /stock, thenPOST /analytics. This is request/response, the style you already know from building your own API.It has three problems that grow with the system:
- Coupling. Checkout has to know every service that cares about a sale, their addresses, and their request formats. Add a loyalty-points service next month and you edit, test and redeploy checkout.
- Shared fate. If the email service is down or slow, the customer's payment hangs or fails, over something that should not block the sale. One sick service infects the caller.
- No replay. The call happens once. If analytics was restarting at that instant, the fact of the sale is gone. Nobody kept a record that anyone can read later.
Event-driven architecture turns the arrows around. Checkout does one thing: it records that an order was placed and publishes that fact to a broker, a piece of middleware whose whole job is to carry messages between programs. Checkout does not know who listens. Payment, email, stock and analytics each subscribe to the fact and react when they are ready. The vocabulary, once, so the rest of the lesson reads cleanly:
- Event: a record that something already happened, in the past tense, like
order.placed. It is a fact, not a command. The publisher is not telling anyone what to do. - Broker: the server in the middle that stores events and hands them to whoever asked for them. Redis, used here, is one. Kafka, RabbitMQ, NATS and cloud services like SQS/SNS are others.
- Stream (also called a topic or subject): the named channel events are published to. Here it is
events:orders. A broker can carry many streams at once. - Publisher (producer): the program that appends events to a stream. Subscriber (consumer): a program that reads them.
- Event-driven architecture: a design where services communicate by publishing and reacting to events through a broker, instead of calling each other directly.
TipThis is not the same as a background job queue. A task queue exists so one producer can hand *work* to workers: "resize this image", and exactly one worker does it. Pub/sub exists so one producer can announce a *fact* to many independent readers, each of whom does something different with it. The difference, and where they overlap, becomes concrete in steps 4 and 5.- Event: a record that something already happened, in the past tense, like
Events do not replace request/response everywhere. When the caller needs an answer right now (is this card valid? what is this user's balance?), a direct call is right. Events shine when the answer is "something happened, and several parties care, and none of them needs to block the others".
order.placed event is more robust than the checkout service calling the email service directly.