# Reference patch — 03-denials-are-counted

The answer key for how the reference absorbs this change, and the thing that makes `budget.json`
reproducible. **Never shipped to the candidate.**

1. **Replace** `reference/src/RateLimiter.java` with the copy in this directory. Four changes, all of
   them inside that one file:
   - one added import, `java.util.concurrent.atomic.AtomicLong`;
   - one added field, `ConcurrentMap<ClientKey, AtomicLong> denials`, with a three-line javadoc;
   - four added lines and one changed line in `tryAcquire`, to hold the decision, increment on a
     denial, and return it;
   - one added method, `denialsFor(ClientKey)`, six lines.
2. **Add** `contract-delta/RateLimiterApi.java` (the full revised interface, with `denialsFor` and
   convention (10) beside the nine that were already there, and "metrics" struck from the
   out-of-scope paragraph) to the candidate's contract. Not counted in the measured diff.

`KeyBudget.java`, both algorithms, `StandardAlgorithms.java`, `AlgorithmFactory.java`, `Scopes.java`
and `Availability.java` are **untouched** — the patch directory contains exactly one file, and it is
not byte-identical to its baseline.

Measured: `reference_diff = 19` (18 added, 1 removed, one file touched, no new files), so
`diff_budget = max(3, 19 + min(ceil(19/2), 6)) = 19 + 6 = 25`.

Both the curveball suite (6 tests) and the base suite (28 tests) are green after the patch, across
three consecutive runs of each.

## Why this one is not near zero, and why that is the right answer rather than a seam failure

The base contract lists "metrics and logging" as out of scope, in writing, and the reference took that
at its word. There is no observer, no listener, and no place a count could go. So this curveball is
the corpus's **third case**, distinct from both of its siblings: not a new value inside an existing
dimension (curveball 01, one line) and not a second implementation of a seam that already existed
(curveball 02, zero lines), but **a requirement the design deliberately did not build for**. Nineteen
lines is what it costs, and pre-building an observer seam against an explicitly out-of-scope
requirement would have been speculative generality — the exact fault `Scopes`' javadoc goes out of its
way to justify not committing twice.

Being able to say that out loud is the point. A candidate who absorbs this in twenty lines has done
well; one who absorbs it in three because he had already built a `MetricsListener` nobody asked for
has not.

## The three decisions in nineteen lines, which is what Phase 5 should ask about

**Where the count lives: its own map, not a field in `KeyBudget`.** A denial is a property of one
*request*, and a request may have consulted several budgets — under curveball 02's backstop scope it
consults two. "How many times was this key refused" is not answerable by adding up per-budget numbers
without double counting, and a count inside `KeyBudget` would also be discarded on every `configure`,
because reconfiguring a key throws the whole budget away. That last point is not a detail: the
requirement says explicitly that `configure` must **not** clear the count, and putting the counter in
the object `configure` replaces would break it silently.

**Where the increment goes: once per request, outside the lock.** `tryAcquire` holds the `Decision`
that came back from `locked(...)` and increments if it was a denial — one increment per request, after
the lock has been released. Two things fall out of that placement, and both are what the tests check:

- A candidate who increments inside `decide`'s per-rule loop, or inside `KeyBudget.objection`, counts a
  request refused by three rules as three denials. `aRequestRefusedByTwoRulesIsOneDenial` is built
  specifically to fail that — two identical rules, so one request past the first is refused by both.
- Incrementing outside the lock means the lock is not held for bookkeeping nobody waits on. It also
  means the increment gets no protection from that lock, which is why it needs its own atomicity.

**Why `ConcurrentHashMap<ClientKey, AtomicLong>` and not a lock.** This is the one place in this
problem where a concurrent collection plus an atomic **is** the right answer, and it is instructive
precisely because `RateLimiter`'s existing javadoc argues the opposite for the counters. The
difference is the size of the unit that has to be atomic: for the rate-limit counters it spans several
reads and several writes across several objects, so no atomic can express it; for a denial count it is
one increment of one number, which is exactly what `AtomicLong` is. A candidate who can state that
difference — same problem, two different answers, for a stated reason — is demonstrating E2 and E4
rather than reciting them.

## Proven against a naive counter

The concurrency scenario is not decorative. `everyDenialInACrowdIsCounted` (200 threads, 25 units
available, 175 denials, five rounds) was run against a variant of this patch with the counter changed
to a plain `HashMap<ClientKey, Long>` updated by `put(key, getOrDefault(key, 0L) + 1)`:
**caught 5 times out of 5**, and it was the only one of the six tests to fail each time — the other
five passed, so the suite points at the counter rather than at the limiter. Verbatim: *"the denial
count is a second read-modify-write on shared state, and it must be exact: every one of the 175
refusals is counted once ==> expected: <175> but was: <…>"*.
