# When not to put the invariant in the constructor

STANDARD v1.0's D5 level 3 reads *"failure modes named in the code's structure — impossible states
unrepresentable rather than merely checked"*. That sentence is what this lesson teaches toward, and
it is also the sentence people over-read. Level 3 asks for the states that matter to be
unrepresentable. It does not ask for every field to be interrogated on the way in.

There is no `over-validated` tag in the rubric, so nothing in the grader will tell you off for this.
The bill arrives in three other places instead, and the third one is the expensive one.

## 1 · A check the type already made

`Algorithm` is an enum. There is no value of that type outside its constants, so a membership check
against them is dead code. Here is that check, written by somebody being careful. Put it next to
`worked/src/Algorithm.java` and it compiles: `javac 21` with `-Xlint:all` prints nothing and exits 0.

```java
public record NoisyRule(String name, Algorithm algorithm, long limit, Duration window) {

    public NoisyRule {
        Objects.requireNonNull(name, "name");
        Objects.requireNonNull(algorithm, "algorithm");
        Objects.requireNonNull(window, "window");
        if (algorithm != Algorithm.FIXED_WINDOW && algorithm != Algorithm.TOKEN_BUCKET) {
            throw new IllegalArgumentException("rule " + name + ": unknown algorithm " + algorithm);
        }
        if (name.length() < 0) {
            throw new IllegalArgumentException("rule " + name + ": a name cannot have negative length");
        }
        if (window.toNanos() != window.toNanos()) {
            throw new IllegalArgumentException("rule " + name + ": the window changed while we read it");
        }
    }
}
```

Two of those three branches can never run. `String.length()` is never negative and `Duration` is
immutable, so both conditions are constant false. `javac` says nothing about either, which is why
they survive review.

The first branch is worse, because it can run, and the day it does it is wrong. Add a third constant
to `Algorithm` and this is the real output:

```
NoisyRule[name=hourly, algorithm=FIXED_WINDOW, limit=100, window=PT1H]
threw java.lang.IllegalArgumentException
  rule burst: unknown algorithm SLIDING_WINDOW
Rule[name=burst, algorithm=SLIDING_WINDOW, limit=5, window=PT1S]
```

`Rule` accepts the new algorithm without being touched. `NoisyRule` refuses it, using the message
that was meant to be protective. A membership check over a closed type is not neutral. It is a
future bug with a helpful tone.

The corpus states the same restraint in `corpus/vending-machine/contract/Coin.java`:

> The enum *is* the accepted set: there is no such thing as a coin the machine does not recognise, so
> `insertCoin` has no "bad coin" failure mode.

`corpus/lru-cache/problem.json` makes the honest version of the point about scale. It calls A5
*"the honest weakest tag on this problem"*, because the only invariant its design has is a positive
capacity, checked once in a constructor. One scalar check with no ceremony round it is the right
answer there. Adding a validation layer to reach level 3 would have made that design worse.

## 2 · A rule that is not a property of the type

Three questions decide whether a rule belongs in a constructor. The interesting failures are the ones
that fail exactly one.

**Does it hold for every instance, or only for some callers?** "`remaining` is never negative" holds
for every decision. "Enterprise tenants may go 1000 into overdraft" does not. Put the second one in
the constructor and it has no access to the tenant, so the rule has to be widened into a parameter or
dropped. Both of those edits touch the type, every factory on it and every construction site.

**Does it hold for the object's whole life, or only at one instant?** A `Slot` in
`corpus/vending-machine/contract/Slot.java` refuses a negative quantity, and that is a property of a
snapshot. "This slot has stock" is not, and it is the machine's business rather than the value's.

**Can it be decided from the arguments alone?** A uniqueness check against a database cannot. A
constructor that opens a connection is a much bigger problem than the invariant it was enforcing.

### The concrete cost: a caller who only wanted to look

An operator console lets somebody type a rule field by field and underlines what is wrong as they go.
`Rule` cannot hold that state, because it refuses to exist until all four fields are right. So a
second type appears. This compiles next to `worked/src/Rule.java`, `-Xlint:all` clean:

```java
public final class RuleForm {

    private String name;
    private Algorithm algorithm;
    private Long limit;
    private Duration window;

    /** The same rules again, so the form can underline one field at a time. */
    public List<String> problems() {
        List<String> found = new ArrayList<>();
        if (name == null || name.isBlank()) found.add("name is required");
        if (algorithm == null) found.add("algorithm is required");
        if (limit == null) found.add("limit is required");
        else if (limit < 1L) found.add("limit must be at least 1");
        if (window == null) found.add("window is required");
        else if (window.isZero() || window.isNegative()) found.add("window must be positive");
        return List.copyOf(found);
    }

    public boolean submittable() { return problems().isEmpty(); }

    public Rule toRule() { return new Rule(name, algorithm, limit, window); }
}
```

Nothing about that is unreasonable, and a real admin console needs it. The cost is that one rule set
now exists twice, and the two copies drift. Real output, and note the last two lines:

```
empty form       [name is required, algorithm is required, limit is required, window is required]
name typed       [algorithm is required, limit is required, window is required]
all four typed   []
submittable      true
toRule() threw   rule firehose: limit 10000000000 over PT1H is too large to count exactly in nanoseconds
```

`submittable` says yes. The constructor says no. The form's author copied three of `Rule`'s four
invariants and missed the overflow one, so the console offers a Save button that always fails. The
operator has no way to tell what is wrong, because the form is the thing that was supposed to say.

The fix is not to drop the constructor check. It is to make `RuleForm.problems()` call one shared
predicate that the constructor also calls, so the rule exists once. What this costs is the extra
public surface, and it is a real cost worth naming out loud when an interviewer asks.

## 3 · An invariant that repairs instead of refusing

This is the one that measures worse than having no invariant at all, and `contrast/` measures it.

`contrast/b-normalising/Decision.java` puts the checks in exactly the place `b/` does. It differs in
one respect: it clamps `remaining` to zero and substitutes a one-second wait, rather than throwing.
That is the change a reviewer asks for after a negative `Retry-After` has caused an incident.

From `node lessons/A5/contrast/measure.mjs`:

| Scenario | `a/` no invariant | `a-validate/` method | `b/` refuses | `b-normalising/` repairs |
|---|---|---|---|---|
| torn count, `remaining` = -3 | never detected, 7 calls | never detected, 7 calls | 2 calls | **never detected**, 7 calls |
| stale window boundary, via the responder | 6 calls | 3 calls | 2 calls | **never detected** |
| stale window boundary, via the caller | 5 calls | 3 calls | 2 calls | **never detected** |
| size | 114 normalised lines | 127 | 124 | 122 |

Read the audit lines, not the counts. In `a/` the log records `remaining=-3`, which a human can find
and reason about. In `b-normalising/` it records `remaining=0` and a response of `100% of quota used`,
which is what an honestly exhausted quota looks like. The evidence is gone.

`b-normalising/` costs 8 more lines than `a/` and detects less. It is the only one of the four
designs where a torn count leaves no trace anywhere.

The same thing happens to the retry hint. The fallback wait is one second, so a caller holding a
stale window boundary sleeps for a second nobody asked for and retries. No error, no log line, and
the boundary is still stale.

## 4 · The clock

A validation pass over a type nobody asked you to validate is time. STANDARD v1.0 caps D2 at 0 when
no `main` exists, on the grounds that *"interviewers run it first"*, and D2 is 25% of the score
against D5's 10%. Four unreachable checks and a form class are not worth a missing driver.

The corpus's own strongest A5 showcase is small.
`corpus/rate-limiter/contract/Decision.java` is one compact constructor. `ClientKey` is one check.
`Rule` is four. `corpus/rate-limiter/problem.json` calls A5 *"strong here, and spread across four
types rather than concentrated in one"*, and none of those four types is large.

## The threshold

**Put the rule in the constructor when all three of these hold.**

1. It holds for every instance of the type, not for some callers.
2. It holds for the object's whole life, not at one instant.
3. It can be decided from the constructor's arguments alone, with no I/O and no context.

Fail any one of the three and the rule belongs somewhere the caller can see it: a policy object, a
service method, or a documented precondition. Tenant overdraft fails the first. Stock levels fail the
second. A uniqueness check against a store fails the third.

**Then, inside the constructor, refuse rather than repair.** If you would not be willing to throw,
the value is not an invariant. It is a default, and a default belongs in a named static factory where
the call site says what was assumed.

**And do not check what a type already closes.** An enum, a sealed hierarchy or a `record` component
that cannot be null needs a null check at most. A membership check over a closed set is a bug waiting
for the set to grow.
