Apply Incoming Events Without Treating Arrival Order as Truth

Drafted and reviewed using AI assistance.

On this page

When incoming events arrive twice or out of order, “use the latest event” is an incomplete instruction. Latest could mean last received, largest timestamp, newest resource version or current authoritative state. Give the coding agent a record it can walk through, including what to do when those signals disagree.

Stripe's webhook documentation is a concrete provider example: delivery order is not guaranteed, timestamps can be shared, and duplicate deliveries need handling. Its duplicate-event section discusses remembering processed event IDs. This guide uses those concerns to build a different, fictional protocol. The resource version below is our design choice, not a Stripe field or API-version guarantee.

Define what an event identifies

Imagine a planning board receiving complete snapshots of cards. It starts with:

{
  "card-7": {"version": 10, "state": {"stage": "queued", "label": "Map"}},
  "card-8": {"version": 3, "state": {"stage": "ready", "label": "Chart"}}
}

The protocol uses this envelope:

{
  "event_id": "e-201",
  "object_id": "card-7",
  "object_version": 12,
  "occurred_at": "2024-01-01T08:00:00Z",
  "type": "card.snapshot",
  "state": {"stage": "ready", "label": "Map"}
}
Field Meaning in this example
event_id Identity of one event, retained across its redeliveries
object_id Card whose complete state is represented
object_version Source-assigned increasing integer for that card; comparable only within that card
occurred_at Informational UTC time; neither an ordering key nor a deduplication key
type Only card.snapshot is supported here
state Complete stage and label snapshot, not a patch to unspecified fields

All records, IDs, versions and outcomes are original teaching data. The receiver assumes input has already passed the real integration's authenticity and schema boundary; it does not implement that boundary or contact Stripe. B62-15-events.json supplies every delivery and both reconciliation snapshots.

Versions may have gaps because each message carries a full snapshot. Applying version 12 over 10 therefore need not reconstruct 11. A delta protocol would require a different gap policy. Do not infer that every provider offers this version contract just because the receiver needs an ordering decision.

Choose the application rule before replaying the stream

For a known card that is not paused:

  1. Compare the event with the retained event-ID record. An identical event already handled has no new effect. The same ID with different semantic content is a conflict: preserve the first record, hold the incoming variant and pause its affected known card.
  2. Require a supported type, known object and valid integer version. Missing evidence is held for reconciliation; an unknown object is not automatically created.
  3. A higher version replaces the card's complete state. A lower version is recorded as stale without changing the card. An equal version with identical state has no new effect; a different state at that same version is a conflict.
  4. For a paused card, hold further otherwise valid events until an explicit reconciliation decision.

The semantic comparison includes the event envelope shown above, excluding delivery-only transport metadata. Different JSON key order or whitespace is not a changed event. Equal-version state comparison concerns the snapshot's state, rather than the event's distinct ID or informational time.

The proposed effect is a local card-state replacement. Commit that replacement and its event decision together. A sequential in-memory reducer can demonstrate the decisions, but cannot prove that a database transaction, concurrent receiver or downstream notification has this boundary.

Walk every delivery with the same IDs

Let T mean 2024-01-01T08:00:00Z. In this table, ready/Map abbreviates the complete state {"stage":"ready","label":"Map"}. Arrival order is the numbered row, not the event timestamp.

Delivery Complete meaningful payload Decision Card state afterward / acknowledgement
1 e-201, card-7, v12, T, ready/Map Apply v12 and record e-201 together card-7 v12 ready/Map; acknowledgement lost
2 Exact e-201 again Known identical event; no new state change card-7 v12 ready/Map; acknowledge existing decision
3 e-200, card-7, v11, T, building/Map Older version; record stale card-7 stays v12; acknowledge recorded decision
4 e-202, card-7, v13, T+1s, ready/Map v2 Apply v13 card-7 v13 ready/Map v2; acknowledge
5 e-207, card-7, v13, T+2s, ready/Map v2 Different event, identical version/state; no effect card-7 stays v13; acknowledge recorded decision
6 e-202 again, but label is Forge Same ID, changed payload; hold variant and pause card-7 card-7 stays v13 ready/Map v2; acknowledge only its durable hold
7 e-203, card-7, v14, T+3s, archived/Map v2 Card paused; hold card-7 stays v13; acknowledge only its durable hold
8 e-300, card-99, v1, T+3s, queued/New Unknown object; hold, do not create card-99 absent; acknowledge only its durable hold
9 e-204, card-8, version absent, T+4s, archived/Chart Missing version; hold and pause card-8 card-8 stays v3 ready/Chart; acknowledge only its durable hold
10 e-205, card-8, v4, T+5s, building/Chart Card paused; hold card-8 stays v3; acknowledge only its durable hold

