← Home
🏗Crack System Design with DSA AnimatorSystem DesignNEW
🏗 System Design›Concurrency & Threading

🧵Thread Pools & ExecutorService

Design a pool as bounded workers + bounded queue + explicit overload policy.

📌 Key IdeaA thread pool is a concurrency control system, not just a performance trick. The core design is worker lifecycle, queue behavior, rejection strategy, and shutdown semantics. ThreadPoolExecutor gives these knobs in production.
🧩

Problem it solves

  • Creating one thread per request can exhaust memory and scheduler capacity under load.
  • Without bounded queues/pools, traffic spikes cause latency blowups and cascading failures.
  • Thread pools provide controlled concurrency, backpressure, and predictable overload behavior.
✨

Why It Matters

  • Without bounds, task bursts turn into memory growth, high latency, and thread thrashing.
  • Interviewers expect you to explain overload behavior, not only Executors.newFixedThreadPool().
  • Most outages here are policy mistakes: unbounded queue, wrong rejection policy, or missing shutdown.
🖼

Visual

🧵 Thread pool design from scratch
submit(task)
   |
   +--> [state check: RUNNING?]
   |
   +--> [try spawn worker until coreSize]
   |           |
   |           +--> worker loop: take() -> run() -> repeat
   |
   +--> [else enqueue in bounded queue]
   |           |
   |           +--> if queue full: rejection policy
   |
shutdown() -> stop accept -> drain/finish -> terminate
✅

When to Use

  • Server workloads executing many independent short tasks.
  • You need predictable throughput and backpressure under burst traffic.
  • You want to integrate with Future/CompletableFuture and observability metrics.
🚫

When NOT to Use

  • Very long/blocking operations mixed with latency-sensitive tasks in the same pool.
  • CPU-bound jobs with oversized pools (context-switch overhead).
  • Workloads requiring per-task fairness/priority without queue design.
💻

Code Variations

1️⃣ Minimal custom thread pool (from scratch)java
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;

final class MiniThreadPool {
  private final BlockingQueue<Runnable> queue;
  private final List<Thread> workers = new ArrayList<>();
  private final AtomicBoolean running = new AtomicBoolean(true);

  MiniThreadPool(int nWorkers, int queueCapacity) {
    this.queue = new ArrayBlockingQueue<>(queueCapacity);
    for (int i = 0; i < nWorkers; i++) {
      Thread t = new Thread(this::workerLoop, "mini-pool-" + i);
      t.start();
      workers.add(t);
    }
  }

  void execute(Runnable task) {
    if (!running.get()) throw new RejectedExecutionException("pool stopped");
    if (!queue.offer(task)) {
      // simple rejection policy: fail fast
      throw new RejectedExecutionException("queue full");
    }
  }

  private void workerLoop() {
    while (running.get() || !queue.isEmpty()) {
      try {
        Runnable task = queue.poll(200, TimeUnit.MILLISECONDS);
        if (task != null) task.run();
      } catch (InterruptedException ignored) {
        Thread.currentThread().interrupt();
      } catch (Throwable t) {
        // log and continue; one bad task should not kill worker loop
      }
    }
  }

  void shutdown() {
    running.set(false);
    for (Thread t : workers) t.interrupt();
  }
}
Interview narrative: start with this skeleton, then discuss task submission race, richer rejection policies, and graceful termination.
2️⃣ Production-grade equivalent using ThreadPoolExecutorjava
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    8,                      // corePoolSize
    16,                     // maxPoolSize (burst)
    60, TimeUnit.SECONDS,   // keepAlive for extra threads
    new ArrayBlockingQueue<>(500),
    r -> {
      Thread t = new Thread(r);
      t.setName("orders-pool-" + t.threadId());
      return t;
    },
    new ThreadPoolExecutor.CallerRunsPolicy()
);

pool.prestartAllCoreThreads();
CallerRunsPolicy pushes back on producers during overload.
3️⃣ Correct shutdown lifecyclejava
pool.shutdown(); // stop accepting new tasks
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
  List<Runnable> dropped = pool.shutdownNow(); // emergency fallback
  // persist/requeue dropped tasks if needed
}
Always define behavior for in-flight and not-yet-started tasks.
📊

Design Decisions You Must Make

AspectOption AOption B
Queue typeUnbounded queue (simple, risky memory)Bounded queue (safe memory, needs rejection policy)
Worker countFixed pool (predictable)Core + burst max (adaptive, tune carefully)
Overload handlingDrop/reject fastCallerRuns/slow producer (backpressure)
ShutdownHard stop (shutdownNow) for emergenciesGraceful (shutdown + awaitTermination)
🔀

Common Confusions Cleared Up

Queue-first vs thread-first behavior

With a bounded queue, tasks fill queue before spawning beyond core threads (depending on config). This affects latency and memory profile under bursts.

Rejection policy is part of API contract

AbortPolicy throws; CallerRuns slows caller; Discard/DiscardOldest lose work. Choose intentionally per business criticality.

One pool per workload class

Separate pools for CPU-heavy, blocking I/O, and scheduled jobs. Mixing everything in one pool causes head-of-line blocking.

🎤

Interview Q&A

🧾

Quick Cheat Sheet

Design coreWorkers + bounded queue + state + rejection.
Golden ruleBound both threads and queue.
Overload strategyRejection policy is mandatory design choice.
Lifecycleshutdown + awaitTermination + fallback shutdownNow.
MetricsQueue depth, active threads, task latency, rejection count.
🔗

Related Topics