Compensating events
A compensating event records, as a new fact, that an earlier event is being offset, such as a reversal entry in a ledger. Chronicle links the two by sequence number and never changes the original. For the concept and the rules the kernel applies, see Event compensation.
Declare a compensation event type
Section titled “Declare a compensation event type”Annotate the compensating event type with @CompensationFor, passing the
event type it compensates for. Both classes need @EventType:
import io.cratis.chronicle.events.CompensationForimport io.cratis.chronicle.events.EventType
@EventType("entry-posted")data class EntryPosted(val account: String, val amount: Long)
@EventType("entry-reversed")@CompensationFor(EntryPosted::class)data class EntryReversed(val reason: String)Chronicle stores the link on the event type’s schema, as its
compensationFor entry, so every client and the Workbench can discover
which compensations exist for an event type. A @CompensationFor whose
target is not an @EventType fails when the schema is built.
Compensate an event
Section titled “Compensate an event”Name the event you are compensating by its sequence number, instead of an event source. The compensation goes to the event source, event stream and subject of that event, so you do not pass them:
import io.cratis.chronicle.IEventStoreimport io.cratis.chronicle.eventSequences.AppendErrorimport io.cratis.chronicle.eventSequences.EventSequenceNumber
suspend fun reverseEntry(store: IEventStore, postedAt: Long) { val result = store.eventLog.compensate( EventSequenceNumber(postedAt), EntryReversed("Posted to the wrong account") )
if (!result.isSuccess) { result.errors.forEach { error -> when (error.message) { AppendError.COMPENSATED_EVENT_NOT_FOUND -> println("There is no event at $postedAt") AppendError.EVENT_TYPE_DOES_NOT_COMPENSATE_EVENT_TYPE -> println("EntryReversed does not compensate that event") } } }}A rejected compensation is reported in the result’s errors; it is not
thrown. Constraints, concurrency checks and compliance run for the
compensating event like any other append, and the same event can be
compensated more than once if your domain allows it.
Pass CompensateOptions for a correlationId, tags, occurred or a
causation chain. From Java, BlockingEventSequence.compensate does the
same without coroutines.
Read the link
Section titled “Read the link”The event context carries the compensated sequence number, for reactors, reducers and anything that reads events back:
import io.cratis.chronicle.events.EventContext
fun compensatedBy(context: EventContext): Long? = context.compensationForcompensationFor is null for an event that is not a compensation, and
isCompensation is the boolean form. The wire format cannot say “absent”, so
a value of 0 reads as “not a compensation”: a compensation of the event at
sequence number 0 is indistinguishable from none. Check the event type when
that matters.
Testing
Section titled “Testing”InMemoryEventSequence from the testing module supports compensate on the
kernel’s terms: it rejects a missing original and an event type that is not
declared for the original’s type, and appends to the original’s event source.