# The measured pair: one line of difference, three instruments, and only one of them can see it

Two limiters. `a/WallClockLimiter.java` calls `Instant.now()` inside `tryAcquire`.
`b/InjectedClockLimiter.java` takes a `Clock` in its constructor and calls `clock.now()` there
instead. Read them side by side. Apart from the class name, the field, the constructor parameter and
that one call, they are the same file.

Both compile clean under `javac -Xlint:all`. Both work. `a/` is not a straw man: it reads the time
once per request and threads the instant into `judge`. That is what a competent engineer writes under
pressure, and it is what most submissions look like.

Then three things happen to them.

## Change one, in the interviewer's words

> One more thing on the limiter. When you refuse a client, refuse it for the next thirty seconds
> whatever happens — even if a new window starts in the middle. We had a customer retrying in a
> tight loop and the per-window reset was letting them straight back in.

## Change two, in the interviewer's words

> Ops wants uptime out of this thing. Careful though: not from the wall clock. Our boxes get NTP
> corrections and we have seen negative durations in logs before. Use a monotonic source.

## The numbers

```
node lessons/C3/contrast/measure.mjs
```

Real output from one run. The four `diffLines` numbers are the same on every run. The three timing
rows are not, and the spread is discussed below.

```
cool-off after a denial   (about time)   a            -> a-cooloff     diffLines   8  touched 1  new 0  [WallClockLimiter.java +7/-1]
cool-off after a denial   (about time)   b            -> b-cooloff     diffLines   8  touched 1  new 0  [InjectedClockLimiter.java +7/-1]
monotonic uptime          (new source)   a            -> a-monotonic   diffLines   5  touched 1  new 0  [WallClockLimiter.java +5/-0]
monotonic uptime          (new source)   b            -> b-monotonic   diffLines  28  touched 4  new 0  [Clock.java +2/-0, InjectedClockLimiter.java +6/-0, ManualClock.java +6/-0, Wiring.java +12/-2]

size of a    2 file(s)  44 normalised lines
size of b    5 file(s)  82 normalised lines

a/ with tests-sleep    2 tests, both sleep   2/2 pass  green true  junit reports 1.989s in tests  whole call 3.27s
b/ with tests-manual   4 tests, none sleep   4/4 pass  green true  junit reports 0.018s in tests  whole call 1.30s
b-cooloff/ tests-cooloff  a 30s freeze    1/1 pass  green true  junit reports 0.028s in tests  whole call 1.31s
```

| | `a/`, wall clock inside | `b/`, clock injected |
|---|---|---|
| fixed cost before any change | 2 files, **44** lines | 5 files, **82** lines |
| cool-off after a denial | **8** lines, 1 file | **8** lines, 1 file |
| monotonic uptime | **5** lines, 1 file | **28** lines, **4** files |
| checking the behaviour | **1.3 to 2.2 s**, 2 tests | **under 0.06 s**, 4 tests |
| checking a 30-second freeze | at least **30 s** per assertion | **under 0.03 s** |

## What each number means

**The cool-off change costs 8 lines either way, and that is the finding.** Identical edits, in one
existing file, in both designs. `measureChange` is the function that scores D4 in a graded attempt,
and here it reports a tie. If you came to this lesson expecting injection to shrink a diff, it does
not. What it changes is elsewhere, and no diff instrument can see it.

**Where it does change is the last row.** `tests-cooloff/CooloffTest.java` asserts the interesting
case: a freeze that starts ten seconds before a window boundary and outlasts it, so a fresh window
opens while the client is still refused. Against `b-cooloff/` that is four assertions in under 0.03
seconds. Against `a-cooloff/` there is no version of it under 30 seconds, and the reason is not
laziness. `COOL_OFF` is `Duration.ofSeconds(30)` in both trees because the requirement said thirty
seconds. A test parameter can be shrunk. A requirement cannot.

**The 1.989 against 0.018 is measured, and the 1.989 is not even stable.** Six runs of `tests-sleep`
reported 1.257, 1.355, 1.492, 1.670, 1.989 and 2.153 seconds in tests. The spread is the second test
waiting out a `retryAfter` whose length depends on where in the window the run happened to start.
`tests-manual` stayed under 0.06 seconds in all six. A suite whose duration you cannot predict is a
suite that eventually gets a longer timeout instead of a fix.

