DEV Community

Dev Cookies
Dev Cookies

Posted on

Task Scheduler Low-Level Design (LLD) in Java

Complete Interview Guide | SOLID Principles | Design Patterns | Production-Ready Architecture


Introduction

A Task Scheduler is a common Low-Level Design (LLD) interview question asked by companies like Amazon, Microsoft, Google, Atlassian, Uber, Walmart, and Oracle.

It evaluates a candidate's understanding of:

  • Object-Oriented Design (OOD)
  • SOLID Principles
  • Design Patterns
  • Java Concurrency
  • Scheduling Algorithms
  • Thread Pools
  • Extensibility
  • Production System Design

Real-world schedulers include:

  • Spring @Scheduled
  • Quartz Scheduler
  • Linux Cron
  • Kubernetes CronJobs
  • Windows Task Scheduler
  • Airflow
  • Jenkins Scheduler

Problem Statement

Design a Task Scheduler that can:

  • Schedule a task to run at a specific time
  • Schedule recurring tasks
  • Cancel scheduled tasks
  • Pause and resume tasks
  • Execute tasks asynchronously
  • Support priorities
  • Retry failed tasks
  • Be thread-safe
  • Be easily extensible

Functional Requirements

Core Features

  • Schedule one-time tasks
  • Schedule recurring tasks
  • Cancel tasks
  • Pause tasks
  • Resume tasks
  • Retry failed tasks
  • Execute multiple tasks concurrently

Optional Features

  • Cron expressions
  • Task dependencies
  • Distributed scheduling
  • Persistence
  • Monitoring
  • Metrics
  • Notifications

Non-Functional Requirements

  • High Throughput
  • Low Latency
  • Thread Safe
  • Fault Tolerant
  • Scalable
  • Extensible
  • Maintainable

Real-World Use Cases

  • Email Scheduler
  • Payment Retry
  • Notification Service
  • Data Cleanup Jobs
  • ETL Pipelines
  • Report Generation
  • Database Backup
  • Order Expiry
  • Subscription Renewal
  • Cache Refresh

High-Level Architecture

                 Client
                    │
                    ▼
          TaskSchedulerService
                    │
     ┌──────────────┼──────────────┐
     ▼              ▼              ▼
 Scheduler      Task Queue      TaskStore
     │
     ▼
 Priority Queue
     │
     ▼
 Worker Threads
     │
     ▼
 TaskExecutor
     │
     ▼
 Runnable Task
Enter fullscreen mode Exit fullscreen mode

Core Components

Component Responsibility
Task Represents a scheduled job
Scheduler Decides when to execute
Executor Executes tasks
Queue Holds pending tasks
Worker Picks and executes tasks
Retry Policy Handles failures
Trigger Determines execution time

Object Model

                +--------------------+
                |   TaskScheduler    |
                +--------------------+
                | schedule()         |
                | cancel()           |
                | pause()            |
                | resume()           |
                +---------+----------+
                          |
                          ▼
                 +----------------+
                 | ScheduledTask  |
                 +----------------+
                 | id             |
                 | name           |
                 | trigger        |
                 | runnable       |
                 | status         |
                 | priority       |
                 +----------------+
                          |
          ---------------------------------
          |               |               |
          ▼               ▼               ▼
     OneTime         FixedDelay      CronTask
Enter fullscreen mode Exit fullscreen mode

Design Patterns Used

Pattern Purpose
Strategy Different scheduling strategies
Factory Create triggers
Command Represent executable task
Observer Task completion notification
State Task lifecycle
Singleton Scheduler instance
Builder Create complex task objects
Template Method Common execution flow

Class Design

Task Status

public enum TaskStatus {

    SCHEDULED,
    RUNNING,
    PAUSED,
    COMPLETED,
    FAILED,
    CANCELLED

}
Enter fullscreen mode Exit fullscreen mode

Priority

public enum Priority {

    HIGH,
    MEDIUM,
    LOW

}
Enter fullscreen mode Exit fullscreen mode

Task Interface

public interface Task {

    void execute();

}
Enter fullscreen mode Exit fullscreen mode

Scheduled Task

import java.time.Instant;
import java.util.UUID;

public class ScheduledTask {

    private final String id;

    private final String name;

    private final Task task;

    private Instant nextExecutionTime;

    private Priority priority;

    private TaskStatus status;

    public ScheduledTask(
            String name,
            Task task,
            Instant nextExecutionTime,
            Priority priority) {

        this.id = UUID.randomUUID().toString();
        this.name = name;
        this.task = task;
        this.nextExecutionTime = nextExecutionTime;
        this.priority = priority;
        this.status = TaskStatus.SCHEDULED;
    }

    // getters

}
Enter fullscreen mode Exit fullscreen mode

Task Comparator

Tasks should be executed based on:

  1. Earliest execution time
  2. Highest priority
import java.util.Comparator;

public class TaskComparator
        implements Comparator<ScheduledTask> {

    @Override
    public int compare(
            ScheduledTask t1,
            ScheduledTask t2) {

        int time =
                t1.getNextExecutionTime()
                  .compareTo(t2.getNextExecutionTime());

        if (time != 0)
            return time;

        return t2.getPriority()
                 .ordinal()
                - t1.getPriority()
                    .ordinal();

    }

}
Enter fullscreen mode Exit fullscreen mode

Scheduler Queue

PriorityBlockingQueue<ScheduledTask>
Enter fullscreen mode Exit fullscreen mode

Why?

  • Thread-safe
  • Automatically sorted
  • Multiple producers
  • Multiple consumers

Scheduler Service

public interface TaskScheduler {

    void schedule(ScheduledTask task);

    void cancel(String taskId);

