# NOTES — seven files, twelve refusals, and one bug nobody wrote a test for

Compile and run from the directory holding the sources:

```
..\..\..\.toolchain\jdk-21\bin\javac.exe -d out *.java
..\..\..\.toolchain\jdk-21\bin\java.exe -cp out Main
```

`javac -Xlint:all` exits 0 and prints nothing. Real output, from exactly this code:

```
== What a valid budget looks like ==
rules                      [hourly, burst]
remaining after 3 requests 2
refused after 5 requests   [burst]
allowed(2)                 Decision[allowed=true, remaining=2, deniedBy=, retryAfter=PT0S]
denied("burst", 200ms)     Decision[allowed=false, remaining=0, deniedBy=burst, retryAfter=PT0.2S]

== One field at a time ==
ClientKey.of("   ")                           java.lang.IllegalArgumentException
                                                a client key cannot be blank
Rule.fixedWindow("hourly", 0, 1h)             java.lang.IllegalArgumentException
                                                rule hourly: limit must be at least 1, got 0
Rule.fixedWindow("hourly", 100, PT0S)         java.lang.IllegalArgumentException
                                                rule hourly: window must be positive, got PT0S
Rule.tokenBucket("firehose", 10_000_000_000, 1h) java.lang.IllegalArgumentException
                                                rule firehose: limit 10000000000 over PT1H is too large to count exactly in nanoseconds

== Four fields that have to agree ==
Decision.allowed(-3)                          java.lang.IllegalArgumentException
                                                remaining cannot be negative, got -3
new Decision(true, 97, "burst", PT0S)         java.lang.IllegalArgumentException
                                                an allowed request was not denied by burst
new Decision(true, 97, "", PT2S)              java.lang.IllegalArgumentException
                                                an allowed request has nothing to wait for, got PT2S
Decision.denied("", PT2S)                     java.lang.IllegalArgumentException
                                                a denial must name the rule that refused
Decision.denied("burst", PT0S)                java.lang.IllegalArgumentException
                                                a denial must say when to come back, and it must be later than now, got PT0S
new Decision(false, 97, "burst", PT2S)        java.lang.IllegalArgumentException
                                                a denied request has nothing remaining, got 97

== An invariant that spans a collection ==
Budget.of(acme, [hourly, hourly])             java.lang.IllegalArgumentException
                                                client acme has two rules called hourly; a denial naming that rule would not say which one refused
Budget.of(acme, [hourly, null])               java.lang.NullPointerException
                                                rule at index 1 for client acme
budget.rules().add(hourly)                    java.lang.UnsupportedOperationException
                                                (no message)

== Closed type versus checked value ==
Algorithm.valueOf("FIXED_WINDO")              java.lang.IllegalArgumentException
                                                No enum constant Algorithm.FIXED_WINDO
RuleByString("hourly", "fixed_windo", ...)    java.lang.IllegalArgumentException
                                                rule hourly: unknown algorithm "fixed_windo", expected one of [FIXED_WINDOW, TOKEN_BUCKET]
RuleByString("hourly", "fixed_window", ...)   java.lang.IllegalArgumentException
                                                rule hourly: unknown algorithm "fixed_window", expected one of [FIXED_WINDOW, TOKEN_BUCKET]
Rule("hourly", Algorithm.FIXED_WINDOW, ...)  built, and no typo was reachable to check for

== A concurrency bug caught by a value type ==
limit 50, 256 threads, no lock across check-then-charge
allowed more requests than the limit permits: true
torn counts refused by Decision's constructor: true
a message from this run: remaining cannot be negative, got -64
```

Two lines in there are the lesson. `remaining cannot be negative, got -3`, which names the field
and the value. And the last one, which is the same message arriving from a code path no test in
this lesson covers.

The negative number in that last line differs on every run. That it is negative does not.

---

## `Decision.java` — four fields, one consistent answer

```java
public record Decision(boolean allowed, long remaining, String deniedBy, Duration retryAfter) {
```

