Skip to content

Distributed Counters

This cookbook shows how to use a CRDT collection for cluster-wide counters and convergent aggregate state.

When This Pattern Fits

Use this pattern when:

  • many nodes should update locally
  • you do not want a single writer bottleneck
  • the state should converge through merge semantics
  • the business question is aggregate state, not current owner

Typical examples:

  • request counters
  • low-volume distributed usage totals
  • convergent worker-visible aggregates
  • collection type: CRDT
  • locator example: shared/worker/request-counter
  • update type: immutable DsmEntity<E> record
  • state type: MergeableState<S>
  • persistence default: local-durable

Mental Model

text
node-a local update: +3
node-b local update: +5

    +3 -------------------+
                           \
                            +--> merge --> visible cluster state = 8
                           /
    +5 -------------------+

Flow

text
apply(update)
    |
    +--> local state merger runs
    |
    +--> version vector advances
    |
    +--> sync layer replicates delta
    |
    +--> peers merge toward same state

Registration Pattern

java
DsmCrdtCollection<PnCounterUpdate, PnCounterState> requestCounter = runtime.crdt(
    CrdtCollectionSpecBuilder.<PnCounterUpdate, PnCounterState>crdt(
            "shared",
            "worker",
            "request-counter")
        .schemaId("request-counter/v1")
        .codec(new PnCounterUpdateCodec())
        .stateCodec(new PnCounterStateCodec())
        .initialState(PnCounterState.empty())
        .build(),
    new PnCounterStateMerger());

Update Pattern

java
requestCounter.apply(PnCounterUpdate.increment("orders", 1));
requestCounter.apply(PnCounterUpdate.decrement("orders", 1));

Read Pattern

java
PnCounterState state = requestCounter.state();
VersionVector version = requestCounter.versionVector();
StateDelta<PnCounterState> delta = requestCounter.deltaSince(version);

Design Rules

  • use a CRDT only when local multi-writer updates are a real requirement
  • keep the update entity focused on one mergeable action
  • choose a schema ID that reflects the state and update compatibility boundary
  • expect eventual convergence, not single-writer overwrite semantics

Common Mistakes

Using A CRDT For Simple Latest-Value Metadata

If one latest route or flag value is enough, a register is simpler.

Expecting Ownership Semantics

CRDTs merge. They do not answer "who is the owner right now".

Forgetting Version Vectors In Repair Reasoning

The version vector is part of how peers understand what delta is needed. It is not just internal bookkeeping.

Spring Boot Shape

yaml
dsm:
  collections:
    - bean-name: requestCounterCollection
      tenant-id: shared
      application-id: worker
      collection-id: request-counter
      schema-id: request-counter/v1
      type: CRDT
      consistency-tier: CRDT
      codec-bean: requestCounterCodec
      crdt:
        state-codec-bean: requestCounterStateCodec
        initial-state-bean: requestCounterInitialState
        merger-bean: requestCounterMerger