    void pause(String taskId);

    void resume(String taskId);

}
Enter fullscreen mode Exit fullscreen mode

Implementation

import java.util.concurrent.*;

public class TaskSchedulerImpl
        implements TaskScheduler {

    private final PriorityBlockingQueue<ScheduledTask> queue =
            new PriorityBlockingQueue<>(
                    100,
                    new TaskComparator());

    private final ExecutorService workers =
            Executors.newFixedThreadPool(5);

    @Override
    public void schedule(
            ScheduledTask task) {

        queue.offer(task);

    }

    @Override
    public void cancel(String id) {

        queue.removeIf(
                t -> t.getId().equals(id));

    }

    @Override
    public void pause(String id) {

        // update status

    }

    @Override
    public void resume(String id) {

        // update status

    }

}
Enter fullscreen mode Exit fullscreen mode

Worker Thread

while(true){

    ScheduledTask task = queue.take();

    if(task.getNextExecutionTime()
            .isAfter(Instant.now())){

        queue.offer(task);

        Thread.sleep(100);

        continue;
    }

    workers.submit(
            () -> task.getTask().execute());

}
Enter fullscreen mode Exit fullscreen mode

Example

Task emailTask =
        () -> System.out.println("Sending Email");

ScheduledTask task =
        new ScheduledTask(

                "Email",

                emailTask,

                Instant.now().plusSeconds(5),

                Priority.HIGH

        );

scheduler.schedule(task);
Enter fullscreen mode Exit fullscreen mode

Thread Safety

Use:

  • PriorityBlockingQueue
  • ConcurrentHashMap
  • AtomicInteger
  • ExecutorService
  • ScheduledExecutorService
  • ReentrantLock (when needed)

Avoid:

  • Global synchronization
  • Manual thread management
  • Shared mutable state

Task Lifecycle

             CREATED
                 │
                 ▼
           SCHEDULED
                 │
                 ▼
             WAITING
                 │
                 ▼
             RUNNING
          ┌──────┴──────┐
          ▼             ▼
     COMPLETED      FAILED
                         │
                         ▼
                      RETRYING
                         │
                         ▼
                     COMPLETED
Enter fullscreen mode Exit fullscreen mode

Retry Strategy

Implement a RetryPolicy interface:

public interface RetryPolicy {

    boolean shouldRetry(int attempt);

    long nextDelay(int attempt);

}
Enter fullscreen mode Exit fullscreen mode

Possible implementations:

  • Fixed Delay
  • Exponential Backoff
  • Randomized Backoff

Scheduling Strategies

One-Time

Runs exactly once.


Fixed Delay

Task finishes

↓

Wait 10 sec

↓

Run again
Enter fullscreen mode Exit fullscreen mode

Fixed Rate

0 sec

10 sec

20 sec

30 sec
Enter fullscreen mode Exit fullscreen mode

Runs at a constant interval regardless of task duration.


Cron

0 0 * * *

Every hour
Enter fullscreen mode Exit fullscreen mode

Production Improvements

Instead of a custom scheduler loop, prefer:

  • ScheduledExecutorService
  • Quartz Scheduler
  • Spring Scheduler
  • Kubernetes CronJobs
  • Airflow (workflow scheduling)

Additional enhancements:

  • Persistent task storage
  • Leader election for distributed deployments
  • Metrics (Micrometer/Prometheus)
  • Dead Letter Queue (DLQ) for repeatedly failing tasks
  • Graceful shutdown
  • Dynamic worker pool sizing
  • Task timeouts and cancellation
  • Distributed locks for singleton execution

SOLID Principles

Principle Application
Single Responsibility Task, Scheduler, Executor, and RetryPolicy each have one responsibility.
Open/Closed Add new trigger or retry strategies without modifying scheduler logic.
Liskov Substitution Any Task or RetryPolicy implementation can be substituted.
Interface Segregation Small, focused interfaces (Task, TaskScheduler, RetryPolicy).
Dependency Inversion Scheduler depends on abstractions instead of concrete task implementations.

Complexity Analysis

Operation Complexity
Schedule Task O(log n)
Fetch Next Task O(log n)
Cancel Task O(n)*
Execute Task O(1) + task execution time

*Using only a PriorityBlockingQueue, cancellation requires searching the queue. A production implementation typically combines a ConcurrentHashMap<taskId, Task> with the priority queue to achieve faster lookups and state management.


Common Interview Follow-up Questions

  1. Why use a PriorityBlockingQueue instead of a regular PriorityQueue?
  2. How would you support millions of scheduled tasks efficiently?
  3. How would you implement cron expressions?
  4. How would you prevent duplicate execution in a distributed system?
  5. How would you handle scheduler crashes and task recovery?
  6. How would you guarantee "exactly once" or "at least once" execution semantics?
  7. How would you implement exponential backoff retries?
  8. How would you support task dependencies (DAGs)?
  9. How would you ensure fairness when multiple tasks have the same execution time?
  10. When would you choose ScheduledExecutorService over a custom scheduler?

Key Interview Takeaways

  • Use PriorityBlockingQueue to efficiently order tasks by execution time.
  • Use ExecutorService or ScheduledExecutorService for concurrent execution instead of manually creating threads.
  • Separate scheduling, execution, retry, and storage into distinct components to follow SOLID principles.
  • Design for extensibility with Strategy, Command, and Factory patterns.
  • Discuss production concerns such as persistence, distributed scheduling, retries, monitoring, and graceful shutdown to demonstrate senior-level system design thinking.

This design provides a clean, interview-friendly foundation that can be extended into a production-grade scheduler similar in architecture to Quartz or Spring's scheduling infrastructure.

Top comments (0)