Architecture
Each node in the cluster runs its own Replicator system actor (GoAktReplicator). There is no central coordinator β each Replicator owns its nodeβs local CRDT store and communicates with peer Replicators via GoAktβs existing TopicActor pub/sub system.

- A user actor sends an
Updatemessage to its local Replicator viaTellorAsk. - The Replicator applies the mutation locally, extracts the delta, and publishes it to the shared
goakt.crdt.deltastopic. - The TopicActor delivers the delta to all peer Replicators (existing cluster pub/sub β no custom gossip).
- Each peer Replicator merges the delta into its local store and notifies any local watchers.

CRDT types
All types implement theReplicatedData interface and are immutable values β every mutation returns a new value plus a delta.
| Type | Description | Primary use cases |
|---|---|---|
GCounter | Grow-only counter. Each node maintains its own increment slot; value = sum of all slots. | Monotonic metrics, event counts |
PNCounter | Positive-negative counter. Two GCounters: one for increments, one for decrements. | Rate limiters, gauges, inventory |
LWWRegister[T] | Last-writer-wins register. Stores a single value with a timestamp. | Configuration, feature flags |
ORSet[T] | Observed-remove set. Supports add and remove without conflict (add-wins semantics). | Session tracking, subscriptions |
ORMap[K, V] | Map where keys are an OR-Set and values are themselves CRDTs. | Shopping carts, user profiles |
Flag | Boolean that can only transition from false to true. | One-time signals, feature activation |
MVRegister[T] | Multi-value register. Concurrent writes are preserved, not overwritten. | Conflict-visible state |
Typed keys
CRDT keys are typed and serializable. The type parameter ensures compile-time safety β no type assertions on read.Enabling CRDTs
CRDT replication is enabled throughClusterConfig.WithCRDT(...). When not enabled, no Replicator is spawned β zero overhead.
Configuration options
| Option | Default | Description |
|---|---|---|
WithAntiEntropyInterval(d) | 30s | Interval between anti-entropy digest exchanges with a random peer |
WithMaxDeltaSize(n) | 64KB | Maximum serialized delta size per publish; larger deltas trigger full-state transfer |
WithPruneInterval(d) | 5m | Interval for pruning expired tombstones and compacting CRDTs |
WithTombstoneTTL(d) | 24h | How long deletion tombstones are retained to prevent key resurrection |
WithCoordinationTimeout(d) | 5s | Timeout for coordinated WriteTo/ReadFrom operations |
WithRole(r) | (all nodes) | Restrict CRDT replication to nodes with this role |
WithSnapshotInterval(d) | (disabled) | Interval for periodic BoltDB snapshots (requires WithSnapshotDir) |
WithSnapshotDir(dir) | (disabled) | Directory for snapshot files |
Using the Replicator
The Replicator PID is accessed viaActorSystem.Replicator(). It returns nil if CRDTs are not enabled. All interaction uses standard Tell and Ask from within an actorβs Receive handler.
Writing data
Send anUpdate[T] to the Replicator. The Modify function receives the current typed value and returns the updated value.
| Field | Purpose |
|---|---|
Key | Typed key identifying the CRDT |
Initial | Zero value used if the key does not yet exist |
Modify | Function that receives the current value and returns the updated value |
WriteTo | (optional) Coordination level: crdt.Majority or crdt.All |
Reading data
Send aGet[T] to the Replicator. The response is typed β no type assertion on the CRDT data itself.
| Field | Purpose |
|---|---|
Key | Typed key to read |
ReadFrom | (optional) Coordination level: crdt.Majority or crdt.All |
Subscribing to changes
Actors can watch keys and receiveChanged[T] notifications whenever the value changes β from local updates or peer deltas.
Deleting data
Consistency model
Default: local-first
Every operation is local-first by default:- Writes apply the mutation locally and publish the delta asynchronously. The Replicator returns immediately.
- Reads return the local value immediately.
Optional coordination
When stronger guarantees are needed, setWriteTo or ReadFrom on the message:
| Level | WriteTo behavior | ReadFrom behavior |
|---|---|---|
| (not set) | Apply locally + publish via TopicActor | Return local value |
Majority | Apply locally + send delta to N/2+1 peers + wait for acks | Query N/2+1 peers, merge results with local value |
All | Apply locally + send delta to all peers + wait for acks | Query all peers, merge results with local value |
ReadFrom: Majority and WriteTo: Majority are used together, the system provides strong eventual consistency with read-your-writes guarantees.
Durable snapshots
By default, the CRDT store is purely in-memory. If the Replicator crashes, state is rebuilt from peers via anti-entropy. For faster recovery, enable durable snapshots to persist the store to BoltDB periodically.- The Replicator saves the full store to BoltDB at the configured interval.
- On startup (or supervisor restart), the Replicator restores from the latest snapshot before participating in anti-entropy.
- A final snapshot is persisted during graceful shutdown. The snapshot file is retained on disk so the Replicator can restore quickly on the next startup.
Observability
WhenWithMetrics() is enabled on the actor system, the Replicator registers OpenTelemetry instruments:
| Instrument | Type | Description |
|---|---|---|
crdt.replicator.store.size | Int64ObservableGauge | Number of CRDT keys in the local store |
crdt.replicator.merge.count | Int64ObservableCounter | Total merges performed from peer deltas |
crdt.replicator.delta.publish.count | Int64ObservableCounter | Deltas published via TopicActor |
crdt.replicator.delta.receive.count | Int64ObservableCounter | Deltas received from peers |
crdt.replicator.coordinated.write.count | Int64ObservableCounter | Coordinated write operations |
crdt.replicator.coordinated.read.count | Int64ObservableCounter | Coordinated read operations |
crdt.replicator.antientropy.count | Int64ObservableCounter | Anti-entropy rounds completed |
crdt.replicator.tombstone.count | Int64ObservableGauge | Active tombstones pending pruning |
actor.system attribute with the actor system name.
Full example
A complete working example: aSessionTracker actor that uses an ORSet to maintain a cluster-wide set of active sessions, and a SessionReporter that watches for changes.
Limitations
- Memory-bound β All CRDT state is held in-memory. Memory usage grows linearly with the number of keys and the size of each value. ORSets and ORMaps carry metadata (causal dots) that grows with unique add operations; periodic compaction prunes redundant dots.
- Single-topic dissemination β All deltas for all keys flow through one TopicActor topic (
goakt.crdt.deltas). This simplifies the protocol but means a high update rate on many keys produces proportional TopicActor throughput. For most workloads this is not a bottleneck. - No cross-datacenter replication β CRDTs replicate within a single cluster. Cross-datacenter delta exchange via the DataCenter control plane is planned for a future release.
- Tombstone window β Deleted keys are protected by a tombstone for the configured TTL. If a node is partitioned for longer than the TTL and still holds the key, it may resurrect the key when it rejoins. Set
WithTombstoneTTLappropriately for your partition tolerance requirements. - Generic type constraints β
ORSet[T]requiresTto becomparable.LWWRegister[T],MVRegister[T], andORMap[K, V]require values to be serializable by the codec layer. The built-in codec currently supportsGCounter,PNCounter,Flag,LWWRegister[string],MVRegister[string],ORSet[string], andORMap[string, *GCounter]. - No partial updates β The
Modifyfunction inUpdate[T]receives the full current value and must return the full updated value. There is no patch or field-level delta mechanism.