The `diffLines` numbers, by contrast, are identical on every run. That is the difference between
measuring structure and measuring behaviour, and it is worth knowing which of the two a number is.

**And `tests-sleep` asserts less, not the same.** Read the two files. `SleepTest` can only check
that `retryAfter` is *no longer than one window*, because it cannot choose where in the window a
request lands. `ManualTest` asserts the exact value, 40 seconds from 20 seconds into a minute, and
then asserts that one nanosecond less is not enough. A design that returned the whole window length
passes `SleepTest` and fails `ManualTest`. The sleep suite is slower *and* weaker.

**`b/` costs 38 normalised lines and three extra files before anything happens.** That is the seam's
fixed price: `Clock.java`, `ManualClock.java`, `Wiring.java`, a field and a constructor parameter.
Both directories are commented for teaching, so read the shape rather than the multiplier.
`when-not.md` is about when that price is not worth paying.

**The 28 against 5 is the change that goes the wrong way.** Uptime needs a monotonic tick count,
and `Clock` carries `Instant now()` and nothing else. Widening it touches the interface, every
implementation, and the wiring — four files. `a-monotonic/` adds a field and a method to the one
class that needed them: five lines.

There is a compile error inside that 28 worth seeing, because it is the seam's cost showing up as
a type error rather than as a design opinion. `b/Wiring.java` reads:

```java
Clock system = Instant::now;
```

Add a second abstract method to `Clock` and that line stops compiling:

```
Wiring.java:11: error: incompatible types: Clock is not a functional interface
        Clock system = Instant::now;
                       ^
    multiple non-overriding abstract methods found in interface Clock
1 error
```

`Wiring.java` is 12 added and 2 removed, and 11 of those additions are the nested `SystemClock` class
that replaces the lambda. This is the same shape `corpus/lru-cache/curveballs/02-per-entry-ttl` measures at full
size. Its `reference_diff` is 9, and its `budget.json` note says all nine are *"a single unreachable
stub method forced by widening `LruCacheApi`"*. A class that implements an interface directly pays
for every method added to it, whether it uses the method or not.

## The alternatives, so the choice is a choice

**Could `b/` have absorbed the uptime requirement by adding a file?** Yes, and it is the better
answer. Inject a second, separate dependency, a `Ticker` with `long nanoTicks()`, and leave `Clock`
alone. Nothing already written changes. That is not what was measured here, and the reason is worth
stating. The 28 is the price of the reflex to widen the interface you already have, which is what
happens with twelve minutes left. Interface segregation is syllabus item **C5**, and this is the
measurement that motivates it.

**Could `a/` have been tested without sleeping?** Only by changing the design, which is the point.
The usual attempts each fail somewhere specific. A `static Instant fakeNow` that tests overwrite
makes the clock global, so two tests cannot run in parallel and one that forgets to reset it breaks
the next. Mocking `Instant.now()` needs a bytecode-rewriting mock framework, and no LLD round lets
you add one. Passing the instant into `tryAcquire` does work.
`corpus/rate-limiter/contract/Clock.java` explains at length why it takes the other road. The
invariant stops being yours: *"this client gets a hundred a minute"* becomes *"provided every caller
passes an honest timestamp"*.

**Is `a/` therefore wrong?** Not in every problem. `corpus/file-system` has no clock anywhere, and
`corpus/file-system/reference/src/InMemoryFileSystem.java` constructs its own collaborator in one
line for that reason. `when-not.md` is about that refusal.

## What this pair does not show

Two files against five, and 44 normalised lines against 82. In a real problem the fixed cost of the
seam stays about the same and everything else grows around it, which makes the ratio flatter and the
testability argument stronger. `corpus/rate-limiter` is the full-scale version: one `Clock`
interface, one `ManualClock`, and **28 base tests, not one of which sleeps**. Its
`reference/DECISION_LOG.md` records what that bought for a system whose rules are measured in minutes
and hours: *"the whole base suite finishes in under two seconds"*.
