Alice unfriends her ex Bob, then posts a rude message — so Bob must not be notified. But friendship and messages live in two independent logs with no shared order. A notification consumer that joins them by arrival order processes the post before the unfriend and notifies Bob. Route both events through one ordered partition, or stamp the post with the friend-list version it depends on and make the consumer wait — and the notification is suppressed.
Alice does two things, in order: first she unfriends her ex Bob, then she posts a rude message. Because she unfriended him before posting, Bob must not be notified. But the two facts live in two independent logs — a friendship log and a message log — with no shared order between them. A notification service is a join that consumes both. Merge them by arrival order and, because the logs travel over independent transports, the post can arrive before the unfriend: the consumer still sees Alice and Bob as friends and fires a notification to Bob — a real side effect that cannot be un-sent. Two fixes the chapter names close the leak: (a) route both events for the (alice, bob) pair through the same ordered partition so the unfriend is delivered first; or (b) stamp the post with a causal reference to the friend-list version it was written against, and have the consumer wait for that version before processing.
Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 13 — A Philosophy of Streaming Systems (referred to in the book's Ch. 5 material as well), §"Ordering events to capture causality":
...the unfriend event and the message-send event may be processed in a different order by a downstream consumer, so the notification service may send the notification even though the sender unfriended the recipient first.
The book uses the break-up scenario to make one point precise: a real-world "A happened before B" survives downstream only if the system records the dependency. Two independent logs discard it, so a consumer joining them is free to process B before A. Here you trigger exactly that notification, then watch each fix suppress it.
The leak turns entirely on order that was never captured. Each line notes where it shows up.
depends_on_friend_version=2.Two independent logs have no shared order, so a consumer that joins them can see an effect before its cause. There are two ways to put the order back.
2, after the unfriend). The consumer buffers the post until it has applied friend-list version 2. The unfriend arrives, advances the version, and only then does the post get processed → no notification.All three run in one script, code/causal_notification_leak.py, against the identical facts; only how order is (or isn't) preserved differs.
cd study/ddia/ch13/code
python3 causal_notification_leak.py
Do not read the output yet — make each prediction first.
Predict. Alice unfriended Bob before posting. But the friendship log and the message log are independent, and the consumer merges them by arrival order — and the post arrives first. When the consumer processes the post, is Bob notified?
Bob is notified — the leak fires. At the instant the consumer handles the post, it has not yet seen the unfriend, so its view still lists Bob as Alice's friend. The notification goes out. The unfriend arrives one moment later, but the side effect is already irreversible.
Predict. Now both events for the (alice, bob) pair go through a single partition, in causal order: unfriend, then post. The consumer reads that one log top to bottom. Is Bob notified?
No notification. One partition means one order. The consumer applies the unfriend first — Bob leaves Alice's friend set — so when it reaches the post, there is no one to notify. The cause is physically forced ahead of the effect.
Predict. Two logs again, and the same bad arrival order — the post still lands first. But the post now carries depends_on_friend_version=2, and the consumer waits for that version before processing. Is Bob notified?
No notification — even though the post arrived first. The consumer sees the post's required friend-list version (2), notices it has only applied version 1, and buffers the post. The unfriend arrives, advances Alice's friend-list version to 2 and removes Bob, and the consumer retries the buffered post: dependency satisfied, no friends to notify. The order was put back logically.
A real-world "A then B" is only preserved downstream if the system records the dependency. Alice's unfriend truly happened before her post, but that "before" lives only in Alice's head and in wall-clock time — neither of which the pipeline carries. The friendship log and the message log are ordered internally and not with respect to each other, so the pair of events forms an antichain: to the join there is no fact of the matter about which came first, and it is free to apply them in whatever order transport happens to deliver. The buggy consumer delivers the post first, evaluates "who are Alice's friends?" against a view that still contains Bob, and fires. Nothing is broken in the notification logic; the information needed to make the right call — the post was written after Bob was removed — was thrown away upstream, at the moment the two facts were split into two independently-ordered streams.
Both fixes reintroduce the lost order; they differ in how. Fix (a) does it physically: send both events through one partition, and a single log's total order becomes the shared order the two facts lacked — the consumer cannot see the post before the unfriend because the log doesn't offer them in that order. Fix (b) does it logically: the post carries a version number naming the state it depends on, turning an invisible real-world "before" into an explicit datum the consumer can honor by waiting. The consumer buffers any event whose causal prerequisite it hasn't applied, so arrival order stops mattering — the post is processed against the correct, post-unfriend view no matter when it lands. One buys ordering by constraining routing; the other buys it by carrying metadata and paying a little latency (the buffer).
Sources: DDIA 2e, Ch. 13, §"Ordering events to capture causality" · Lamport, "Time, Clocks, and the Ordering of Events in a Distributed System" (1978).