Production LLD
Clock, randomness, UUIDs and tests
Imagine a parking fee test that fails at midnight, a snake-and-ladder test that cannot force a landing, or a booking hold that expires "sometimes." The domain called LocalDateTime.now(), UUID.randomUUID(), and new Random() in the middle of a use case. You cannot freeze those globals, so the same path is unrepeatable.
Deterministic design means tests control time, ids, and randomness instead of the wall clock or a global Random. Treat , , and as injected dependencies. Never call the wall clock from deep domain code.
What you inject
Fees, TTL (expiry window), rate limits, and calendar overlap all read time.
Tickets, orders, short URLs, and correlation ids need injectable identifiers.
Snake and ladder, shuffle, and matchmaking jitter must not call global Random.
What actually happens when time is hidden
The midnight story above is the default whenever a use case reads the world instead of values it was given. A fee that calls the wall clock depends on when you ran the test. A Board.move that constructs its own Random cannot land on a chosen square. A reservation that asks "now" inside the domain expires only when the laptop clock happens to pass the hold.
The fix is to treat those reads as ports. A answers "what time is it?" so tests can install Clock.fixed or a MutableClock (a Clock you can advance). An returns the next id so tests expect stable values. A returns the next roll so a FakeDice can script 4, then 6.
Push those reads to the edge: the controller (the object that starts the use case) reads clock.instant() once, then a does arithmetic on the Instants (timestamp values) it received. If you cannot replay a scenario with the same timestamps, you cannot debug it in production logs either.
Simple rule of thumb
Once you see a hidden , , or , the habit is the same: inject the port, pass values inward, and control the port in the test.
In simpler terms
Freeze time. Script the dice. Sequence the ids. Determinism is for production logs as much as for unit tests, not a testing luxury.
From problem to mechanism
Here is the parking exit again, slowed down so you can see where the naive fee fails and what the injected changes.
Problem. PricingStrategy (the fee policy) and Ticket.create read the wall clock and a random UUID themselves. Every test becomes a race with the laptop, and you cannot write an assertion on a known ticket id.
Mechanism. The service reads clock.instant() once and passes that Instant into a . next() runs at Ticket.open. roll() is injected into the game. TTL (expiry), rate limits, and calendar overlap use the same habit: advance a FakeClock past the hold, then assert.
Clock at the edge, not in the entity
class ParkingService {
private final Clock clock;
private final TicketRepository tickets;
private final PricingStrategy pricing;
Receipt exit(TicketId id) {
Instant now = clock.instant(); // not LocalDateTime.now()
Ticket ticket = tickets.find(id).orElseThrow();
Money fee = pricing.fee(ticket, now);
ticket.close(now);
tickets.save(ticket);
return new Receipt(ticket.id(), fee, now);
}
}
class HourlyPricing implements PricingStrategy {
public Money fee(Ticket ticket, Instant exitAt) {
long seconds = Duration.between(ticket.enteredAt(), exitAt).getSeconds();
if (seconds < 0) throw new IllegalArgumentException("negative stay");
long hours = Math.max(1, seconds / 3600 + (seconds % 3600 == 0 ? 0 : 1));
return ticket.tariff().times(hours);
}
}
Clock frozen = Clock.fixed(Instant.parse("2026-01-15T10:00:00Z"), ZoneOffset.UTC);
class MutableClock extends Clock {
private Instant now;
MutableClock(Instant start) { this.now = start; }
public void advance(Duration d) { now = now.plus(d); }
public Instant instant() { return now; }
public ZoneId getZone() { return ZoneOffset.UTC; }
public Clock withZone(ZoneId zone) { return this; }
}Scripted dice and sequential ids
interface Dice {
int roll();
}
class FakeDice implements Dice {
private final Deque<Integer> script;
FakeDice(Integer... faces) { this.script = new ArrayDeque<>(List.of(faces)); }
public int roll() { return script.removeFirst(); }
}
class SequentialIds implements IdGenerator {
private int n;
public TicketId next() { return new TicketId("T-" + (++n)); }
}Worked example: advance the clock; do not sleep
The mechanism names an injectable ; this is what advancing it looks like for a park and a reservation hold. The numbers sit in one place so later sections can point here.
Frozen timeline
T+0
Freeze Clock at 10:00, park KA-01, and store enteredAt as 10:00, not the laptop time.
T+2h
Advance MutableClock two hours, exit, and charge using 12:00. No Thread.sleep.
T+2h1s
Advance one more second if TTL (expiry) matters, then assert the reservation expired.
Reservation expiry without sleep
@Test
void reservationExpiresAfterTenSeconds() {
MutableClock clock = MutableClock.at("2026-01-15T10:00:00Z");
HoldService holds = new HoldService(clock, new InMemoryHolds());
HoldId id = holds.reserve(spot("A-1"), Duration.ofSeconds(10));
clock.advance(Duration.ofSeconds(11));
holds.expireDue();
assertTrue(holds.find(id).isEmpty());
}Numbers to know
- Frozen stay: entry 10:00, exit 12:00, so the fee test never waits two real hours.
- Hold of 10 seconds; advance 11 seconds, then expireDue() (see the test above).
- Sequential test ids T-1 then T-2, so two parks in one test stay assertable.
Which side of the edge is this call on?
The playbook only works if you can spot a hidden read. Run every now(), UUID, or Random through this pair; anything on the left needs a port.
Inside the domain (wrong)
LocalDateTime.now()in PricingStrategy races every test against the real wall clock.UUID.randomUUID()in Ticket.create makes a known-id assertion impossible to write.new Random().nextInt(6)in Board.move makes a chosen landing untestable.Thread.sleepto wait for TTL (expiry) is slow, flaky, and not deterministic.
At the edge (right)
- Controller reads .
instant()once per use case, then passes that Instant down. - .
next()at Ticket.open so tests can expect sequential ids (see Numbers to know). - .
roll()injected into the game so FakeDice can return 4, then 6. - MutableClock.advance past the hold, then run expireDue(); do not sleep (see Numbers to know).
Where this shows up, and what still breaks
The same three ports keep returning in machine-coding problems. After you inject them, replay is the production payoff, and a few habits still fail.
Where this shows up in machine coding
- Parking lot: entry and exit Instants let a test charge a frozen stay.
- Rate limiter: advance , then assert the next token is allowed.
- Task scheduler: due time is a value; the runner asks Clock.
- Calendar / booking: check overlap and expiry by advancing Clock, not waiting.
- Snake and ladder: pass so a test can land on a snake on purpose.
- URL shortener: sequential or scripted codes make short-URL tests assert exact ids.
Replay is the production payoff
If every ticket id is a random UUID and every fee reads the wall, an incident log cannot reconstruct "what would exit have charged at 10:07?" Injected and make that scenario replayable in a test and in a support tool.
- Log the Instant you used, not "now" implied by the log line timestamp.
- Log TicketId from IdGenerator so two parks stay T-1 and T-2 (see Numbers to know).
- Rate limiter and scheduler tests become advance, run tick(), then assert.
Still fails when…
- PricingStrategy reads the clock itself; fee math must receive exit time (see the wrong column).
- A test waits for TTL instead of advancing Clock (see the wrong column).
- create() still allocates its own id, so sequential TicketId asserts fail (see the wrong column).
Wrap-up
You have seen the hidden reads, the injected ports, and the frozen timeline. Use these prompts to check the ideas stuck, then follow the related pages.
Check yourself
- Rewrite a fee method that reads the wall clock so tests can freeze entry and exit (see Numbers to know).
- A reservation expires after a short hold. How do you assert expiry without waiting on the laptop clock?
- Why is
Clock.systemUTC()allowed in Main but not in Ticket?
Key takeaway
Inject , , and . Push Instant to the edge. Advance time in tests. Determinism is for production logs as much as for unit tests.