---
title: "PN-Counter (Positive-Negative Counter)"
url: "https://nsta1.github.io/Orleans.Lattice/docs/crdt/pncounter.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/docs/crdt/pncounter.md"
documents: "Orleans.Lattice 9.9.0 (release line 9.9)"
built: "2026-10-04"
all-pages: "https://nsta1.github.io/Orleans.Lattice/llms.txt"
bundle: "https://nsta1.github.io/Orleans.Lattice/docs/crdt/llms-full.txt"
---
# PN-Counter (Positive-Negative Counter)

Part of the [CRDTs documentation](readme.md).

`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

Figure: 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.

```mermaid
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

```csharp verify
// 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](ormap.md) to key many counters (e.g. per-user vote tallies)
under one map, and the [CRDT overview](readme.md).
