# When not to inject

Inversion is not "put an interface in front of every collaborator". STANDARD v1.0 says so with a
number attached. D3 level 2 wants *"dependencies are injected, including time"*. D3 level 3 wants
*"the seam set is minimal — no speculative interface with a single implementation and no foreseeable
second one"*. The standard then states the symmetry outright: *"level 3 penalises over-abstraction as
much as level 0 penalises none."* The failure tag is `over-engineered (premature interface)`.

So level 2 asks you to inject the clock. Level 3 asks you to stop there.

## The corpus refused, and it is worth reading how

`corpus/file-system` is the case. Its one field and its only constructor, in
`reference/src/InMemoryFileSystem.java`:

```java
private final PathResolver tree;

public InMemoryFileSystem() {
    this.tree = new TreeResolver(new DirectoryNode());
}
```

The field is the abstraction. `PathResolver` is an interface, and not one of the eight operations in
that class ever holds a `Node` it found by walking children itself. Every one of them asks the
resolver. That is inversion working, and `reference/DECISION_LOG.md` shows the payoff three times
over. The shortcuts curveball taught seven operations about a new kind of node with **zero lines
changed** in any of them, because only the resolver had to learn.

And the constructor builds its own `TreeResolver` anyway.

**That is not an oversight, and the syllabus tag proves it.** `corpus/file-system/problem.json` lists
`A3 A5 A6 A7 · B5 · C1 C2 · D1 D2 D3 · F1 F2 F3 F4 F5`. **C3 is not on it.** The problem depends on
the right *type* and declines to add the injection point. The decision log states the test it applied
one paragraph over, about the factory it also refused, under *"What was deliberately not built"*:

> **No factory.** Nothing about what a standard file system is *made of* varies — there are no
> policies to wire, no starting contents, no configuration.
>
> [...] A factory earning its keep needs a decision to hide; this problem's construction has none.

Read what it checked for: no policies to wire, no starting contents, no configuration. Add the one it
did not have to mention, because the problem statement scopes out timestamps: no clock. So there is
nothing a test would want to substitute, and nothing that varies. A `PathResolver` parameter on that
constructor would let a caller pass a different tree, and no caller ever would.

**What it would have cost.** Every construction site would name a resolver. `Entry.create()` becomes
`new InMemoryFileSystem(new TreeResolver(new DirectoryNode()))`, and the class stops being able to
guarantee that its resolver is the one it started with. A caller could hand it a half-built tree.
The invariant "this file system's tree is rooted at a directory that exists" moves out of the
constructor and becomes the caller's problem. D1 level 3 asks for the opposite: invariants in
constructors, so no invalid instance can exist.

## The concrete bad example

Here is the version that looks like this lesson has been learned. Applied to `worked/src`, it
compiles: `javac 21` with `-Xlint:all` prints nothing and exits 0.

```java
public interface CounterStore {
    long get(Object key);
    void put(Object key, long value);
}

public final class InMemoryCounterStore implements CounterStore {
    private final java.util.Map<Object, Long> counters = new java.util.HashMap<>();

    @Override public long get(Object key) { return counters.getOrDefault(key, 0L); }
    @Override public void put(Object key, long value) { counters.put(key, value); }
}

public final class CounterStoreFactory {
    public static CounterStore forLimiter(String limiterName) {
        return new InMemoryCounterStore();
    }
}
```

Then `FixedWindowLimiter` takes a `CounterStore` beside its `Clock`, and the argument for it sounds
identical to the argument for the clock. It is a collaborator. It holds state. Redis exists.

**What a reviewer sees.** Three files and one behaviour. `forLimiter("edge")` and
`forLimiter("internal")` return the same kind of object, so the parameter is a promise the code does
not keep. `Object` as a key type is the tell: the real key is the private `Counter` record, and an
interface that cannot name its own key type has not found a boundary. The next person adds a
`CounterStoreRegistry` so stores can be configured, and now there is a configuration format for a
system with one store.

**Why the clock is different, in one sentence each.** The clock has two implementations in the
lesson, `ManualClock` and the `Instant::now` lambda in `Main.java`, and one of them is the only way
to observe the behaviour at all. `CounterStore` has one implementation, no second one named in any
requirement, and a `HashMap` that is already deterministic, so a substitute would prove nothing a
test cannot prove today.

**What the grader sees.** Behaviour is behind an interface, so D3 reaches level 2. Level 3 is now out
of reach, because the seam set is not minimal. `over-engineered` routes to `lessons/B1` at the
`when-not` stage. This is a case where doing more scores less.

## The axis this lesson deliberately did not seam

`worked/src/FixedWindowLimiter.java` has three time-related helpers and none of them is injectable:

```java
static long windowIndex(Instant at, Duration window)
static Duration untilNextWindow(Instant at, Duration window)
private static long epochNanos(Instant at)
```