**A record, so every component is `final` and the guarantee travels with the object.** J4 covered
the mechanics: `final` fields, one assignment, `javac` proving it. The design consequence is what
matters here. A `Decision` handed to a logger, a header writer and a metrics counter is the same
`Decision` in all three, so a check that ran once covers all three forever. Make one component
mutable and the check covers only the instant it ran in.

```java
    public Decision {
        Objects.requireNonNull(deniedBy, "deniedBy");
```

**The compact constructor, which runs before the fields are assigned.** No caller can reach a
`Decision` that skipped it, including a caller written next year who never read this file. That is
the whole claim of A5. It is also the difference from a `validate()` method, which has to be called,
and no compiler will tell you it was not.

`requireNonNull` returns its argument, so a check and an assignment can be one statement. Here it is
the check alone, because a record's compact constructor assigns the fields for you.

```java
        if (remaining < 0) {
            throw new IllegalArgumentException("remaining cannot be negative, got " + remaining);
        }
```

**The message is part of the invariant, not decoration on top of it.** `IllegalArgumentException:
-3` names neither the field nor the type. This message names both the field and the value, so the
person reading it at 1am has the whole fact in one line. `contrast/` measures what the alternative
costs.

**Why refuse rather than clamp.** `remaining = Math.max(0L, remaining)` also compiles, also stops
the negative escaping, and reads as the considerate thing to do. It destroys the only evidence that
a counter was charged twice for one request. The torn count becomes an ordinary "you have nothing
left" denial and nobody ever finds out. `when-not.md` has the measurement.

```java
        if (allowed) {
            if (!deniedBy.isEmpty()) {
                throw new IllegalArgumentException("an allowed request was not denied by " + deniedBy);
            }
```

**Cross-field, which is the kind of invariant a field-by-field check cannot express.** Each of the
four values is legal on its own. `deniedBy = "burst"` is a fine string and `allowed = true` is a
fine boolean, and together they are nonsense: something both passed and was refused. A `validate()`
per field would pass this. Only a check that sees all four at once catches it.

The bug it prevents is a caller reading `deniedBy` to build an alert. With this combination
representable, an allowed request raises a "burst limit exceeded" page.

```java
            if (!retryAfter.isZero()) {
                throw new IllegalArgumentException(
                        "an allowed request has nothing to wait for, got " + retryAfter);
            }
```

**This one has a downstream cost with a name.** A client that honours `Retry-After` sleeps for it.
An allowed decision carrying two seconds means every successful request pauses. Throughput drops and
no error is logged anywhere.

```java
            if (retryAfter.isZero() || retryAfter.isNegative()) {
```

**Both halves, and `isZero()` is the one that gets dropped.** A denial with `Duration.ZERO` tells the
caller to retry immediately, which produces a hot loop against a limiter that is already refusing.
Writing `isNegative()` alone compiles and passes any test that only checks negatives.
`faded/GapTest.java` asserts the zero case for this reason.

```java
            if (remaining != 0) {
                throw new IllegalArgumentException(
                        "a denied request has nothing remaining, got " + remaining);
            }
```

**A policy decision wearing an invariant's clothes, and worth knowing that about it.** "A rule
denies precisely when it has nothing left" is this problem's convention, quoted from
`corpus/rate-limiter/contract/Decision.java`. It is true of this design and it is not a law of
nature. `when-not.md` measures what happens when a requirement asks a denial to report a non-zero
figure, because that is where a constructor-placed rule bites back.

```java
    public static Decision allowed(long remaining) {
        return new Decision(true, remaining, "", Duration.ZERO);
    }
```

**Two factories, and between them they make three of the four fields unspecifiable.** A caller
cannot pass `deniedBy` to `allowed` or a non-zero `remaining` to `denied`, so most of the invariant
is enforced by the shape of the call rather than by a check inside it. The compact constructor stays
anyway, because `new Decision(...)` is still public on a record and the concurrency path in `Main`
goes through `allowed`.

---

## `Rule.java` — four per-field invariants, and one that looks like a detail

```java
        Objects.requireNonNull(algorithm, "algorithm");
```

**One line, and no membership check anywhere.** There is no value of type `Algorithm` that is not
`FIXED_WINDOW` or `TOKEN_BUCKET`, so the set was closed before this constructor ran. Compare
`RuleByString` below, which needs four lines and a second copy of the constant list to reach a
weaker version of the same guarantee.

