Dependency Inversion Principle
Core. Expect to meet this one, and expect to be asked for it by name.
Start with the problem
A rate limiter has to know what time it is to decide whether a window has rolled over. The first version asks the machine directly, right where the decision gets made.
public Decision tryAcquire(String client) {
Instant now = Instant.now();
long index = windowIndex(now, window);
// ... check and update the counter for this window
}
For running the limiter, this is correct, and it is the version anyone reaches for first: Instant.now() is one call, and it is always right there.
Watch where it goes
Then you write the test. Two requests a minute, and the third inside the same minute should be denied. That part is simple to assert. The next assertion is the one that hurts: after the window rolls over, a request should be allowed again. Proving that means waiting for a real minute to pass, in a test that runs on every commit.
@Test
void allowsAgainAfterTheWindowRolls() throws InterruptedException {
limiter.tryAcquire("acme");
limiter.tryAcquire("acme");
Thread.sleep(60_000); // and now everyone waits a minute for one assertion
assertTrue(limiter.tryAcquire("acme").allowed());
}
This test is correct and it will pass. It is also the reason a thirty-second cool-off requirement, added later, becomes nearly impossible to test honestly: a test's timeout can shrink, but the thirty seconds the requirement asks for cannot. The limiter's own code has no bug. The bug is that nothing about it can be told what time it is, so nothing about it can be tested without actually waiting.
The move
Instant.now() is not a detail. It is a collaborator, exactly the way a database or a network call is, and it is the one collaborator every method in this class secretly depends on. Naming it and asking for it, instead of reaching for it, is the whole fix.
public interface Clock {
Instant now();
}
public final class FixedWindowLimiter {
private final Clock clock;
public FixedWindowLimiter(Clock clock) {
this.clock = Objects.requireNonNull(clock, "clock");
}
public Decision tryAcquire(String client) {
Instant now = clock.now();
// ... exactly the same decision as before, on this instant
}
}
The test gets a ManualClock that only moves when told to.
ManualClock clock = new ManualClock(Instant.parse("2026-08-18T10:00:00Z"));
FixedWindowLimiter limiter = new FixedWindowLimiter(clock);
limiter.tryAcquire("acme");
limiter.tryAcquire("acme");
clock.advance(Duration.ofMinutes(1));
assertTrue(limiter.tryAcquire("acme").allowed());
No sleeping, and the thirty-second cool-off requirement that was nearly untestable a page ago is now one call to advance. Production hands the limiter the real clock at the one place that constructs it.
FixedWindowLimiter live = new FixedWindowLimiter(Instant::now);
That is the whole principle, stated as code instead of as a slogan. FixedWindowLimiter used to depend on a concrete fact, the machine's own clock. Now it depends on an interface it defines the shape of, and both the production clock and the test's ManualClock depend on that same interface. High-level code no longer reaches down into a low-level detail; the detail is written to satisfy the shape the high-level code asked for.
What modern Java changes here
Clock has one method, so it is a functional interface, and the production wiring above is a method reference with no class behind it at all: Instant::now. That is why constructor injection in modern Java costs one constructor parameter and one field, not a factory and a configuration file. final on the field matters as much as the injection itself. javac proves the field is assigned exactly once, on every path through the constructor. So there is never a moment where a FixedWindowLimiter exists without a clock, and never a method that has to consider one being absent. A setter would give that guarantee back: two callers could each hand it a different clock, and whichever call happened last would win, silently.
The version of this that is wrong
Not every collaborator is the clock. A rate limiter also needs somewhere to keep its counters, and a HashMap is the obvious choice. Taken as far as the clock argument seems to suggest, that becomes an interface too.
public interface CounterStore {
long get(Object key);
void put(Object key, long value);
}
public final class InMemoryCounterStore implements CounterStore { /* wraps a HashMap */ }
Nothing in the requirements names a second CounterStore, and a HashMap is already deterministic: it needs no substitute to make a test pass. The tell is in the key type. The real key is a small record combining the client, the rule and the window, and an interface that can only describe its key as Object has not found a boundary. It has hidden one. over-engineered (premature interface) is the tag STANDARD v1.0 gives this, for the identical reason a speculative PricingPolicy with one implementation earns it.
The clock earns injection because a test needs to control it and because a second implementation, the real one and ManualClock, already exists the day the interface is written. Ask the same two questions of every other collaborator before reaching for a constructor parameter. Does a test need to substitute this, and does a second implementation exist, or get named in a requirement? Answer no to both, and construct it in place instead.
Where this lives in the app
Syllabus item C3 is the full lesson, measured rather than argued. The suite against a wall-clock limiter spends over a second on two tests; the suite against the injected version runs four broader tests in under 0.06 seconds. Read C3 for those numbers, and for the file-system case that declines this exact injection on purpose because nothing in that problem ever varies what a tree is built from.