Code that asks the system clock for the current time looks harmless. The difficulty appears when behavior depends on the answer. A test for an expired reservation may pass now and fail later. A boundary such as midnight can become awkward to reproduce. Business logic becomes tied to an environmental input that callers cannot see or control.
A useful design move is to treat the current time as a dependency. Instead of letting decision-making code reach for a clock whenever it needs one, obtain the time at a clear boundary and pass the relevant value or clock into the code that makes the decision.
This article explains that mental model, shows the smallest useful refactoring, and examines when passing a timestamp is enough and when a clock abstraction earns its extra structure.
The current time is an input
Consider a reservation that may be cancelled until its deadline:
function canCancel(reservation):
return systemNow() < reservation.cancelUntilThe function appears to take one input: reservation. In reality it has another input: whatever systemNow() returns during that call.
That second input is hidden in the function body. Two calls with the same reservation can therefore produce different answers because time has advanced. That may be correct business behavior, but it makes the function harder to reason about in isolation.
A better mental model is:
canCancel = decision(reservation, currentTime)Time has not disappeared. It has become visible.
Start with the smallest useful change
If the code only needs to make a decision about one moment, pass that moment directly:
function canCancel(reservation, currentTime):
return currentTime < reservation.cancelUntilProduction code can still read the real clock:
currentTime = systemNow()
allowed = canCancel(reservation, currentTime)The important change is where the read happens. The business decision no longer chooses its own time source.
A test can now state the exact scenario it wants to prove:
reservation.cancelUntil = 2026-09-05T10:00:00Z
assert canCancel(reservation, 2026-09-05T09:59:59Z) == true
assert canCancel(reservation, 2026-09-05T10:00:00Z) == falseThese examples also expose a boundary decision that was easy to overlook: cancellation is allowed strictly before the deadline, not at the deadline. Making time explicit helps the test describe that rule directly.
Read once when one operation needs one notion of now
A subtler problem appears when an operation reads the clock several times:
record.startedAt = systemNow()
performWork(record)
record.finishedCheckAt = systemNow()Sometimes those separate readings are intentional because the elapsed time matters. Sometimes they accidentally give one logical operation several different meanings of “now.”
Suppose an order service checks whether a promotion is active and then records the time at which the order was accepted. If both facts are meant to describe the same business event, reading the clock independently can create an awkward boundary case: the promotion check occurs just before midnight and the acceptance timestamp just after it.
When one operation should use one logical instant, capture it once:
now = systemNow()
if promotion.isActiveAt(now):
order.applyPromotion(promotion)
order.acceptedAt = nowThis is not a rule that every operation must read time only once. It is a question about semantics. Ask whether multiple readings represent meaningful elapsed time or accidental variation inside one decision.
Pass a value before inventing a clock interface
Making time explicit does not automatically require an abstraction such as Clock.
For a pure decision, a timestamp is often enough:
isExpired(session, now)
priceFor(subscription, now)
isWithinWindow(request, now)Passing a value has useful properties. The dependency is obvious, the function remains simple, and tests do not need a fake object.
A clock abstraction becomes more useful when a component performs several operations that legitimately need to ask for the current time itself. For example:
interface Clock:
now()
class TokenIssuer:
constructor(clock):
this.clock = clock
issue(userId):
issuedAt = this.clock.now()
return Token(userId, issuedAt)Production code can provide a system-backed clock. Tests can provide a fixed clock whose now() returns a chosen instant.
The abstraction creates a seam: a deliberate point where one implementation can be replaced by another. That is useful when the component owns the act of reading time. If callers already know the relevant timestamp, passing the value directly is usually simpler.
Keep the clock close to the boundary
Making time explicit can go too far. Passing a Clock through many layers that do not use it adds plumbing without improving the design.
Prefer to read or inject time near the part of the system that coordinates the operation, then pass ordinary values deeper into decision-making code.
For example:
class CheckoutService:
constructor(clock):
this.clock = clock
checkout(cart):
now = this.clock.now()
price = calculatePrice(cart, now)
order = createOrder(cart, price, now)
return orderHere the service coordinates the operation and owns the environmental dependency. calculatePrice and createOrder can remain functions of explicit data.
This arrangement also reduces the number of tests that need a fake clock. Most business rules can be tested with timestamps directly, while only the coordinating code needs to prove that it obtains time from the clock and passes it correctly.
Time zones are a separate design decision
An explicit clock solves control over the current instant. It does not by itself solve time-zone rules.
An instant such as 2026-09-05T03:00:00Z identifies a point on the timeline. A rule such as “the store closes at 18:00 local time” also needs a time zone or another clearly defined local-time policy to determine which instant that wall-clock time represents.
Keep those concerns distinct:
- use a clock or timestamp to control which instant is “now”;
- model the required time zone explicitly when a rule depends on local calendar or wall-clock time.
Hiding a time-zone lookup inside business logic recreates the same kind of invisible dependency that an explicit clock was meant to remove.
Common mistakes
Replacing every time value with a clock
A clock is a source of time, not a replacement for timestamps. If a function needs to compare a deadline with a known instant, pass the instant. Introducing a clock makes that function more stateful than necessary.
Using sleep to reach a time boundary
A test that waits for real time to pass is tied to scheduling and execution speed. If the behavior under test is a business rule rather than the clock implementation itself, supply the relevant instant directly or use a controllable clock.
Making the fake clock unrealistically magical
A test clock should have clear semantics. A fixed clock that always returns one instant is often sufficient. If tests need to advance it, make that operation explicit rather than changing time implicitly whenever now() is called. Otherwise the test double can introduce behavior that the production clock does not have.
Ignoring the exact boundary rule
Time-dependent bugs often live at equality boundaries. “Expires at 10:00” might mean invalid starting at 10:00, or valid through the end of that instant according to some domain convention. Decide the comparison deliberately and test just before, exactly at, and just after the boundary when those cases matter.
When the simpler approach is better
Not every use of the system clock deserves refactoring. Logging a timestamp for diagnostics, for example, may not affect a business decision and may not need deterministic unit tests. A small script that prints the current time can reasonably call the platform clock directly.
The technique earns its cost when time changes observable behavior that you need to reason about or test. Typical signals include expiration rules, scheduling decisions, age calculations, retry deadlines, time-based pricing, validity windows, or records whose timestamps must be consistent within one operation.
The goal is not to remove real time from software. It is to control where real time enters the logic.
Conclusion
When a function reads the current time internally, the clock is an input even if its signature does not show it. Making that input explicit turns time-dependent behavior into scenarios that developers can state and test precisely.
Start with the least complicated form: pass a timestamp to code that only needs a value. Introduce a clock abstraction when a component genuinely owns the act of obtaining the current time. Capture one instant when one operation should share one notion of “now,” and model time-zone rules separately when local time matters.
That small shift makes an important dependency visible. Once time is visible, boundary behavior, tests, and design decisions become easier to explain and maintain.