```java
        if (limit < 1) {
            throw new IllegalArgumentException("rule " + name + ": limit must be at least 1, got " + limit);
        }
```

**Every message in this class starts with the rule name.** A limiter holds several rules per caller.
`limit must be at least 1, got 0` would leave you grepping a config file. `rule hourly: ...` does
not.

**A limit of zero is a configuration mistake, not a rule that denies everything.** That reading is
stated in the corpus contract, and it is the sort of thing an interviewer asks about. "Deny
everything" is a different feature and belongs somewhere a reader can see it.

```java
        try {
            Math.multiplyExact(limit, window.toNanos());
        } catch (ArithmeticException overflow) {
```

**The invariant that looks like an implementation detail and is not.** A token bucket carries
fractional tokens by holding its balance in token-nanoseconds, so one token is `windowNanos` units.
That representation is exact only while `limit × window` fits in a `long`. Past that bound the
arithmetic drifts, and an allowance that erodes over a week is a bug nobody can reproduce.

Refusing the rule at construction converts an unreproducible drift into a startup failure with a
message. `Math.multiplyExact` is doing the work: it throws on overflow, where `limit * nanos` would
wrap silently. Writing the check against `window.toSeconds()` instead compiles and lets the
overflowing rule through, which is one of the wrong answers `faded/GapTest.java` catches.

---

## `ClientKey.java` — the one-invariant case, and why it is still a type

```java
public record ClientKey(String value) {
    public ClientKey {
        Objects.requireNonNull(value, "value");
        if (value.isBlank()) {
```

**`isBlank()`, not `isEmpty()`.** A key of `"   "` is a caller nobody can identify, and it reaches
a map as a perfectly good key. `isEmpty()` compiles, passes a test written with `""`, and lets the
whitespace key through.

**The invariant here is thin and the type still pays.** Everything in a rate limiter is keyed by
this value, and a rule name is also a `String`. A method taking two bare strings is a call the
compiler cannot check, and swapping the arguments produces a limiter that works and limits the wrong
thing. `lru-cache` makes the opposite trade honestly: its `problem.json` calls A5 *"the honest
weakest tag on this problem"*, because one positive-capacity check is all its contract gave it a
place to put. One invariant does not earn ceremony. This one earns a `record` and eight lines.

---

## `Algorithm.java` and `RuleByString.java` — placement by type, not by `if`

Both files carry the same fact: which of two counting conventions a rule uses. One of them cannot be
got wrong.

Write the typo against the enum and the build stops:

```
> javac -d out *.java
Typo.java:5: error: cannot find symbol
        return new Rule("hourly", Algorithm.FIXED_WINDO, 100L, Duration.ofHours(1));
                                           ^
  symbol:   variable FIXED_WINDO
  location: class Algorithm
1 error
```

Write the same typo against the string and it compiles, because `"fixed_windo"` is a valid
`String`. The best that placement can then do is refuse at runtime:

```
rule hourly: unknown algorithm "fixed_windo", expected one of [FIXED_WINDOW, TOKEN_BUCKET]
```

That is a good message and it arrives late. Three further costs come with it:

```java
    private static final List<String> KNOWN = List.of("FIXED_WINDOW", "TOKEN_BUCKET");
```

**`KNOWN` is a second copy of the constant set, kept in step by hand.** Add a sliding-window
algorithm and forget this line, and the new rule is refused by a check that was meant to help.
Forget the check entirely and the rule is accepted and never counted.

**Matching is a question the enum never posed.** `"fixed_window"` in lower case is refused, as the
run above shows. Whether that is right is now a decision somebody has to make, defend and test. The
enum has no such case to consider.

**There is nothing to switch on exhaustively.** `switch` over `Algorithm` with no `default` stops
compiling when a third constant arrives, at the place a decision is owed. A `switch` over strings
compiles forever and falls through.

This is what "invariant placement" means beyond constructors. Some invariants are placed in a type,
where the compiler holds them for nothing.