Rows 1 and 3 share exactly the same timestamp. A timestamp tie says nothing about which snapshot is newer; versions distinguish them in this invented protocol. Row 2 also demonstrates why losing an acknowledgement is not proof that the first application failed.

The acknowledgement policy is another explicit choice: after the service has durably recorded an applied, ignored or held decision, it may acknowledge receipt. A held decision means the service has accepted responsibility for reconciliation, not that it applied the state. If it cannot durably record the decision, it must not claim that responsibility. The model does not implement an HTTP response or durable inbox.

An event-ID list alone does not solve row 6. Silently discarding every repeated ID would hide a changed-payload conflict. Nor should the receiver overwrite the first stored event with the variant and lose the evidence it needs to investigate.

Reconcile without declaring an arbitrary winner

The conflicting e-202 variant does not explain why its content changed. The board pauses rather than deciding that the first or last arrival must be true.

For this exercise, a separately supplied authoritative snapshot at T+20s establishes:

Object Authoritative snapshot Disposition
card-7 v14, archived/Map v2 Replace from this authority, resolve the pause, then reconsider held e-203
card-8 v5, ready/Chart Replace from this authority, resolve the pause, then reconsider held e-205
card-99 No authoritative identity supplied Keep e-300 held; do not invent a card

These are fixed reconciliation fixtures, not responses fetched from an account. A real integration must establish the authority, scope and version semantics of its lookup before applying the same policy.

After reconciliation, e-203 is equal to card-7's v14 snapshot and needs no new effect. e-205 is older than card-8's v5 and is recorded stale. The unversioned e-204 is retained as unresolved input; the authoritative snapshot supersedes its need for a state decision, without manufacturing a version for that event. The original e-202, its conflicting variant and the resolution remain in the audit record.

The final application state is therefore exactly:

{
  "card-7": {"version": 14, "state": {"stage": "archived", "label": "Map v2"}},
  "card-8": {"version": 5, "state": {"stage": "ready", "label": "Chart"}}
}

There were two event-driven state replacements before reconciliation, followed by two authoritative replacements. The duplicate, stale and equal-state deliveries did not create extra replacements. This accounting describes the fixture's reducer; it is not an exactly-once delivery or effect guarantee.

For the example, event identities and decisions are retained for 24 hours; every shown delivery is within that window. After that window, an absent identity record alone cannot distinguish a new event from an old replay. Reconciliation or an explicit expiry-recognition policy is needed. No unlimited deduplication lifetime is implied.

Copy the receiver-state brief

Design a local receiver-state model for the fictional card.snapshot protocol.
Use B62-15-events.json, its ten deliveries and two authority snapshots.
Do not build a webhook server or contact a provider.

Treat event_id as event identity and object_id as resource identity.
object_version is an increasing integer per card in this fictional contract.
state is a full snapshot. occurred_at is informational, not an ordering key.
Do not assume a real provider exposes this version or authority model.

Keep the first event-ID record and compare semantic content on redelivery.
Same ID/same content: recover the existing decision with no new effect.
Same ID/changed content: retain both variants, pause the affected known card,
and hold subsequent events. Do not overwrite the first record.

For unpaused known cards: higher version replaces the complete state;
older version is stale; equal version/equal state is a no-op;
equal version/different state is a conflict requiring reconciliation.
Hold unknown objects, missing versions and unsupported types without mutation.
Durable hold is not successful application.

Demonstrate the acknowledgement-lost/repeated-event case, equal timestamps,
out-of-order v11 after v12, changed e-202 payload, missing version and unknown
card. Reconcile only from the supplied authority snapshots, then classify
held e-203 as equal and e-205 as stale. Preserve the unresolved input/audit trail.

Return every intermediate card version/state, event decision, pause/hold,
acknowledgement meaning, final state and state-replacement count.
Explain the real storage transaction and concurrency boundary needed to commit
state and receipt together before implementing it. Downstream notifications,
authentication, storage retention enforcement and provider APIs are outside
this model. Separate executed model checks from unimplemented guarantees.

The useful output is a converging state history with named unresolved inputs. “Deduplicate events” becomes actionable only when the agent can say which event it recognized, which object it changed, and what evidence let it advance that object's state.