Vol. 4 · № 02 · Un atelier d'architectureAn architecture workshop

Event-Driven & Message Queue, ou le fait déposé dans un tamponEvent-Driven & Message Queue, or the fact dropped into a buffer

Livrer un fait à chaque consommateur en direct transmet le pic tel quel et perd l'événement quand un service est absent. Intercaler une file de messages pose un tampon durable : elle amortit les pics, survit aux pannes, diffuse à N consommateurs — Observer (Vol 3) durci et réseauté. Mais la file ne promet qu'« au moins une fois, sans ordre » : tout consommateur doit être idempotent et tolérant au désordre, sous peine de bâtir une machine à doublons.Delivering a fact to each consumer directly passes the spike through as-is and loses the event when a service is absent. Inserting a message queue lays a durable buffer: it absorbs spikes, survives outages, broadcasts to N consumers — Observer (Vol 3) hardened and networked. But the queue promises only 'at-least-once, no order': every consumer must be idempotent and reorder-tolerant, or you build a duplicate machine.

AudienceAudience
Dev qui veut découpler par événements sans perdre la trace ni doubler les effetsDev who wants to decouple by events without losing the trace or doubling effects
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026June 2026
≈ 18 min●●○○CommunicationEvent-DrivenTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

Un fait, cinq consommateurs, aucun tampon. Et si l'un est débordé, ou absent ?One fact, five consumers, no buffer. And if one is overwhelmed, or absent?

Au #1, on a coupé le fil : « commande payée » n'attend plus le mail. Mais l'émetteur appelle encore chaque consommateur en direct, un à un. Deux choses varient désormais ensemble : la liste de ceux qui réagissent — Logistique, Growth, Data, Fidélité, demain d'autres — et la charge dans le temps, qui explose un jour de soldes. Sans rien entre l'émetteur et eux, un pic les écrase et une panne perd l'événement. Le réflexe : ne livre pas le fait à des destinataires ; dépose-le dans une file, et laisse chacun le consommer à son rythme.In #1 we cut the thread: 'order paid' no longer waits for the email. But the emitter still calls each consumer directly, one by one. Two things now vary together: the list of reactors — Logistics, Growth, Data, Loyalty, more tomorrow — and the load over time, which explodes on a sale day. With nothing between the emitter and them, a spike crushes them and an outage loses the event. The reflex: don't deliver the fact to recipients; drop it in a queue, and let each consume it at its own pace.

Le code naïf — l'émetteur livre à chacun, en directThe naive code — the emitter delivers to each, directly
def on_order_paid(order):
    # async, oui — mais l'émetteur appelle chaque consommateur, en direct
    warehouse_api.post("/reserve", order)    # l'équipe Logistique
    email_api.post("/receipt", order)        # l'équipe Growth
    analytics_api.post("/track", order)      # l'équipe Data
    loyalty_api.post("/points", order)       # … et demain Fidélité, Compta, Anti-fraude
    # un pic de commandes ? chaque service le prend de plein fouet.
    # Analytics en plein déploiement ? son événement est perdu, sans trace.
Livrer vs déposerDeliver vs drop off
post(/reserve)…post(/reserve)…
livrer à chacun · couplédeliver to each · coupled
|
vsvs
publish(OrderPaid)publish(OrderPaid)
déposer une fois · libredrop once · free

Livrer, c'est connaître chaque destinataire et le supposer prêt, maintenant. Déposer, c'est énoncer un fait une seule fois et laisser une file le garder jusqu'à ce que chacun vienne le prendre — prêt ou pas, débordé ou en panne.Delivering means knowing each recipient and assuming it ready, now. Dropping off means stating a fact once and letting a queue hold it until each comes to take it — ready or not, swamped or down.

Le réflexe du volumeThe volume's reflex

