Event sourcing systems need a strategy for handling failed commands and invalid state changes. This post explains why compensation is preferred over distributed transactions when multiple storage systems or subsystems are involved. Rather than deleting events, the approach marks them as compensated to preserve history while allowing them to be filtered out. The implementation uses an operation runner pattern that tracks command state, sends compensation messages via a service bus on failure, and uses SQL partial indexes (filtering on compensatedAt IS NULL) for query performance. A concrete F# example shows how employee resignation events are persisted and compensated across SQL, Redis, and table storage.
Table of contents
Compensation vs TransactionsLocal transactions, global compensationCompensate an eventWhat if compensation fails?SummaryDeep DiveSort: