Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,068visitors

Built for engineers who want to go deep.


← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
Chaturmind
← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
HomeLearnSystem DesignLow-Level Design & Design PatternsMachine Coding Exercises
✓ FreeIntermediate· 8 min read

Design a Job Scheduler

Job/Schedule/JobExecutor/JobQueue classes, a single-node priority-queue-by-next-run-time implementation, and the extension path toward a distributed scheduler.

Published September 23, 2026


Design a Job Scheduler

Core classes

interface Job { void execute(); }

class Schedule {
    Instant nextRunTime;
    Duration recurrenceInterval; // null for a one-time job
}

class ScheduledJob {
    Job job;
    Schedule schedule;
    String id;
}

Advertisement

Single-node implementation: priority queue ordered by next-run-time

class JobQueue {
    private final PriorityQueue<ScheduledJob> queue = new PriorityQueue<>(
        Comparator.comparing(sj -> sj.schedule.nextRunTime) // always exposes the SOONEST job at the top
    );

    synchronized void schedule(ScheduledJob job) { queue.offer(job); }
    synchronized ScheduledJob peekNext() { return queue.peek(); }
    synchronized ScheduledJob pollNext() { return queue.poll(); }
}

class JobExecutor {
    private final JobQueue jobQueue;
    private volatile boolean running = true;

    void run() {
        while (running) {
            ScheduledJob next = jobQueue.peekNext();
            if (next == null) { sleepBriefly(); continue; }

            long waitMs = Duration.between(Instant.now(), next.schedule.nextRunTime).toMillis();
            if (waitMs > 0) { sleepFor(waitMs); continue; } // not due yet — wait and re-check

            jobQueue.pollNext().job.execute();
            if (next.schedule.recurrenceInterval != null) {
                next.schedule.nextRunTime = Instant.now().plus(next.schedule.recurrenceInterval);
                jobQueue.schedule(next); // re-insert — the priority queue naturally re-sorts it to its new position
            }
        }
    }
}

A priority queue ordered by nextRunTime means "what's the next job to run" is always an O(log n) peek/poll away, regardless of how many jobs are scheduled — no need to scan the full job list on every tick. Recurring jobs re-insert themselves with an updated nextRunTime after each execution, and the priority queue automatically places them correctly relative to every other pending job — no manual re-sorting logic needed.

Extending toward the distributed scheduler

This single-node design has an implicit, easy-to-miss assumption: exactly one JobExecutor process exists. Design: Distributed Task Scheduler's actual hard problems — finding "jobs due now" efficiently across a much larger job set (a time-bucketed structure instead of one in-memory priority queue), distributing execution across multiple worker nodes, and specifically avoiding double-execution of the same job when multiple workers could pick it up — are the direct extension points from this LLD foundation. The core data model (Job, Schedule, next-run-time ordering) transfers directly; what changes at distributed scale is where that priority ordering lives (a shared, coordinated store instead of one process's in-memory heap) and how workers coordinate to ensure only one of them actually claims and runs a given due job.

A correct single-node scheduler loop

The priority-queue design is right. The difficult part is the waiting loop: sleeping until the next job is due, but waking early if a new job is scheduled sooner. Busy-polling wastes CPU, and a plain sleep misses newly added urgent jobs. A DelayQueue (or a lock and condition) handles both:

final class ScheduledJob implements Delayed {
    final String id; final Job job; final Schedule schedule;
    final Instant runAt; final int attempt;

    ScheduledJob(String id, Job job, Schedule schedule, Instant runAt, int attempt) {
        this.id = id; this.job = job; this.schedule = schedule; this.runAt = runAt; this.attempt = attempt;
    }
    public long getDelay(TimeUnit unit) {
        return unit.convert(Duration.between(Instant.now(), runAt));
    }
    public int compareTo(Delayed o) {
        return runAt.compareTo(((ScheduledJob) o).runAt);
    }
}

final class Scheduler {
    private final DelayQueue<ScheduledJob> queue = new DelayQueue<>();   // take() blocks until the earliest job is due
    private final ExecutorService workers;
    private final Map<String, Boolean> cancelled = new ConcurrentHashMap<>();

    Scheduler(int threads) { this.workers = Executors.newFixedThreadPool(threads); }

    void schedule(ScheduledJob j) { queue.put(j); }                     // wakes the dispatcher if j is earlier
    void cancel(String jobId) { cancelled.put(jobId, true); }            // lazy cancellation

    void runDispatcher() throws InterruptedException {
        while (!Thread.currentThread().isInterrupted()) {
            ScheduledJob due = queue.take();
            if (cancelled.remove(due.id) != null) continue;               // skip cancelled jobs
            workers.submit(() -> runOnce(due));                           // never run jobs on the dispatcher thread
        }
    }

    private void runOnce(ScheduledJob j) {
        try {
            j.job.execute();
            scheduleNext(j, 1);                                          // success: next occurrence, reset attempts
        } catch (Exception e) {
            if (j.attempt < 3) {                                         // transient failure: retry with backoff
                Duration backoff = Duration.ofSeconds((long) Math.pow(2, j.attempt) * 5);
                schedule(new ScheduledJob(j.id, j.job, j.schedule, Instant.now().plus(backoff), j.attempt + 1));
            } else {
                alert(j, e);                                             // give up this run…
                scheduleNext(j, 1);                                      // …but keep the recurrence alive
            }
        }
    }

    private void scheduleNext(ScheduledJob j, int attempt) {
        j.schedule.nextAfter(j.runAt).ifPresent(next ->                  // from the PLANNED time: no drift
            schedule(new ScheduledJob(j.id, j.job, j.schedule, next, attempt)));
    }
}

Design choices to explain:

  • DelayQueue.take() blocks until the head's delay reaches zero, and inserting an earlier job makes it re-evaluate. There's no polling and no missed wake-ups.
  • The dispatcher only dispatches. Running jobs on a separate worker pool means a long job can't delay other due jobs.
  • Scheduling the next run from the planned time, not from "now", avoids drift: an hourly job stays on the hour, even if individual runs take a few seconds.
  • Failures don't kill the recurrence. A job that fails three times raises an alert, but still runs at its next scheduled time.
  • Lazy cancellation avoids an O(n) removal from the heap. Cancelled entries are skipped when they come due.

Misfires and overlapping runs

Two policies every scheduler must define:

  1. Misfire policy: the process was down at 02:00, when the nightly job was due. On restart, should it run once immediately (fire once), run every missed occurrence (catch up, rarely wanted), or skip to the next scheduled time (do nothing)? Quartz exposes exactly these options. Persist each job's lastRunAt, so the decision is possible after a restart.
  2. Concurrency policy: if a run is still going when the next one is due, should they overlap, or should the new one be skipped or queued? Most business jobs (billing, report generation) must not overlap. Track "running" per job ID, and skip or delay if it's set. Spring's @Scheduled(fixedDelay=…) avoids overlap by design, and fixedRate can also queue runs back to back.

Cron expressions as a Schedule

Schedule.nextAfter(instant) is the only thing the scheduler needs from a schedule, which makes schedules pluggable (Strategy):

  • fixed interval: runAt + interval;
  • cron: parse "0 0 2 * * MON-FRI" and compute the next matching time, in an explicit time zone;
  • one-off: return empty after the first run.

Time zones and daylight-saving changes matter. A job at 02:30 local time doesn't exist on the night clocks jump forward, and happens twice when they fall back. Scheduling in UTC avoids this. When business rules demand local time, the cron library must define the behaviour.

Follow-up questions this topic invites — and their answers

Q: Why does peekNext() exist as a separate method rather than always calling pollNext() and re-inserting if not yet due? A: Peeking avoids unnecessary remove-then-reinsert churn on the priority queue for a job that isn't due yet — pollNext() should only be called once a job is actually confirmed ready to execute, keeping the queue's O(log n) operations reserved for genuine state changes rather than repeated no-op poll/re-insert cycles on every check.

Q: What happens if execute() throws an exception for a recurring job — does it still get rescheduled? A: Worth deciding explicitly and stating the choice: rescheduling regardless of failure keeps the job running on its normal cadence despite one bad execution (appropriate for most cases), while NOT rescheduling on failure requires manual intervention to resume — the code above reschedules unconditionally, which is a specific, statable design decision, not an oversight.

Q: How would you avoid double-execution even on a SINGLE node with multiple executor threads? A: The synchronized JobQueue methods already prevent two threads from both polling the same ScheduledJob instance — pollNext() atomically removes it from the queue, so a second thread's pollNext() call simply won't see it, which is the single-node analog of the distributed double-execution problem, solved here by ordinary mutual exclusion rather than a distributed lock.

Q: Could missed jobs (the process was down when a job was due) be handled by this design? A: Not by the structure shown — a job whose nextRunTime already passed while the process was down would need explicit 'catch-up' handling (run immediately on restart, or skip to the next scheduled occurrence) rather than assuming JobExecutor.run() is always live at the exact moment a job comes due, which is a real gap worth naming when discussing production readiness.

Q: Why compute the next run from the planned time instead of the completion time? A: Computing from completion adds each run's duration to the schedule, so an "every hour" job drifts later and later. Computing from the planned time keeps it aligned. Use completion-based delays only when you deliberately want a gap between runs.

Q: How should a scheduler handle jobs missed while it was down? A: Persist each job's last run time, and apply an explicit misfire policy on startup: run once now, run all missed occurrences, or skip to the next one. The right choice depends on the job. A daily report usually needs one catch-up run, while a cache refresh can simply skip.

Q: How do you prevent a long-running job from overlapping with its next run? A: Track in-progress runs per job ID (in memory for one node, or as a distributed lock or database row for many), and skip or postpone a run while the previous one is still active.

Advertisement

Previous

Design a Recommendation Engine

Next

Design a Circuit Breaker

AI Tutor

Lesson: Design a Job Scheduler

Quick actions

AI responses can be inaccurate. Verify critical information.