Compensating Events Won't Save You¶
A customer calls about their account statement. One line records a payment of 4,800 euros, and nobody at your company can explain where it came from. You look into the event store, and there it is: payment-received, timestamped, hash-chained, immutable. The event is wrong, and it will be wrong forever.
Ask around and you will hear the same answer everywhere: write a compensating event – a new event that reverses the effect of an earlier one, the way a refund reverses a payment. It is good advice, and for two of the three situations that produce a wrong event, it is exactly right. For the third, it makes things worse – and the third is the one nobody warns you about.
Facts Don't Lie, Records Do¶
There is a sentence that gets repeated so often in Event Sourcing that it has stopped carrying meaning: you cannot change the past. We have said it that way ourselves – Thinking in Events puts it as "a correction is a new Event, never a changed one" – and as a principle it holds. It just does not hold for the event store alone: the world is every bit as immutable as your append-only log. The customer either transferred that money on a Tuesday in March or they did not, and no amount of engineering will revisit that Tuesday.
So if both are equally unchangeable, why does one of them cause trouble? Because immutability is not the interesting difference between them. A fact simply is. An event is a claim about that fact – and claims can be wrong.
That distinction sounds academic until you try to fix something. "Can we correct the past?" is unanswerable, and teams burn weeks on it. "What exactly was wrong?" has three possible answers, and each one leads somewhere different. Either the claim did not match the fact, or the claim matched it and the world moved on, or there was no fact for the claim to match at all. Call them the typo, the reversal, and the phantom – the names matter less than the habit of asking which of the three you have.
One thing this post is not about: events whose shape no longer fits. Renaming a field, splitting a payload, turning a string into an array – that is schema evolution, it has a well-worn answer in upcasting, and we covered it in Versioning Events Without Breaking Everything. Here, the structure is fine. The content is the problem.
The Typo That Isn't a Change¶
Start with the harmless one, the typo. A support agent registers a new customer and types Meyer. The man's name is Meier. The store now holds customer-registered with a name that was never his.
Notice what did not go wrong. The fact in the world was correct the entire time – he has always been Meier, before the registration and after it. Nothing in the business changed, nothing needs to be undone. Only the sentence your system wrote down was false, and the moment someone noticed, that noticing became a fact in its own right. A correction is not a repair of history; it is a new event about a mistake somebody made.
Which means the domain already has words for it. Somebody spotted a data entry error and fixed it. That is an ordinary business occurrence, and it deserves an ordinary business event: customer-name-corrected, carrying the right name and, ideally, who noticed. Such an event is written today about something that went wrong earlier, so it carries two different times, and keeping them apart is its own small discipline – we worked through it in Time is of the Essence.
What it must not be is customer-name-changed. That event already means something else: the man got married, or changed his name by deed poll. Those two events look identical in their payload and mean opposite things – one says the world moved, the other says we wrote it down wrong. Flatten them into one type, and you can never tell them apart again.
This is not naming pedantry, and the cost shows up in the read models. A projection that lists customers whose legal name changed in the past year – for a reporting obligation, for duplicate detection, for a mailing – needs the marriages and not the typos. If both arrive as customer-name-changed, that projection is quietly wrong, and nobody will notice for months. We wrote about this pull toward generic verbs in Naming Events Beyond CRUD; the correction case is where it bites hardest, because the generic name is so tempting.
For this case, the standard advice works perfectly. Write the correcting event, let the projections apply it, move on.
A Cancellation Is Not a Correction¶
The second case, the reversal, is the one teams misfile most often. An order is canceled. A subscription is terminated and later reactivated. A payment is refunded because the customer sent it back.
Here the original event was true. The store said a payment arrived, and a payment did arrive. Then the world produced another fact, and the second fact does not retract the first – it follows it. The money came in on Tuesday and went back out on Friday, and both of those things really happened, in that order.
This is why the question "how do I correct this?" leads nowhere: there is nothing to correct. You write order-canceled or subscription-reactivated, exactly as you would write any other event, and you are done. This is the case compensating events are made for, and notice that they need no special status here: the compensation is simply the next domain event. We made the same point from the other direction in Soft Delete Is a Workaround: nothing is removed, so nothing needs restoring, and the reactivation sits in the history right after the cancellation, where it belongs.
The failure mode here is not technical, it is conceptual. Teams call these cases corrections, and then they build machinery for them – a correction framework, a reversal flag on the event, a special path through the projections. All of it is scaffolding around plain domain modeling. Worse, the scaffolding blurs the model: once cancellations travel through a correction mechanism, the domain no longer says clearly that canceling is a thing customers do.
There is a one-question test that separates a reversal from a typo, and it does not involve any technology. Would somebody in the business use the word "mistake"? A canceled order is not a mistake, it is a customer changing their mind. A misspelled name is a mistake. If the domain expert shrugs and says "yes, that happens all the time", you are not correcting anything.
When Nothing Happened At All¶
Then there is the third case, the phantom, and it is the reason this post exists.
A retry loop fires without a precondition and writes 40,000 payment-received events for payments that were already recorded. A migration script runs against the wrong configuration and creates customers who do not exist. A test fixture escapes into production. The store now asserts facts that have no counterpart in the world whatsoever. Not described inaccurately – not described at all, because there is nothing there to describe.
That is what makes this case different in kind rather than in degree. In the first two cases, you could point at something real when you corrected the record: he is actually called Meier, she actually canceled. With a phantom, the world has nothing to say. No agent mistyped anything. No customer changed their mind. Inside the business, the day passed without event.
And that is exactly why the vocabulary runs out. Domain events describe what happened in the business, so a domain event for "this never happened" is a contradiction. Yet the standard advice keeps insisting: write a compensating event. So somebody writes payment-refunded, forty thousand times.
Look at what that claims. It says money went back to the customer. No money went back, because no money came in. You have not corrected a false statement, you have added a second one to cover it – and a second lie does not average out with the first into truth. The store now contains twice as much fiction as before, and both halves look perfectly legitimate to anything reading them.
The bill arrives when someone asks questions. An auditor sees 40,000 payments and 40,000 refunds and wants to know what on earth happened here, and the actual answer – nothing, it was a bug – is nowhere in the data. In You Don't Need an Audit Log we argued that the events are the audit trail, which is precisely why this hurts: you have just written invented business activity into the only record you have.
The cheapest phantom is the one you never get. Most of them come from writes that should have been rejected and were not, and EventSourcingDB can reject them for you: isSubjectPristine refuses a write when a subject already has events, and isSubjectOnEventId refuses one when anything has been appended to that subject since you last read it. Both are evaluated inside the same transaction as the write, so a duplicated retry fails with a 409 instead of doubling your history. If you want the details, they are in the documentation on preconditions – a few lines in a request body that keep the events you would otherwise have to disown from ever existing.
What's Left When the Vocabulary Runs Out¶
Suppose the 40,000 events already exist. There is no elegant answer here, only a choice between two awkward ones, and pretending otherwise is how teams end up with the refunds.
The first option is an event that makes no claim about the world at all, only about the store: these event IDs came from a defect and are not facts. It claims nothing false, which is exactly its appeal, and it keeps the knowledge inside the store where the events are. The price is that every consumer has to understand it – a projection that does not know the type keeps counting revenue that was never earned.
The second option leaves the store untouched and fixes the readers: the affected IDs go into a filter, and the projections get rebuilt. Nothing fictional is ever written, which is its great virtue, and rebuilding a projection is routine work rather than an emergency – we walked through one in The Three-Cent Problem. The price is that the knowledge now lives in application code, so every new consumer built next year has to be told about a bug from last year, and eventually one of them will not be.
Which one fits depends on how many consumers there are and how long the defect stays relevant. A handful of projections you own, a bounded window of bad events, a rebuild you can run tonight – filtering is enough, and it keeps the store clean. Many consumers, some of them outside your team, or a defect whose window nobody can pin down – then the knowledge belongs in the store, because that is the only place everyone reads.
What is not on the menu is rewriting the store. It is tempting, it looks surgical, and with hash-chained events it does not stay hidden: the chain breaks at the point you edited, and the integrity check fails from there on. You would be trading a data problem for the loss of the one property you bought an event store for – the ability to demonstrate that the history is intact, which we described in Proving Without Revealing. There is also a subtler cost, and it is permanent. Rewrite once, and you can never again prove that you only did it once.
Sorting the Case Costs Less Than Solving It¶
Each of the three cases has a workable answer, and none of them is hard. What is expensive is filing the situation under the wrong case, because then you apply a correct solution to the wrong problem and the damage compounds quietly.
Treat a reversal as a typo, and you build correction machinery for ordinary business behavior. That one is merely wasteful. Treat a typo as a phantom, and you reach for technical retractions when the domain had a perfectly good word available, losing the useful information that a human being caught an error. Treat a phantom as a reversal, and you write fiction into your audit trail – that is the one that costs you the trust of the record itself.
The sorting takes two questions. Was there a fact in the world that this event was trying to describe? If yes, did the event describe it wrongly, or describe it correctly before the world moved on? If no – if nothing happened out there at all – you are looking at a phantom, and no amount of domain vocabulary will rescue you, because there is no domain occurrence to name.
Notice that both questions are about the business, not the database. You cannot answer them by reading the payload, which is why this is a modeling conversation and not a debugging session. The event that says 4,800 euros arrived looks identical whether the amount was mistyped, refunded a week later, or conjured by a retry loop.
Back to the Statement¶
So which one was it? If an agent fat-fingered the amount, write the correction and name it for what it was. If the money did arrive and did go back, there was never anything to fix – the refund is just the next fact in the story. And if no payment ever existed, then the worst thing you can do is invent a refund to balance it out.
In none of the three cases does the original event change. It stays exactly where it is, wrong, forever. What changes is what the system knows about it – and the fact that this knowledge gets appended rather than overwritten is the whole reason anyone can reconstruct, years later, that it was a mistake at all. A system that quietly fixed the number would have left you with a correct statement and no way to explain it.
Compensating events are not the villain here. They are the right tool for a real fact that reverses another real fact, and they will serve you well for as long as the world is what went wrong. They just cannot help you when the world was never involved – and knowing which of those two you are looking at is the entire skill.
If you are sitting on a phantom right now, with a store full of events you wish had never been written, we would like to hear about it. Write to us at hello@thenativeweb.io – working out which events are facts and which are artifacts is a conversation we have often, and it is usually a shorter conversation than teams expect.