All three are `static`, which means they cannot reach a field, which means they cannot read a clock
even by accident. That is not a missing seam. It is the compiler enforcing the rule the class exists
to hold. A `WindowStrategy` interface here would be an implementation nobody has asked for. It would
also hand back the guarantee the `static` keyword is buying. An implementation of it *could* read a
clock, and then a request would no longer be judged at one instant.

Sliding windows are the obvious "but what about" here. They are a real variation, and
`corpus/rate-limiter`'s reference does put algorithms behind `RateLimitAlgorithm` for exactly that
reason. Two points about that. It has a second implementation, `TokenBucketAlgorithm`, so it passes
the test. And its stated terms **forbid** implementations from reading a clock: the instant arrives
as a parameter. The seam is on the counting rule. It is still not on the source of time.

## What the seam costs, measured

Three prices, and the middle one is the expensive one.

**It costs code before any requirement arrives.** From `node lessons/C3/contrast/measure.mjs`:

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

Both directories are commented for teaching, so read the shape rather than the multiplier. The seam
added three files and a constructor parameter for behaviour that did not change.

**It costs more than no seam when the requirement wants a different kind of time.** Also measured:

| | `a/`, wall clock inside | `b/`, clock injected |
|---|---|---|
| cool-off after a denial | 8 | **8** |
| monotonic uptime | 5 | **28** |

The 28 is the number that matters here. Uptime needs a monotonic tick count, and `Clock` carries
`Instant now()` and nothing else. Widening it touched the interface, the manual implementation, the
limiter and the wiring — four files, where the design with no seam touched one. Eleven of those lines
are one nested class replacing a lambda, because a two-method interface is not a functional
interface:

```
Wiring.java:11: error: incompatible types: Clock is not a functional interface
        Clock system = Instant::now;
```

Note also the tie on the row above. The cool-off change cost the same 8 lines in both designs, so
injection bought nothing that `measureChange` can see. What it bought was under 0.03 seconds instead
of 30, and only the D2 instrument reports that.

**It costs the ability to construct the object casually.** `new FixedWindowLimiter(clock)` has no
no-argument form, on purpose. Every test, every driver and every call site now names a clock. That is
the right trade for a rate limiter, where time is the subject. It is the wrong trade for a class
whose collaborator never varies, and `corpus/file-system` is what the wrong trade avoided.

## Constructor, not setter, and the reason is one keyword

```java
private final Clock clock;

public FixedWindowLimiter(Clock clock) {
    this.clock = Objects.requireNonNull(clock, "clock");
}
```

`final` plus a constructor assignment is what makes an injected dependency trustworthy. `javac`
proves the field is assigned exactly once on every path. `lessons/J4/` has the two messages:
`variable clock might not have been initialized`, and `variable clock might already have been
assigned`. So there is no moment when a `FixedWindowLimiter` exists without a clock, and no method
has to consider one.

A `setClock(Clock)` gives all of that back. Every method has to tolerate a null clock, because the
object can now exist before the clock does. Two callers can disagree about which clock is installed,
and the later one wins silently. Worst of all, a test that swaps the clock halfway through leaves the
object holding counters from one timeline and reading another. Constructor injection makes the
dependency part of the type. Setter injection makes it a suggestion.

## The threshold, from both sides

**Inject** when either half is true.

The collaborator reads something outside the program that a test needs to control. Time, randomness,
the network, the file system, an id generator. One test is the whole justification, and for the clock
the test does not otherwise exist: you cannot assert a 30-second freeze in under 30 seconds without
it.

Or a second implementation exists, or a requirement sentence names one. Two that exist, or one plus
one named, is enough. `corpus/rate-limiter/contract/Clock.java` clears this twice over: the grader
hands the design a `ManualClock`, and *"production would hand you `Instant::now` as a one-line
lambda."*

**Construct in place** when neither is true. The collaborator is a pure function of its inputs, it
has one implementation, and no requirement names a second. Then `new TreeResolver(new
DirectoryNode())` in the constructor is the right answer, and the invariant stays where it can be
enforced.

**Never inject to make something testable that is already deterministic.** A `HashMap` needs no
substitute. `worked/src` keeps `spent` and `rules` as plain fields for that reason, and every
assertion in `faded/GapTest.java` reaches them through the limiter's own methods.

## What this file is not saying

It is not saying inject less. The single highest-frequency reason an LLD design becomes untestable is
a call to `Instant.now()` buried inside a method that decides something, and one line fixes it.
`corpus/cost-explorer/contract/TimeSource.java` puts the consequence of skipping it in the plainest
terms in the corpus. A design that reads the wall clock inside a proration calculation *"will fail
the suite in a way that looks like an arithmetic bug."*

Inject time. Then look at the next collaborator and ask what a test would want to substitute, and be
willing to answer nothing.
