Pular para o conteúdo
Português (BR)

Conflict resolver

Este conteúdo não está disponível em sua língua ainda.

When two replicas of the same entity write events concurrently (detected via vector clocks), the framework needs to know which event wins, or how to merge the pair. That’s the conflict resolver — a strategy you supply by overriding resolver() on the actor.

import { ReplicatedEventSourcedActor, LastWriterWinsResolver } from 'actor-ts';
import type { ConflictResolver } from 'actor-ts';
class Counter extends ReplicatedEventSourcedActor<Command, Event, State> {
// The default is last-writer-wins; override to change it:
protected override resolver(): ConflictResolver<Event> {
return new LastWriterWinsResolver<Event>();
}
// ...
}

The resolver reconciles a concurrent pair — it returns one event (a winner, or a synthesised merge of the two):

interface ConflictResolver<E> {
resolve(a: ConflictCandidate<E>, b: ConflictCandidate<E>): E;
}
type ConflictCandidate<E> = {
event: E; // the user-domain event payload
timestamp: number; // wall-clock at the originating replica
replica: ReplicaId; // originating replica id
vc: VectorClock; // vector clock at persist time
};

Note the single type parameter (the event, not the state). When more than two events are concurrent, the framework folds them pairwise through resolve, so the whole set reduces to one winning event — which is then applied through your normal onEvent.

resolve must be:

  • Deterministic — the same (a, b) pair yields the same result on every replica; no Date.now(), no Math.random(), no external reads.
  • Commutativeresolve(a, b) must equal resolve(b, a). Events arrive in different orders on different replicas, so a non-commutative resolver makes replicas diverge.

Decide the winner from the candidates’ own data (timestamp, replica, payload) — never from arrival order or “prefer a.”

protected override resolver(): ConflictResolver<Event> {
return new LastWriterWinsResolver<Event>();
}

Higher timestamp wins; on a tie the higher (lexicographic) replica id wins, so every replica converges. Simple and often “good enough.” Caveat: relies on roughly comparable wall clocks across replicas — the same trade-off as LWWRegister.

Wrap a commutative merge of the two event payloads — use it when you have domain knowledge LWW doesn’t capture (e.g. two concurrent deposits simply add):

import { CustomMergeResolver } from 'actor-ts';
protected override resolver(): ConflictResolver<Event> {
return new CustomMergeResolver<Event>((a, b) => ({
kind: 'deposited',
amount: a.amount + b.amount, // additive merge
}));
}

CustomMergeResolver sorts the two candidates by replica id before calling your merge, so it receives a deterministic argument order even if your function isn’t perfectly symmetric — but the contract still says be commutative.

For anything else, implement ConflictResolver<Event> directly. resolve returns a single event; the actor applies it via onEvent.

class HighestWins implements ConflictResolver<Event> {
resolve(a: ConflictCandidate<Event>, b: ConflictCandidate<Event>): Event {
if (a.event.value !== b.event.value) {
return a.event.value > b.event.value ? a.event : b.event;
}
return a.replica > b.replica ? a.event : b.event; // deterministic tie-break
}
}

Right for values that must not regress: stock levels, high scores. Break exact ties on replica so the result is the same on every replica.

Deterministic order by (timestamp, replica)

Section titled “Deterministic order by (timestamp, replica)”
class OrderedWins implements ConflictResolver<Event> {
resolve(a: ConflictCandidate<Event>, b: ConflictCandidate<Event>): Event {
if (a.timestamp !== b.timestamp) return a.timestamp > b.timestamp ? a.event : b.event;
return a.replica > b.replica ? a.event : b.event;
}
}

This is exactly what LastWriterWinsResolver does — shown as the template for your own tie-breaking.

Sometimes concurrent events shouldn’t merge — they conflict semantically (e.g. “close account” concurrent with “deposit”). For those, prefer single-writer with a lease — see Single-writer lease — which forces sequential writes so no conflicts arise.

Use replicated ES when concurrent events merge naturally; use the lease when they shouldn’t happen at all.

resolve runs only on concurrent-write detection — bounded by the rate of cross-replica events, typically rare. Even a heavier merge is cheap next to the journal I/O.