« Ai-je besoin d'une réponse maintenant, ou seulement que ce soit fait un jour ? » Au #1, la réponse « un jour » justifiait de couper le fil. Ici elle justifie d'intercaler une file : si personne n'attend de réponse, l'émetteur n'a aucune raison de connaître ses consommateurs, ni de subir leur charge ou leurs pannes. La file devient le point de découplage — dans l'espace (qui) et dans le temps (quand).'Do I need an answer now, or only that it gets done someday?' In #1, the 'someday' answer justified cutting the thread. Here it justifies inserting a queue: if no one awaits a response, the emitter has no reason to know its consumers, nor to bear their load or their outages. The queue becomes the decoupling point — in space (who) and in time (when).

Observer, porté sur le réseau.Observer, carried onto the network.

C'est exactement Observer (Vol 3 #2), poussé hors du processus : l'émetteur publie un fait, des consommateurs s'y abonnent, et il ignore qui ils sont. La nouveauté n'est pas le découplage du savoir — Observer l'avait — mais le tampon durable inséré entre eux : la file persiste les faits, absorbe les pics et survit aux pannes. Pub/Sub en mémoire devient Event Bus puis file de messages sur le réseau.It's exactly Observer (Vol 3 #2), pushed beyond the process: the emitter publishes a fact, consumers subscribe, and it ignores who they are. The novelty isn't decoupling knowledge — Observer had that — but the durable buffer inserted between them: the queue persists facts, absorbs spikes and survives outages. In-memory Pub/Sub becomes Event Bus then message queue on the network.

№ 02La douleurThe pain4 min

Le pic n'a nulle part où attendre, et l'absent perd son fait.The spike has nowhere to wait, and the absent loses its fact.

Livrer chaque fait en direct, même en asynchrone, laisse trois douleurs sans réponse. Sans tampon, un pic se transmet tel quel : l'émetteur produit 10× plus vite que la Logistique ne digère, et le service fond. Sans persistance, un consommateur en panne ne rattrape rien : son événement est passé, perdu. Et la liste des destinataires reste dans l'émetteur : ajouter Compta, c'est rouvrir le code qui publie.Delivering each fact directly, even asynchronously, leaves three pains unanswered. Without a buffer, a spike passes through as-is: the emitter produces 10× faster than Logistics digests, and the service melts. Without persistence, a downed consumer catches up on nothing: its event went by, lost. And the recipient list stays in the emitter: adding Accounting means reopening the publishing code.

№ 1 · Le pic n'est pas amortiThe spike isn't absorbed

Un jour de soldes, les commandes affluent à 10× le débit normal. La Logistique encaisse 100/s, on lui en envoie 1000/s : timeouts, files internes saturées, effondrement. Le rythme de l'émetteur dicte celui du plus lent des consommateurs.On a sale day, orders pour in at 10× the normal rate. Logistics handles 100/s, you send 1000/s: timeouts, saturated internal buffers, collapse. The emitter's pace dictates that of the slowest consumer.

№ 2 · L'absent perd l'événementThe absent loses the event

Analytics redémarre pour un déploiement de deux minutes. Les faits publiés pendant ce temps n'ont nulle part où être gardés : ils sont passés, personne ne les a pris. La donnée est trouée, et rien ne le signale.Analytics restarts for a two-minute deploy. Facts published during that window have nowhere to be held: they went by, no one took them. The data is full of holes, and nothing flags it.

№ 3 · Le fan-out est codé en durThe fan-out is hardcoded

Chaque nouveau consommateur — Fidélité, Compta, Anti-fraude — est une ligne de plus dans on_order_paid. L'émetteur connaît tout le monde et grossit à chaque équipe qui veut réagir. C'est le couplage qu'Observer visait, revenu sur le réseau.Each new consumer — Loyalty, Accounting, Anti-fraud — is one more line in on_order_paid. The emitter knows everyone and grows with every team that wants to react. It's the coupling Observer targeted, back on the network.

Le pic transmis tel quelThe spike passed through as-is
# soldes : 1000 commandes/s
for order in flood:
    warehouse_api.post("/reserve", order)   # capacité : 100/s → 💥 sature
    analytics_api.post("/track", order)     # en déploiement → 💥 perdu
# l'émetteur n'a pas de frein, les consommateurs pas de file d'attente.
Le diagnostic.The diagnosis.

Le détecteur : « la charge est-elle irrégulière, les consommateurs nombreux ou faillibles, et personne n'attend de réponse ? ». Si oui, intercale une file. Elle fait trois métiers à la fois : amortir (elle se remplit au pic, se vide au calme — le lissage de charge), retenir (le message persiste jusqu'à ce qu'un consommateur le prenne — il survit aux pannes), et diffuser (N consommateurs s'abonnent sans que l'émetteur les connaisse). Le réflexe : un fait publié, un tampon durable, des consommateurs indépendants.The detector: 'is the load uneven, the consumers many or fallible, and no one awaits a response?'. If yes, insert a queue. It does three jobs at once: absorb (it fills at the spike, drains in the calm — load leveling), retain (the message persists until a consumer takes it — it survives outages), and broadcast (N consumers subscribe without the emitter knowing them). The reflex: one published fact, a durable buffer, independent consumers.

№ 03Le tamponThe buffer5 min

On intercale une file : elle retient, elle amortit, elle diffuse.Insert a queue: it retains, it absorbs, it broadcasts.

La file n'est pas un tuyau de plus : c'est un tampon durable posé entre l'émetteur et ses consommateurs. L'émetteur y dépose un fait et rend la main ; la file le garde jusqu'à ce qu'on vienne le prendre. L'animation insère ce tampon, puis montre ses trois métiers : retenir le message pour qu'une panne ne le perde pas, amortir un pic en se remplissant, diffuser à N consommateurs qui lisent chacun à leur curseur.The queue isn't one more pipe: it's a durable buffer placed between the emitter and its consumers. The emitter drops a fact and returns; the queue holds it until someone takes it. The animation inserts that buffer, then shows its three jobs: retain the message so an outage doesn't lose it, absorb a spike by filling up, broadcast to N consumers each reading at its own cursor.

Animation — le tampon entre l'émetteur et ses consommateursAnimation — the buffer between emitter and consumers
Départ — livraison directeStart — direct delivery · 1/4
OrderService
livre · couplédelivers · coupled
▶
Logistique
frappé par le pichit by the spike
Analytics
en panne · perdudown · lost
Email
frappé par le pichit by the spike
pas de tampon : le pic frappe, l'absent perdno buffer: the spike hits, the absent loses
L'émetteur pousse vers chaque consommateur. Le pic les frappe tel quel, et celui qui est en panne ne reçoit rien : l'événement est passé, perdu.The emitter pushes to each consumer. The spike hits them as-is, and the one that's down receives nothing: the event went by, lost.

Déposer un fait, lire à son rythme — 3 lignes à retrouverDrop a fact, read at your own pace — 3 lines to recall
# l'émetteur — dépose un fait, ne connaît aucun consommateur
# chaque équipe — un groupe de consommateurs indépendant, son propre curseurdef reserve(evt): warehouse.reserve(evt.order_id)   # amorti, rejoué si panne
def track(evt): analytics.track(evt.order_id)
3 ligne(s) à retrouver — écris-la de tête avant de cliquer3 line(s) to recall — write it in your head before clicking
Le motif transférable.The transferable pattern.

« Publie un fait dans un tampon durable ; qui le lit, et quand, ne te regarde pas. » Le même tampon, selon ce qu'on en attend, s'appelle Event Bus (la diffusion interne), file de tâches (le lissage d'un travail lourd), ou journal d'événements (le fait conservé pour être rejoué — pont vers l'Event Sourcing, Vol 5 #3). Une seule pièce, trois usages : amortir, survivre, diffuser.'Publish a fact into a durable buffer; who reads it, and when, is none of your business.' The same buffer, depending on what you want from it, is called an Event Bus (internal broadcast), a task queue (leveling heavy work), or an event log (the fact kept to be replayed — bridge to Event Sourcing, Vol 5 #3). One piece, three uses: absorb, survive, broadcast.

Modèle mentalMental model
L'imageThe image

Le rail à commandes d'une cuisine de restaurant : les serveurs y accrochent les tickets, les cuisiniers les décrochent à leur rythme. Un coup de feu remplit le rail ; un cuisinier lent ne bloque jamais la salle.A restaurant kitchen's order rail: waiters clip tickets onto it, cooks pull them at their own pace. A rush fills the rail; a slow cook never blocks the dining room.

La règleThe rule

Publie un fait dans un tampon durable ; qui le lit et quand ne te regarde pas — mais conçois chaque lecteur pour « au moins une fois, sans ordre ».Publish a fact into a durable buffer; who reads it and when is none of your business — but design every reader for 'at-least-once, no order'.

Où l'analogie casseWhere the analogy breaks

Sur le rail, un ticket reste où on l'a accroché et n'est décroché qu'une fois. La file ne promet ni l'un ni l'autre : elle peut relivrer le même message (au moins une fois) et le sortir dans le désordre. Le confort « ordonné, une fois » du rail est précisément le pari qui casse au chapitre suivant.On the rail, a ticket stays where it was clipped and is pulled only once. The queue promises neither: it can redeliver the same message (at-least-once) and hand it out of order. The rail's 'ordered, once' comfort is exactly the bet that breaks in the next chapter.

№ 04Le piègeThe trap4 min

Brancher une file ne suffit pas. Le consommateur qui parie exactement-une-fois.Wiring a queue isn't enough. The consumer that bets on exactly-once.

Le piège n'est pas d'oublier la file — c'est de la traiter comme un appel de fonction fiable. Une file de messages donne deux garanties faibles et contre-intuitives : au moins une fois (un message peut être relivré) et aucun ordre global (OrderShipped peut précéder OrderPaid). Un consommateur écrit comme si chaque événement arrivait une seule fois, dans l'ordre, devient une machine à doublons et à courses. Bascule le toggle : cherche l'effet qui ne supporte ni rejeu ni désordre.The trap isn't forgetting the queue — it's treating it like a reliable function call. A message queue gives two weak, counter-intuitive guarantees: at-least-once (a message may be redelivered) and no global ordering (OrderShipped can precede OrderPaid). A consumer written as if each event arrives once, in order, becomes a duplicate-and-race machine. Flip the toggle: hunt the effect that tolerates neither replay nor reordering.

# la file garantit AU MOINS une fois, sans ordre global → on s'y prépare
@subscribe("OrderPaid", group="logistique")
def reserve(evt):
    if seen(evt.id): return                 # idempotent : rejouer ne re-réserve pas
    mark_seen(evt.id)
    warehouse.reserve(evt.order_id)

@subscribe("OrderShipped", group="logistique")
def ship(evt):
    order = load(evt.order_id)
    if order.state != "paid": defer(evt)    # tolère le désordre : pas encore payé ? on attend
    else: warehouse.dispatch(order)
Pourquoi c'est justeWhy it's right

Le consommateur épouse les garanties réelles de la file. Idempotent : il note les identifiants déjà traités, donc rejouer un message ne refait rien. Tolérant au désordre : avant d'expédier, il vérifie l'état attendu et diffère si le préalable manque. La relivraison devient inoffensive, le désordre se résorbe tout seul. C'est le seul code qui mérite le nom d'event-driven : conçu pour au moins une fois, sans ordre.The consumer embraces the queue's real guarantees. Idempotent: it records already-processed ids, so replaying a message does nothing twice. Reorder-tolerant: before shipping, it checks the expected state and defers if the prerequisite is missing. Redelivery becomes harmless, reordering resolves itself. It's the only code that earns the name event-driven: built for at-least-once, no order.

La règle qui tranche.The rule that settles it.

Une file livre au moins une fois et sans ordre global : tout consommateur doit être idempotent et indifférent à l'ordre, sinon on a bâti une machine à doublons. Les tests jumeaux : « si ce message arrive deux fois, l'effet est-il le même qu'une fois ? » et « si celui-ci précède son préalable, est-ce que je diffère au lieu d'agir à tort ? ». Si une réponse est non, ce n'est pas la file qu'il faut blâmer — c'est le pari du consommateur.A queue delivers at-least-once and without global order: every consumer must be idempotent and order-indifferent, or you've built a duplicate machine. The twin tests: 'if this message arrives twice, is the effect the same as once?' and 'if this one precedes its prerequisite, do I defer instead of acting wrongly?'. If either answer is no, don't blame the queue — blame the consumer's bet.

Le piège inverse : tout-événement.The opposite trap: event-everything.

L'autre excès : transformer chaque appel de fonction interne en événement « pour découpler ». On obtient un spaghetti d'événements qu'aucune pile d'appel ne raconte plus — suivre une commande exige de reconstituer une chaîne invisible à travers six services. Et le piège jumeau, la double écriture : publier l'événement après avoir commité en base, sans transaction commune, perd des messages au crash. La réponse a un nom : l'Outbox (Vol 4 #1) — écrire le fait et le message dans la même transaction.The other excess: turning every internal function call into an event 'to decouple'. You get an event spaghetti no call stack narrates anymore — tracing one order means reconstructing an invisible chain across six services. And the twin trap, the dual write: publishing the event after committing to the DB, with no shared transaction, loses messages on a crash. The answer has a name: the Outbox (Vol 4 #1) — write the fact and the message in the same transaction.

№ 05Bilan & éditoWrap-up & editorial2 min

Livrer à chaque consommateur, ou déposer un fait dans un tampon durable.Deliver to each consumer, or drop a fact into a durable buffer.

Les deux approches, côte à côteThe two approaches, side by side
appel directdirect callfile de messagesmessage queue
Un pic de chargeA load spikefrappe le consommateurhits the consumerabsorbé par la fileabsorbed by the queue
Consommateur en panneConsumer downévénement perduevent lostmessage gardé, rejouémessage kept, replayed
Ajouter un consommateurAdd a consumerrouvrir l'émetteurreopen the emitterun abonné de plus, émetteur intactone more subscriber, emitter untouched
LivraisonDeliveryexactement une fois, ordonnéeexactly once, orderedau moins une fois, sans ordreat-least-once, unordered
Le prixThe pricecouplage & fragilitécoupling & fragilitycohérence à terme, broker à exploitereventual consistency, broker to run

Quand ne pas sortir la file de messagesWhen not to reach for the message queue

Une file est de l'infrastructure : un broker à exploiter, une cohérence à terme à assumer, un flux à tracer sur plusieurs services. On garde l'appel direct — et c'est correct — quand :A queue is infrastructure: a broker to operate, eventual consistency to accept, a flow to trace across several services. Keep the direct call — and that's fine — when:

  • tu as besoin de la cohérence maintenant : si le résultat doit être vrai au moment où tu réponds (le solde après un virement), la file et son « un jour » sont la mauvaise sémantique. C'est le tri du #1 : la réponse gate-t-elle la décision ?you need consistency now: if the result must be true the moment you answer (the balance after a transfer), the queue and its 'someday' are the wrong semantics. It's #1's sorting: does the answer gate the decision?
  • il y a un seul consommateur connu et stable : pas de fan-out, pas de pic, un service toujours disponible. La file n'amortit rien et ne diffuse à personne ; elle n'ajoute qu'un broker à exploiter — de la cérémonie (YAGNI, Vol 0 #3).there's a single, known, stable consumer: no fan-out, no spike, an always-available service. The queue absorbs nothing and broadcasts to no one; it only adds a broker to run — ceremony (YAGNI, Vol 0 #3).
  • la traçabilité prime sur le découplage : un flux critique qu'il faut pouvoir suivre d'un bout à l'autre se lit mieux en appels explicites qu'en événements anonymes. Ne disperse pas dans une file ce que tu auras besoin de raconter en ligne droite.traceability matters more than decoupling: a critical flow you must follow end to end reads better as explicit calls than as anonymous events. Don't scatter into a queue what you'll need to narrate in a straight line.

Le réflexe n'est donc pas « mets une file partout, c'est découplé ». C'est : « la charge est-elle irrégulière, les consommateurs multiples ou faillibles, et personne n'attend de réponse ? Si oui, dépose un fait dans un tampon durable — mais conçois chaque consommateur pour au moins une fois, sans ordre. Sinon, appelle, et garde le flux traçable. »So the reflex isn't 'put a queue everywhere, it's decoupled'. It's: 'is the load uneven, the consumers many or fallible, and no one awaits a response? If yes, drop a fact into a durable buffer — but design every consumer for at-least-once, no order. If not, call, and keep the flow traceable.'

Mot de l'éditeurFrom the editor

L'event-driven est la rencontre de deux numéros qu'on a déjà écrits. Observer (Vol 3 #2) a coupé le savoir : l'émetteur ignore qui réagit. Synchrone vs asynchrone (Vol 4 #1) a coupé le temps : personne n'attend la réponse. La file de messages ne fait que durcir et réseauter cette double coupure — elle pose un tampon qui survit aux pannes là où Observer n'avait qu'une liste en mémoire. Ce qu'on achète est réel : absorber les pics, survivre aux absences, diffuser sans se connaître. Ce qu'on paie l'est tout autant : la cohérence à terme, l'absence d'ordre, la relivraison, et un broker de plus à garder en vie.Event-driven is where two issues we've already written meet. Observer (Vol 3 #2) cut knowledge: the emitter ignores who reacts. Synchronous vs asynchronous (Vol 4 #1) cut time: no one awaits the response. The message queue merely hardens and networks that double cut — it lays a buffer that survives outages where Observer had only an in-memory list. What you buy is real: absorb spikes, survive absences, broadcast without knowing each other. What you pay is just as real: eventual consistency, no ordering, redelivery, and one more broker to keep alive.

Le danger n'est pas la file : c'est l'enthousiasme. Transformer chaque appel en événement « parce que c'est découplé » fabrique un système que plus personne ne sait suivre — un monolithe distribué, mais par messages. L'architecte ne demande pas « est-ce event-driven ? » mais « ce fait mérite-t-il un tampon durable, et puis-je encore raconter ce qui se passe ? ». Le prochain numéro pose la question qui reste : quand plus personne n'attend en ligne droite, qui tient le fil d'une transaction métier en plusieurs étapes ?The danger isn't the queue: it's the enthusiasm. Turning every call into an event 'because it's decoupled' builds a system no one can follow anymore — a distributed monolith, but by messages. The architect doesn't ask 'is it event-driven?' but 'does this fact deserve a durable buffer, and can I still narrate what happens?'. The next issue poses the question that remains: when no one waits in a straight line, who holds the thread of a multi-step business transaction?

Les voisins que ce numéro débloqueThe neighbors this issue unlocks

  • Pub/Sub — le modèle de diffusion de la file : un fait publié, N abonnés indépendants. C'est Observer (Vol 3 #2) à l'échelle du réseau, avec un tampon durable entre l'émetteur et ses publics.the queue's broadcast model: one published fact, N independent subscribers. It's Observer (Vol 3 #2) at network scale, with a durable buffer between the emitter and its audiences.
  • Event Bus — la file vue comme une colonne vertébrale interne : tous les modules y publient et s'y abonnent. Pratique pour un monolithe modulaire (Vol 6 #1) ; dangereux dès qu'on l'utilise pour masquer un couplage qu'il faudrait nommer.the queue seen as an internal backbone: every module publishes to it and subscribes. Handy for a modular monolith (Vol 6 #1); dangerous the moment it hides a coupling you should name.
  • Outbox pattern — la pièce qui ferme le piège de la double écriture : enregistrer le message dans la même transaction que la donnée métier, puis le relayer vers le broker. La garantie « publié si et seulement si commité », reprise du #1.the piece that closes the dual-write trap: record the message in the same transaction as the business data, then relay it to the broker. The 'published if and only if committed' guarantee, carried over from #1.

Le kata — écris-le, ne le relis pasThe kata — write it, don't reread it

Tu as vu la file relivrer et désordonner à l'animation, et repéré le pari cassé au chapitre 4. Reste le geste qui ancre : écrire le consommateur qui absorbe les deux faiblesses. Copie ce fichier dans ton langage et ton runner habituels — la spec tient dans les noms des tests — et fais-les passer au vert sans revenir au numéro. Le dernier test est le sournois : il vérifie que tu dédoublonnes par identifiant de message, pas par effet — sinon un second paiement légitime sur la même commande serait avalé comme un doublon.You watched the queue redeliver and reorder in the animation, and spotted the broken bet in chapter 4. What's left is the anchoring gesture: writing the consumer that absorbs both weaknesses. Copy this file into your usual language and runner — the spec fits in the test names — and turn them green without coming back to the issue. The last test is the sneaky one: it checks that you dedup by message id, not by effect — otherwise a legitimate second payment on the same order would be swallowed as a duplicate.

test_consumer.py
# test_consumer.py — la file promet « au moins une fois, sans ordre ».
# Écris le consommateur qui rend ces deux faiblesses inoffensives.

def test_traite_une_fois():
    c = Consumer()
    c.handle(Event("evt-1", "OrderPaid", order=42))
    assert reserved(42) == 1                  # un événement → un effet

def test_rejeu_ne_double_pas():
    c = Consumer()
    e = Event("evt-1", "OrderPaid", order=42)
    c.handle(e); c.handle(e)                   # même message relivré…
    assert reserved(42) == 1                   # …un seul effet : idempotent

def test_desordre_differe():
    c = Consumer()
    c.handle(Event("evt-9", "OrderShipped", order=42))  # arrive AVANT le paiement
    assert shipped(42) == 0                     # préalable absent → on diffère, rien n'est expédié

def test_le_differe_repart_apres_son_prealable():
    c = Consumer()
    c.handle(Event("evt-9", "OrderShipped", order=42))  # différé…
    c.handle(Event("evt-1", "OrderPaid", order=42))     # …le préalable arrive
    assert shipped(42) == 1                     # …et le différé repart tout seul

def test_dedup_par_id_pas_par_effet():
    c = Consumer()
    c.handle(Event("evt-1", "OrderPaid", order=42))
    c.handle(Event("evt-2", "OrderPaid", order=42))     # DEUX événements distincts, même commande
    assert reserved(42) == 2                    # deux ids = deux effets : on dédoublonne l'ID,
    #                                             PAS l'effet — sinon on avale un vrai 2e paiement.
Chez toi, cette semaine.In your codebase, this week.

Ouvre un consommateur de file existant et pose-lui les deux tests jumeaux du chapitre 4. Un point de départ pour les trouver : grep -rn "@subscribe\|on_message\|consume\|@app.task\|SqsListener" src/. Pour chaque handler, demande : « si ce message arrive deux fois, l'effet est-il le même qu'une fois ? » — sinon il manque une clé d'idempotence — et « s'il précède son préalable, est-ce que je diffère au lieu d'agir à tort ? ». Le premier handler qui répond « non » aux deux est ta machine à doublons.Open an existing queue consumer and put chapter 4's twin tests to it. A starting point to find them: grep -rn "@subscribe\|on_message\|consume\|@app.task\|SqsListener" src/. For each handler, ask: 'if this message arrives twice, is the effect the same as once?' — if not, an idempotency key is missing — and 'if it precedes its prerequisite, do I defer instead of acting wrongly?'. The first handler that answers 'no' to both is your duplicate machine.