Table of Contents

PN-Counter (Positive-Negative Counter)

This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at pncounter.md, and llms.txt lists every page.

tree.PnCounter(key) -> PnCounterAccessor, merge mode LatticeMergeMode.PnCounter.

Semantics

A PN-Counter is an integer counter that any number of replicas can increment and decrement concurrently while still converging on the correct total.

The trick: each replica keeps its own private tally of how much it has added (P) and how much it has subtracted (N). A replica only ever advances its own entries, so there is no write-write conflict. Merging takes the per-replica maximum of every entry, and the scalar value is sum(P) - sum(N). Because each entry only grows, taking the max is safe, commutative, and idempotent - a re-delivered update changes nothing.

Use it for: like counts, inventory / stock levels, quota consumption, active connection counts, votes - any quantity edited from many places at once.

Behaviour

Concurrent PN-Counter updates meeting at their join. An order diagram shaped like a diamond. At the bottom every P and N entry is 0, so the value is 0. Cluster A adds 5, reaching P[A]=5 and N[A]=0, value 5. Cluster B, at the same time, adds 2 and subtracts 1, reaching P[B]=2 and N[B]=1, value 1. Neither state is above the other. Merging takes the larger value of every entry, so both paths rise to the same top state: P[A]=5, P[B]=2, N[A]=0, N[B]=1, value (5 + 2) - (0 + 1) = 6.

Both clusters hold P[A]=5, P[B]=2 and N[A]=0, N[B]=1: value (5 + 2) - (0 + 1) = 6.

A replica only ever grows its own entries and merge takes the per-entry maximum, so a re-delivered update changes nothing.

graph TD
    subgraph "Cluster A tally"
      A["P[A]=5, N[A]=0"]
    end
    subgraph "Cluster B tally"
      B["P[B]=2, N[B]=1"]
    end
    A -->|merge = per-replica max| M["P[A]=5 P[B]=2 N[A]=0 N[B]=1"]
    B -->|merge = per-replica max| M
    M --> V["value = (5 + 2) - (0 + 1) = 6"]

Example

// A "likes" counter incremented from two clusters at once.
var likes = tree.PnCounter("post:99:likes");

// Cluster A records 5 likes; cluster B records 2 and one un-like.
await likes.IncrementAsync("cluster-A", 5, cancellationToken);
await likes.IncrementAsync("cluster-B", 2, cancellationToken);
await likes.DecrementAsync("cluster-B", 1, cancellationToken);

// After both sides merge, the total is (5 + 2) - 1 = 6, regardless of the
// order the updates arrived in.
long total = await likes.ValueAsync(cancellationToken); // 6

See also: OR-Map to key many counters (e.g. per-user vote tallies) under one map, and the CRDT overview.