Replicated event sourcing overview
이 콘텐츠는 아직 번역되지 않았습니다.
Standard
event sourcing has
one writer per persistenceId — a single actor instance
appends events; another instance (after failover) replays them.
This is fine for sharded entities where the framework
guarantees one-actor-per-key.
Replicated event sourcing removes that constraint. Multiple replicas of the same entity can be active at once — on different nodes, in different regions — and each persists independently. The framework uses vector clocks to detect concurrent edits + conflict resolvers to merge them.
When to use it
Section titled “When to use it”This is the niche persistence pattern. Most apps shouldn’t need it. Use cases:
- Multi-region active-active — same entity writable in EU + US. Network partition between regions doesn’t stop either side.
- Edge-style replication — entities replicate close to users, reconcile centrally.
- Cluster-spanning concurrent writers — same entity edited on multiple cluster nodes without singleton coordination.
For typical sharded-entity setups, ClusterSharding + PersistentActor gives exactly-one-writer per key automatically — simpler than this.
What you give up
Section titled “What you give up”Replicated event sourcing trades simplicity for availability:
| Single-writer ES | Replicated ES |
|---|---|
| Total event order per pid | Partial order — concurrent events can be unordered |
| State is a deterministic fold | State is a fold + conflict resolution |
| Commands are validated against latest state | Commands are validated against local replica’s view |
| Restart replays the log | Restart replays the log + reconciles concurrent branches |
The mental model is CRDT-like for events — multi-writer convergence by design.
A minimal example
Section titled “A minimal example”import { ReplicatedEventSourcedActor, vectorClock, type ConflictResolver,} from 'actor-ts';
type State = { value: number };type Event = { kind: 'set'; value: number };
const maxWinsResolver: ConflictResolver<Event> = { // When two replicas concurrently set different values, the higher wins: resolve(a, b) { return a.event.value >= b.event.value ? a.event : b.event; },};
class Counter extends ReplicatedEventSourcedActor<Command, Event, State> { readonly persistenceId = 'counter-42'; protected override resolver(): ConflictResolver<Event> { return maxWinsResolver; }
initialState() { return { value: 0 }; } onEvent(state: State, event: Event) { return { value: event.value }; } // ... onCommand etc.}The actor extends ReplicatedEventSourcedActor instead of
PersistentActor. Nothing is passed into the constructor: the base
class reads the Cluster off the ActorSystem (see
this.cluster).
Two additional things to specify:
resolver()— how to merge concurrent events (optional; defaults to last-writer-wins).- The journal must be shared across replicas (Cassandra, shared object-storage, etc.).
replicaId — this replica’s stable identifier, distinct from
persistenceId — defaults to this node’s cluster address. Override it
only when the id has to survive a re-address, e.g. a fixed region name:
override get replicaId(): string { return process.env.REPLICA_ID!; }Two replicas that ever share an id will dedupe each other’s events away, so the default is the safe one.
The pieces
Section titled “The pieces”| Component | Purpose |
|---|---|
| VectorClock | Tracks causality across replicas — detects concurrent writes. |
| ConflictResolver | Decides how to merge concurrent events into a single state. |
| Single-writer lease | Optional — gates writes via a lease for stronger consistency. |
| Replicated snapshots | Snapshots that include the vector clock for full recovery. |
Each gets its own deep-dive page.
When NOT to use it
Section titled “When NOT to use it”Comparison
Section titled “Comparison” Sharding + PersistentActor Replicated ESMultiple writers per entity? No (exactly one) YesConflict resolution needed? No YesCross-region active-active? Sharding favors one region YesOperational complexity? Low HighUse when Default You really need itWhere to next
Section titled “Where to next”- Vector clocks — how concurrency is detected.
- Conflict resolver — how concurrent events merge.
- Single-writer lease — optional stronger-consistency gate.
- Replicated snapshots — vector-clock-aware snapshots.
- PersistentActor — the simpler single-writer alternative.
- Sharding overview — the usual scale-out pattern.
