Clock and Testable Time
Inject a Clock or InstantSource instead of calling now() directly, so time-dependent code is predictable in tests and correct in production.
Clock clock = Clock.systemUTC(); // production
Clock fixed = Clock.fixed(Instant.parse("2026-10-13T09:00:00Z"), ZoneOffset.UTC); // tests
Instant now = Instant.now(clock);
LocalDate today = LocalDate.now(clock);Code that calls Instant.now() or LocalDate.now() directly depends on a hidden input: the machine's clock. Tests for "the token expires after 15 minutes" or "the report covers yesterday" then either sleep, or pass most days and fail at midnight or on 29 February. The fix is to treat time as a dependency and pass it in.
java.time.Clock
Clock provides the current instant and a zone. Every now method has an overload that takes one: Instant.now(clock), LocalDate.now(clock), ZonedDateTime.now(clock). The useful factories:
Clock.systemUTC()for production code that works in UTC;Clock.system(zone)when you need a specific zone.Clock.fixed(instant, zone)for tests: time stands still.Clock.offset(base, duration)to shift another clock, for example to see what the code does an hour later.
LocalDate.now(clock) uses the clock's zone to decide what day it is, which makes that choice visible instead of depending on the server.
InstantSource
Java 17 added InstantSource, an interface for "what moment is it now" with no zone attached; its only abstract method is instant(), and millis() has a default. Clock implements it. If a service needs only the current moment and not a zone, depend on InstantSource: it states the dependency more precisely, and because instant() is its only abstract method, a lambda can implement it in a test.
Wiring it up
final class TokenService {
private final InstantSource clock;
TokenService(InstantSource clock) { this.clock = clock; }
Instant expiry() { return clock.instant().plus(Duration.ofMinutes(15)); }
}
// production: new TokenService(Clock.systemUTC())
In a Spring application you declare one Clock bean and inject it wherever time matters. In tests you construct the service with a fixed or mutable clock and move time forward between calls, without sleeping.
Mistakes people make
- Injecting the clock but still calling
now()somewhere deep inside. Search the code base for.now()calls with no argument; each one is untestable. - A
staticclock that tests swap. Tests running in parallel then fight over it. Pass the clock through constructors. - Using a fixed clock to test timeouts that need time to pass. A fixed clock never moves; use a small mutable implementation instead, like the one below.
Example
When to use it
- Inject a Clock bean into a token service so tests can check expiry at exactly 15 minutes without sleeping.
- Compute 'yesterday' for a nightly per-customer report using a Clock in that customer's zone.
- Drive a rate limiter or session-expiry sweeper with a mutable test clock to cover window boundaries precisely.
More examples
A mutable clock for tests that need time to pass
The test moves time forward exactly to the window boundary, without sleeping, and checks the behaviour on both sides of it. Implementing InstantSource is enough because the limiter needs no zone, and tryAcquire is synchronized because one limiter is shared by every request thread.
final class TestClock implements InstantSource {
private Instant now;
TestClock(Instant start) { this.now = start; }
@Override public Instant instant() { return now; }
void advance(Duration d) { now = now.plus(d); }
}
final class RateLimiter {
private final InstantSource clock;
private final Duration window;
private final int limit;
private Instant windowStart;
private int count;
RateLimiter(InstantSource clock, Duration window, int limit) {
this.clock = clock;
this.window = window;
this.limit = limit;
this.windowStart = clock.instant();
}
synchronized boolean tryAcquire() {
Instant now = clock.instant();
if (!now.isBefore(windowStart.plus(window))) {
windowStart = now;
count = 0;
}
return ++count <= limit;
}
}
void main() {
TestClock clock = new TestClock(Instant.parse("2026-10-13T09:00:00Z"));
RateLimiter limiter = new RateLimiter(clock, Duration.ofMinutes(1), 2);
IO.println(limiter.tryAcquire() + " " + limiter.tryAcquire() + " " + limiter.tryAcquire());
clock.advance(Duration.ofSeconds(59));
IO.println("after 59s: " + limiter.tryAcquire());
clock.advance(Duration.ofSeconds(1));
IO.println("after 60s: " + limiter.tryAcquire());
}
// Output:
// true true false
// after 59s: false
// after 60s: trueYesterday's report in the customer's zone
At 03:00 UTC it is already 13 October in London but still 12 October in New York, so 'yesterday' is a different day for each. The clock carries the zone, and the half-open range of Instants plugs straight into a SQL query.
record Range(Instant from, Instant to) {}
Range yesterday(Clock clock) {
LocalDate today = LocalDate.now(clock);
ZoneId zone = clock.getZone();
return new Range(today.minusDays(1).atStartOfDay(zone).toInstant(),
today.atStartOfDay(zone).toInstant());
}
void main() {
Instant moment = Instant.parse("2026-10-13T03:00:00Z");
IO.println("London: " + yesterday(Clock.fixed(moment, ZoneId.of("Europe/London"))));
IO.println("New York: " + yesterday(Clock.fixed(moment, ZoneId.of("America/New_York"))));
}
// Output:
// London: Range[from=2026-10-11T23:00:00Z, to=2026-10-12T23:00:00Z]
// New York: Range[from=2026-10-11T04:00:00Z, to=2026-10-12T04:00:00Z]A lambda as an InstantSource
InstantSource has a single abstract method, so a lambda returning a fixed Instant is a complete test double. In production the same method receives Clock.systemUTC().
record Session(String id, Instant lastSeen) {}
List<String> expired(List<Session> sessions, InstantSource clock, Duration idle) {
Instant cutoff = clock.instant().minus(idle);
return sessions.stream()
.filter(s -> s.lastSeen().isBefore(cutoff))
.map(Session::id)
.toList();
}
void main() {
List<Session> sessions = List.of(
new Session("a", Instant.parse("2026-10-13T08:00:00Z")),
new Session("b", Instant.parse("2026-10-13T08:40:00Z")),
new Session("c", Instant.parse("2026-10-13T08:29:59Z")));
InstantSource nineAm = () -> Instant.parse("2026-10-13T09:00:00Z");
IO.println(expired(sessions, nineAm, Duration.ofMinutes(30)));
}
// Output:
// [a, c]
Discussion