Early Access: 87 spots left.

Claim
Low Level DesignInterview QuestionsDesign a Cron Job Scheduler (Virtual Clock)

Course content

Design a Cron Job Scheduler (Virtual Clock)

Medium·Tagsllddesigncronschedulervirtual-clock

Problem Statement

Design a job scheduler driven by a virtual clock. Time advances explicitly via `tick()` calls, not real wall time — this makes the scheduler deterministic and unit-testable, which is how production schedulers are tested anyway. Each `tick()` advances `currentTick` by exactly 1 and fires every job whose next-fire time has been reached. API contract ```java class CronScheduler { public CronScheduler(); // currentTick = 0 /** Schedule `task` to first fire at `currentTick + initialDelay`. * If `period > 0`, after each fire the job is rescheduled to fire every `period` ticks. * If `period == 0`, the job is one-shot and is marked completed after its first fire. * Throws IllegalArgumentException for null task, initialDelay < 1, or period < 0. Returns a unique jobId. / public String schedule(Runnable task, int initialDelay, int period); /** Cancel a job. Returns true if the job was active (and is now cancelled), false if the * jobId is unknown, already cancelled, or already completed (one-shot that fired). */ public boolean cancel(String jobId); /** Advance virtual time by 1 and fire every job whose next-fire time is now <= currentTick. * Cancelled and completed jobs are skipped. Each job fires at most once per tick. */ public void tick(); /* How many times the given job has fired so far. Returns 0 for unknown jobIds. / public int getRunCount(String jobId); public int getCurrentTick(); } ``` The validator runs three checks: 1. one_shot_fires_at_delay — `schedule(task, 3, 0)` with `currentTick == 0`. After 5 calls to `tick()`, the job has fired exactly once (at the tick that brought `currentTick` to 3). Cancelling a completed one-shot returns `false`. 2. periodic_fires_at_period — `schedule(task, 2, 3)` with `currentTick == 0`. After 8 ticks, the job has fired 3 times — at ticks 2, 5, and 8. `getCurrentTick()` is 8. 3. cancel_and_isolation — schedule two jobs with different periods; cancel one mid-flight; the other keeps firing on its own schedule. `getRunCount(cancelledId)` reflects only the fires that happened before cancel; `getRunCount(activeId)` keeps incrementing. The stub `CronScheduler` throws `UnsupportedOperationException` from every method, so it fails every check until you implement it.

Examples

Example 1
Input
var s = new CronScheduler(); var id = s.schedule(task, 3, 0); 5×s.tick(); s.getRunCount(id)
Output
"1"
Why
One-shot fires when currentTick reaches 3. Subsequent ticks (4, 5) do nothing because the job is now completed.
Example 2
Input
var id = s.schedule(task, 2, 3); 8×s.tick(); s.getRunCount(id)
Output
"3"
Why
First fire at tick 2. After firing, nextFire = 2 + 3 = 5. Then again at 5 (next = 8). Then 8. Three total.
Example 3
Input
s.cancel(id) after the job fires twice; further ticks
Output
"getRunCount stays at 2"
Why
Cancellation freezes the count. Cancelled jobs are skipped on every subsequent tick.

Constraints

  • Constructor: CronScheduler(). currentTick starts at 0.
  • schedule throws IllegalArgumentException for null task, initialDelay < 1, or period < 0.
  • First fire happens when currentTick reaches (scheduling time + initialDelay).
  • After each fire of a periodic job, the next fire is rescheduled to currentTick + period.
  • If period == 0 the job is one-shot — completed after first fire and skipped thereafter.
  • Each job fires at most once per tick().
  • cancel returns true only when it actively cancelled an in-flight job; false for unknown, already-cancelled, or already-completed.

Hints

Stuck? Reveal a nudge toward the right pattern, one step at a time.

Hint 1
Inner class JobRecord with: Runnable task, int nextFire, int period, int runCount, boolean cancelled, boolean completed. Use a HashMap<String, JobRecord>.
Hint 2
schedule: validate inputs first, then mint a jobId ('J-' + counter++) and put a fresh JobRecord with nextFire = currentTick + initialDelay.
Hint 3
tick: increment currentTick by 1, then iterate jobs.values(). For each job that is NOT cancelled and NOT completed, if nextFire <= currentTick: run the task, increment runCount, then either mark completed (period==0) or set nextFire = currentTick + period.
Hint 4
cancel: look up the job. Return false for unknown, already cancelled, or completed. Otherwise set cancelled=true and return true.
Hint 5
getRunCount: simple map lookup; return 0 for unknown ids. Don't remove completed jobs from the map — getRunCount needs to keep working after the one-shot has fired.