A Cancelled Record Still Needs To Say So

If a source system cancels something it already told a downstream system about, silence is not an answer. I was in a conversation this week about exactly this shape of problem: a feed sends details of an event as it happens, someone later voids that event back at the source, and the question on the table was simple to ask and surprisingly easy to get wrong — should the downstream side just stop seeing it, should the record disappear, or should something explicit come through saying "this happened, and then it was undone"? The honest answer is the third one, and the reason it matters goes well beyond one integration.
Every integration that streams events instead of snapshots eventually meets this problem, because real-world processes are not append-only. People book things and cancel them. Clinicians start procedures and abandon them. Orders get placed and reversed. Any system that only ever sends "this happened" messages is implicitly assuming nothing downstream will need to know when "this happened" stops being true. That assumption holds right up until the first cancellation, at which point it breaks in one of two ways, and both are worse than they look from the source side.
The first failure is silence: the source stops sending anything for the voided record, and the feed simply moves on as if it never came up. From the sending system's point of view this feels harmless, because the record really has been voided and there is technically nothing more to say. But the downstream system already has state built from the original message. It has already counted that procedure, filed that booking, or reconciled that transaction against something else. Nothing in the new silence tells it to undo any of that. The record just sits there, permanently true in one system and permanently false in another, and nobody finds out until a report doesn't balance or a customer asks about something that no longer exists on one side of the fence.
The second failure is deletion, which sounds more correct and is often worse in practice. Deleting the downstream record makes today's numbers match, but it destroys the fact that a cancellation happened at all. Six months later, someone auditing why a particular period's totals moved, or investigating whether a pattern of cancellations means something, finds nothing, because the event and its reversal have both vanished as if neither ever occurred. You've traded a reconciliation problem today for an explainability problem later, and the second one is harder to notice because it only shows up when someone specifically goes looking and finds an unexplained gap instead of an unexplained record.
The fix costs almost nothing extra to build and is easy to skip anyway, which is exactly the profile of the mistakes that persist longest in integration code. Send the cancellation as its own explicit signal — a status field, a void flag, a cancellation event with a timestamp and, ideally, a reason — rather than expecting the downstream side to infer it from absence. The event becomes a small state machine instead of a one-shot fact: created, then possibly voided, each transition visible in the data. That single design decision turns a guessing game into a lookup. The downstream system doesn't need to notice that something stopped arriving; it gets told, in the same channel it was told about the original event, that the event no longer stands.
This is where the harder design question actually lives, and it's worth taking seriously rather than assuming the obvious answer is right: once a downstream system receives that void signal, should the original record be removed from its working view, or kept and clearly marked as cancelled? I lean toward keeping it, visibly marked, for the same reason I'd keep a crossed-out line in a paper ledger rather than tearing out the page. A visible cancellation preserves the fact that something was attempted, which is often exactly the information someone needs later — how often does this get booked and then cancelled, was this voided before or after a related step happened elsewhere, did the same person cancel and rebook within minutes in a way that suggests a data-entry correction rather than a genuine change of plan. None of those questions can be answered from a record that was quietly deleted. All of them can be answered from one that says "cancelled" instead of just disappearing.
There's a sharper version of this question in anything touching healthcare or other sensitive data, which is where I've spent a fair amount of my own time. A voided clinical event isn't just a data-quality nuisance, it can be operationally and legally significant — a procedure that was started and then abandoned, a delay that was logged and then reclassified, a treatment recorded and then corrected. Deleting that history because the current state changed erases exactly the kind of detail that matters when someone later needs to reconstruct what actually happened and when. The right instinct in sensitive-data systems is almost always to keep the trail and mark the current status clearly, rather than keeping only the current status and losing the trail. That's not a compliance reflex, it's just taking seriously that other people are going to have to trust this record without having been in the room.
None of this is an argument for over-engineering every feed into a full event-sourced ledger from day one. Plenty of integrations genuinely are simple enough that "send the current state, refresh on schedule" is the right level of complexity, and adding cancellation semantics to something that will never need them is its own kind of waste. The judgment call is about which integrations carry state that other systems build decisions on top of. If a downstream consumer counts something, bills for something, reports on something, or triggers a workflow because of something arriving in the feed, that consumer needs to be told explicitly when the thing it counted stops being real. The cost of adding that signal is a field and a bit of design conversation up front. The cost of skipping it is a support investigation, months later, by someone who has to guess why two systems disagree and has none of the context the original engineers had when they decided silence was good enough.
The pattern generalises further than integrations, too. Any place where one party updates a shared record and another party has already acted on the old version of it runs into the same problem: does the correction get communicated explicitly, or does it just show up as a difference someone eventually notices? Communicating it explicitly costs a little more up front and saves a great deal of confused reconciliation later. It's a small design habit, but it's one of those habits that separates integrations people trust from integrations people quietly work around, and the difference rarely shows up in the first few weeks of testing. It shows up the first time something gets cancelled for real.


Share your thoughts