Conflict resolver
Esta página aún no está disponible en tu idioma.
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 interface
Section titled “The interface”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.
Two hard contracts
Section titled “Two hard contracts”resolve must be:
- Deterministic — the same
(a, b)pair yields the same result on every replica; noDate.now(), noMath.random(), no external reads. - Commutative —
resolve(a, b)must equalresolve(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.”
Built-in resolvers
Section titled “Built-in resolvers”LastWriterWinsResolver (default)
Section titled “LastWriterWinsResolver (default)”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.
CustomMergeResolver
Section titled “CustomMergeResolver”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.
Custom strategies
Section titled “Custom strategies”For anything else, implement ConflictResolver<Event> directly.
resolve returns a single event; the actor applies it via
onEvent.
Pick a winner (max / min)
Section titled “Pick a winner (max / min)”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.
When a merge is the wrong abstraction
Section titled “When a merge is the wrong abstraction”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.
Performance
Section titled “Performance”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.
Where to next
Section titled “Where to next”- Replicated event sourcing overview — the bigger picture.
- Vector clocks — how conflicts are detected.
- Single-writer lease — preventing conflicts vs resolving them.
- Snapshotting — snapshots that include the resolver’s converged state.