The same trade appears wherever the corpus has a closed set.
`corpus/parking-lot/contract/VehicleType.java` is three constants on one line, and `MOTORBKE` does
not compile. `corpus/vending-machine/contract/Coin.java` says it outright in its javadoc: the enum
*is* the accepted set, so `insertCoin` has no bad-coin failure mode to handle or to test.

---

## `Budget.java` — an invariant across a collection

```java
    private final ClientKey key;
    private final List<Rule> rules;

    private Budget(ClientKey key, List<Rule> rules) {
```

**A private constructor, and it is the only writer of either field.** So there is one place to look
when a budget is wrong, and no caller can build one that skipped `of`. J4 covered the mechanism;
what it buys here is that the duplicate-name check below has no way around it.

```java
        for (int i = 0; i < rules.size(); i++) {
            Objects.requireNonNull(rules.get(i), "rule at index " + i + " for client " + key);
        }

        List<Rule> copy = List.copyOf(rules);
```

**The loop runs before `List.copyOf`, and that ordering is the whole point of the loop.**
`List.copyOf` already refuses a null element. It refuses it with no message at all, which the run
above shows as `(no message)` for the unmodifiable case. This loop names the index and the client, so
the output reads `rule at index 1 for client acme`. Reverse the two statements and the message is
gone, because `copyOf` throws first.

```java
        List<Rule> copy = List.copyOf(rules);
```

**`copyOf` is what makes `final` on the field mean anything.** `final` stops the reference being
repointed and says nothing about the list. Store the caller's `ArrayList` and the caller can add a
duplicate name a second after the check passed. The invariant would then hold for one instant rather
than for the object's life, which is a different and much weaker promise.

The run shows the other half: `budget.rules().add(hourly)` throws
`UnsupportedOperationException`, so `rules()` can hand the field out with no copy per call.

```java
        Set<String> seen = new HashSet<>();
        for (Rule rule : copy) {
            if (!seen.add(rule.name())) {
```

**`Set.add` returning false is the duplicate test, so the loop is one pass.** The invariant is a
property of the whole list rather than of any element, so no `Rule` constructor could hold it. The
bug it prevents is stated in the message: a denial reporting `hourly` when two rules are called
`hourly` explains nothing, and the operator reading it cannot tell which quota was hit.

This is the rule `KeyBudget.of` enforces in
`corpus/rate-limiter/reference/DECISION_LOG.md`, for that reason.

```java
    public long remaining(long usedSoFar) {
        long fewest = Long.MAX_VALUE;
        for (Rule rule : rules) {
            fewest = Math.min(fewest, rule.limit() - usedSoFar);
        }
```

**No clamp, deliberately.** `Math.max(0L, rule.limit() - usedSoFar)` here would look careful and
would swallow the signal. A subtraction that goes negative means the counter was charged more times
than requests were allowed. Letting it through to `Decision` is what turns that into a thrown
exception naming the field and the value.

A key with no rules returns `Long.MAX_VALUE`, which is this problem's way of writing unlimited. That
is a convention, so it is stated rather than inferred.

---

## The last block of output, which nobody designed

`Main.theTornCount` runs 256 threads through a check-then-charge with the counter atomic and no lock
held across the two steps. The counter loses nothing. The decision does: a thread checks while units
remain, other threads charge, and by the time it charges the budget is gone.

Nothing in that method asserts anything about `remaining`. There is no test for it. The refusal
comes from `Decision`'s constructor, and the message names the field and the value:

```
remaining cannot be negative, got -64
```

This is not a scenario invented for the lesson. It happened during the corpus's own authoring, and
`corpus/rate-limiter/reference/DECISION_LOG.md` records it:

> several of the observed failures on the first variant were not assertions at all but
> `IllegalArgumentException: remaining cannot be negative, got -3` thrown by **`Decision`'s own
> compact constructor**.

The log's own reading of it. Putting the check in the value type meant a torn count could not be
reported quietly. The suite got that without needing a checker of its own.

That is the argument for placement at construction in its strongest form. The check was written to
stop a nonsense header. It caught a concurrency bug in a different file, in a code path its author
was not thinking about, years of code later in the general case. A `validate()` method could not
have done it, because nobody would have called it there.
