How does a PN-Counter support both increment and decrement using only grow-only counters internally, and why doesn't it work to just merge a single counter value using max even if deltas can be negative?
answer
- two G-Counters glued together
- P for increments, N for decrements
- value = sum(P) - sum(N)
- merge = max per slot on P and N separately
- single-scalar max can't represent 'went down'
basics
~20 sA PN-Counter is really two separate 'only goes up' counters glued together - one counts all the increments, one counts all the decrements - and the real value is increments minus decrements. You can't just keep one number and take the max across replicas, because max can't tell a smaller number from 'this replica went down on purpose' versus 'this replica hasn't caught up yet.'
solid answer
~40 sPN-Counter (Positive-Negative Counter) composes two G-Counters, P (increments) and N (decrements), each following the standard per-node-slot / max-merge rule. Incrementing bumps the caller's own slot in P; decrementing bumps the caller's own slot in N; the counter's value is sum(P) - sum(N). Merging a PN-Counter merges P and N independently with element-wise max, so all the G-Counter guarantees (idempotent, commutative, associative, no lost updates) carry over untouched. You can't achieve this with a single scalar merged by max because max is monotonic - it can only move a value up, so it structurally cannot represent 'this replica's value went down,' which is required for decrement to ever take effect.
go deeper
Should get that a decrementable counter needs two separate 'only goes up' counters under the hood, even if they can't yet explain precisely why a single max-merged number fails.
Should be able to state precisely why single-scalar max-merge can't represent decrement, and describe the value formula (sum(P) - sum(N)).
Should reason about metadata cost (double the vector) and know that PN-Counter offers no domain invariant enforcement (e.g. non-negativity) despite looking like an ordinary counter.
Should identify PN-Counter's applicability boundary in system design - fine for approximate/eventually-accurate gauges, wrong choice wherever a real-time invariant (non-negative stock, budget cap) must never be violated - and know a real product's counter CRDT offering.
## How a PN-Counter is built A **PN-Counter** (Positive-Negative Counter) is built by composing two independent G-Counters internally: `P`, which accumulates all increments, and `N`, which accumulates all decrements. Each is a standard grow-only counter — a vector with one slot per replica, where a replica only bumps its own slot, and merging two vectors takes the element-wise maximum per slot. - To increment the PN-Counter, a replica bumps its own slot in `P`; to decrement, it bumps its own slot in `N` (both operations only ever increase the relevant internal number). - The externally visible value of the counter is computed on read as **sum(all slots in `P`) minus sum(all slots in `N`)**. - Merging two PN-Counter replicas is simply merging `P` with `P` (max per slot) and `N` with `N` (max per slot) independently — no new merge logic is needed beyond what G-Counter already provides. ## Why a single scalar fails This design exists because a CRDT's merge function must be well-defined purely from the states being merged, with no access to shared history or arrival order. A single scalar that both increments and decrements apply directly to cannot be merged this way: if replica A's counter shows 7 and replica B's shows 5, there is no way to tell, from the numbers alone, whether - B did a decrement A hasn't seen yet (in which case merge should reflect the decrement), or - B simply hasn't caught up to some of A's increments yet (in which case merge should keep moving toward 7). Splitting into "all my increments, ever" and "all my decrements, ever" resolves the ambiguity: both P and N are monotonically growing, so each independently supports the same unambiguous max-merge rule a G-Counter uses, and subtracting the two sums at read time reconstructs the true net value without ever needing a decreasing merge. ## What it costs The cost of this design is roughly **double the memory** of a G-Counter (two vectors instead of one) and **read-time computation** (two sums and a subtraction), which is negligible for small replica counts but adds up if the counter is read at very high frequency across many replicas with large vectors. A more interesting trade-off is what's not preserved: PN-Counter tells you the net current value, but it does not preserve the intermediate history of increments/decrements beyond the granularity your own snapshotting provides — it collapses history into two running totals, same as any counter. ## The mistakes 1. The classic mistake is trying to shortcut the design by keeping a single scalar per replica and merging with max even though decrements are allowed — as reasoned above, this silently breaks: a decrement looks exactly like "stale, hasn't caught up," so the merge will happily discard it and the counter can never actually go down once merged with a replica that saw a higher pre-decrement value. 2. Another subtler mistake is letting the exposed "value" be cached rather than recomputed as `sum(P)-sum(N)` on every merge — if the cached value isn't recomputed after every merge, the exposed value can lag behind the true post-merge state once concurrent increments and decrements from multiple replicas are involved. ## Where it ships PN-Counters are the standard "net delta" counter type in systems offering native CRDT support: - **Riak KV** ships a PN-Counter type directly usable from application code for cases like tracking a "net votes" or "inventory adjustment" figure across geographically distributed replicas that need to accept writes locally without a coordinator. - **Akka's Distributed Data** module provides a `PNCounter` CRDT for cluster-wide gauges (e.g. "number of active sessions" across nodes) where losing real-time exactness for always-available local writes is an acceptable trade.
- Could a PN-Counter go negative?Yes, structurally nothing prevents it - if decrements outnumber increments, sum(N) exceeds sum(P) and the computed value is negative; if the application domain requires a non-negative invariant (like inventory can't go below zero), PN-Counter alone can't enforce that, since it has no way to reject a decrement based on the current merged value at write time.
- How would you support 'increment by 5' or 'decrement by 3' rather than unit steps?Each slot in P or N accumulates the total amount that replica has contributed rather than a count of operations - a replica incrementing by 5 adds 5 to its own P slot in one step; the merge rule (max per slot) and read rule (sum P minus sum N) are unchanged.
- Does PN-Counter need to know the full set of replicas in advance?No - a new replica simply starts with an implicit zero for its slot in both P and N, and its slot appears in other replicas' vectors the first time a merge observes a nonzero contribution from it; the vector effectively grows to cover whichever replicas have actually written.
Like tracking a checking account not as one running balance but as two separate grow-only ledgers - a deposits ledger and a withdrawals ledger - and computing the balance as deposits-so-far minus withdrawals-so-far; each ledger only ever gets longer, so combining two branches' copies of either ledger is as simple as keeping every line either branch has recorded.
saying these in an interview costs you the question
- Thinks a single scalar merged with max can support decrement
- Doesn't know PN-Counter is built from two G-Counters
- Believes PN-Counter enforces non-negativity
- Confuses PN-Counter's 'P' and 'N' with positive/negative numbers stored directly rather than separate monotonic counters