Asynchronous messaging and events
Decouple services with events and message brokers, and handle duplicates and ordering safely.
- Tell commands from events, and queues from publish/subscribe
- Explain how brokers decouple services in time and availability
- Write idempotent consumers and keep related messages in order with partition keys
When an order is placed, the Kitchen should start cooking, Notifications should text the customer, and Analytics should count it. If Ordering called all three synchronously, a slow Notifications service would slow down every order, and adding Analytics would mean changing Ordering.
Instead, Ordering publishes an event - OrderPlaced - to a message broker (Kafka, RabbitMQ, a cloud queue). Every interested service subscribes and reacts in its own time. Ordering doesn’t know or care who’s listening.
- A command asks one specific service to do something:
CookOrder. It may fail or be rejected. - An event announces that something already happened:
OrderPlaced. Any number of services may react - or none. - A queue delivers each message to one consumer (work sharing); publish/subscribe delivers each event to every subscriber group.
Try it
Follow an OrderPlaced event
Step through what happens after a customer orders. Predict before each reveal.
Duplicates and ordering
Brokers almost always give at-least-once delivery: a message may arrive more than once (and “exactly-once” usually means “at-least-once plus deduplication”). So consumers must be idempotent - processing a message twice has the same effect as once. The usual way: give every message a unique ID and record which IDs you’ve handled, in the same transaction as the work.
Order matters too: “OrderPlaced” must be handled before “OrderCancelled” for the same order. Kafka keeps order within a partition, and messages with the same key always go to the same partition - so use the order ID as the key. Messages for different orders may interleave, which is fine.
1stock = {"miso": 10}
2processed = set()
3
4def handle(message):
5 if message["id"] in processed:
6 return "duplicate, skipped"
7 stock[message["dish"]] -= message["quantity"]
8 processed.add(message["id"]) # in real life: same transaction as the update
9 return f"reserved {message['quantity']}, {stock[message['dish']]} left"
10
11for message in [{"id": "m1", "dish": "miso", "quantity": 2},
12 {"id": "m2", "dish": "miso", "quantity": 3},
13 {"id": "m1", "dish": "miso", "quantity": 2}]:
14 print(message["id"], handle(message))m1 reserved 2, 8 left m2 reserved 3, 5 left m1 duplicate, skipped
Key takeaways
Events announce facts to any number of subscribers; commands ask one service to act.
Brokers decouple services in time: a consumer can be down and catch up later.
Delivery is at-least-once, so consumers must be idempotent - track processed message IDs.
Key messages by entity (e.g. order ID) to keep each entity’s events in order within a partition.
Lesson quiz
7 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: simulate microservice patterns in Python
Build small Python simulations of the patterns - routers, sagas, outboxes, circuit breakers, traces - and run them against sample inputs. They run locally in your browser; no servers or containers needed.
An idempotent inventory consumer
The first input line is the starting stock as dish=quantity pairs. Each following line is a message: id dish quantity. Reserve stock for each message exactly once: skip duplicate IDs (m1: duplicate), and reject a message if there isn’t enough stock (m4: rejected, only 1 tonkotsu left) - a rejected message still counts as processed. Otherwise print m1: reserved 2 miso. End with the final stock in the input’s dish order and duplicates skipped: N.
- Friday night
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Partition by key
The first input line is the number of partitions. Each following line is an event order_id event_name. Assign each event to a partition with a stable hash - the sum of the key’s UTF-8 byte values modulo the partition count - and print each partition’s events in arrival order (partition 0: 42:OrderPlaced 42:OrderPaid, or partition 1: -). Then confirm ordering: order 42 stays in partition 0 for each order, sorted by order ID.
- Three partitions
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…