DEV Community

Vishal Aggarwal
Vishal Aggarwal

Posted on • Originally published at javalld.com

Java LLD Interview Prep

Java LLD: Designing a Scalable Job Scheduler Without Busy Polling

In my time at companies like Apple and Amazon, the "Job Scheduler" was a go-to interview question for senior roles. It isn't just about whether you can write a loop; it’s a litmus test for your understanding of resource management, thread safety, and low-level synchronization primitives. If you can't explain how to wake up a thread at the exact millisecond a job is due, you aren't passing the bar.

Why This Topic Matters

Designing a job scheduler is a foundational Low-Level Design (LLD) problem because it mirrors real-world systems like Crontab, Quartz, or the internal task executors used in microservices. In an interview, you aren't being asked to use ScheduledExecutorService; you are being asked to build it. The interviewer is looking for precision, efficiency, and a deep grasp of Java’s java.util.concurrent package.

The Mistake Most Candidates Make

The most common pitfall is "Busy Polling." I’ve seen countless candidates implement a while(true) loop that calls Thread.sleep(1000) and checks a queue for pending tasks.

This approach is flawed for two reasons:

  1. Latency: If a job is scheduled for T+50ms and your thread just started a 1000ms sleep, the job will be 950ms late.
  2. CPU Waste: If the queue is empty, the thread keeps waking up unnecessarily, consuming CPU cycles and draining battery/resources for no reason.

Experienced engineers know that the goal is to have the scheduler thread sleep for the exact duration until the next task is due, or until a new, higher-priority task is added.

The Right Approach: The Mental Model

To build a production-grade scheduler, you need to combine the Command Pattern with a Priority Queue and fine-grained synchronization.

Key Entities

  • Job: A wrapper around a Runnable that includes an executionTime (epoch timestamp).
  • Scheduler: A Singleton that manages the lifecycle of the background worker.
  • JobStore: A PriorityQueue (min-heap) that keeps the job with the earliest execution time at the head.
  • WorkerThread: The consumer thread that waits for jobs to become "due."

Why This Wins

Instead of generic sleeping, we use ReentrantLock and its associated Condition object. This allows us to implement a "wait-with-timeout" strategy. If a new job arrives that is scheduled earlier than the current head of the queue, we can signal the worker thread to wake up, recalculate its wait time, and go back to sleep—or execute the new job immediately.

The Core Insight: Smart Waiting

The magic happens in the worker's execution loop. We don't just wait; we wait conditionally. We use Condition.await(delay) to put the thread into a timed-waiting state. This is more efficient than Thread.sleep because it can be interrupted by a signal() call when a new job is pushed.

public void run() {
    while (running) {
        lock.lock();
        try {
            while (jobQueue.isEmpty()) {
                newJobCondition.await(); // Wait indefinitely for the first job
            }

            long currentTime = System.currentTimeMillis();
            Job nextJob = jobQueue.peek();
            long delay = nextJob.getExecutionTime() - currentTime;

            if (delay <= 0) {
                executorService.submit(jobQueue.poll()); // Execute due job
            } else {
                // Wait until the job is due OR a new job is added
                newJobCondition.await(delay, TimeUnit.MILLISECONDS);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Concurrency Angle: Thread Safety and Atomicity

When designing this, you must account for the Producer-Consumer race condition. While the worker thread is checking the peek() of the queue, a producer thread might be calling addJob().

If you use a standard PriorityQueue, it is not thread-safe. Even if you use a PriorityBlockingQueue, it doesn't solve the "wait-until-due" logic on its own. You need the ReentrantLock to ensure that the process of checking the time and deciding to wait is atomic.

Top comments (0)