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
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
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
}
Priority
public enum Priority {
HIGH,
MEDIUM,
LOW
}
Task Interface
public interface Task {
void execute();
}
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
}
Task Comparator
Tasks should be executed based on:
- Earliest execution time
- 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();
}
}
Scheduler Queue
PriorityBlockingQueue<ScheduledTask>
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);
}
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
}
}
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());
}
Example
Task emailTask =
() -> System.out.println("Sending Email");
ScheduledTask task =
new ScheduledTask(
"Email",
emailTask,
Instant.now().plusSeconds(5),
Priority.HIGH
);
scheduler.schedule(task);
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
Retry Strategy
Implement a RetryPolicy interface:
public interface RetryPolicy {
boolean shouldRetry(int attempt);
long nextDelay(int attempt);
}
Possible implementations:
- Fixed Delay
- Exponential Backoff
- Randomized Backoff
Scheduling Strategies
One-Time
Runs exactly once.
Fixed Delay
Task finishes
↓
Wait 10 sec
↓
Run again
Fixed Rate
0 sec
10 sec
20 sec
30 sec
Runs at a constant interval regardless of task duration.
Cron
0 0 * * *
Every hour
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 aConcurrentHashMap<taskId, Task>with the priority queue to achieve faster lookups and state management.
Common Interview Follow-up Questions
- Why use a
PriorityBlockingQueueinstead of a regularPriorityQueue? - How would you support millions of scheduled tasks efficiently?
- How would you implement cron expressions?
- How would you prevent duplicate execution in a distributed system?
- How would you handle scheduler crashes and task recovery?
- How would you guarantee "exactly once" or "at least once" execution semantics?
- How would you implement exponential backoff retries?
- How would you support task dependencies (DAGs)?
- How would you ensure fairness when multiple tasks have the same execution time?
- When would you choose
ScheduledExecutorServiceover a custom scheduler?
Key Interview Takeaways
- Use
PriorityBlockingQueueto efficiently order tasks by execution time. - Use
ExecutorServiceorScheduledExecutorServicefor 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)