Chaturmind
LearnDSASystem DesignDevOpsEngineering 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

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

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
✓ FreeAdvanced· 8 min read

Producer-Consumer Class Design

Hand-rolling a bounded queue with wait/notify from first principles, extending it into a multi-consumer task-processing system, and the poison-pill technique for graceful shutdown.

Published September 23, 2026


Producer-Consumer Class Design

Every other lesson in this course reaches for BlockingQueue and moves on. This one deliberately builds the same mechanism from wait()/notify() — the exercise interviewers use specifically to check whether you understand what BlockingQueue is doing underneath, not just that you know to import it.

A bounded queue from scratch, with wait/notify

class BoundedQueue<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;

    BoundedQueue(int capacity) { this.capacity = capacity; }

    synchronized void put(T item) throws InterruptedException {
        while (queue.size() == capacity) { // WHILE, not if — see below
            wait(); // releases the monitor lock, unlike sleep() — see wait() vs sleep() in Thread Basics & Lifecycle
        }
        queue.add(item);
        notifyAll(); // wake any consumer(s) blocked in take()
    }

    synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();
        }
        T item = queue.poll();
        notifyAll(); // wake any producer(s) blocked in put()
        return item;
    }
}

Why while, not if, around the wait() call: this is the single most common bug in hand-written wait/notify code. notifyAll() wakes every waiting thread, not just one — if three consumers are all blocked in take()'s wait() and one item becomes available, notifyAll() wakes all three, but only one of them should actually get that item. With while, each woken thread re-checks the condition (queue.isEmpty()) before proceeding — two of the three find the queue empty again (the third one took it) and go back to waiting. With if, all three would proceed past the check and attempt queue.poll(), with two of them getting incorrect behavior (an empty poll, or worse, corrupting shared state if the check-then-act weren't otherwise protected).

Why notifyAll() rather than notify(): notify() wakes exactly one waiting thread, chosen arbitrarily by the JVM — with both producers and consumers potentially waiting on the same monitor (as here, since put() and take() share the same object's lock), notify() could wake a producer when a consumer was the one that needed to proceed (or vice versa), causing a spurious wakeup that immediately re-waits, or in pathological cases, a missed wakeup. notifyAll() is the safe default; notify() is a valid optimization only when you can prove every waiter is interchangeable.

Extending into a task-processing system

class TaskProcessingSystem {
    private final BoundedQueue<Task> queue;
    private final List<Thread> consumers = new ArrayList<>();
    private volatile boolean shuttingDown = false;

    void submitTask(Task task) throws InterruptedException { queue.put(task); }

    void startConsumers(int count, TaskHandler handler) {
        for (int i = 0; i < count; i++) {
            Thread consumer = new Thread(() -> {
                while (!shuttingDown) {
                    try {
                        Task task = queue.take();
                        if (task == POISON_PILL) break; // graceful shutdown signal — see below
                        handler.process(task);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            });
            consumers.add(consumer);
            consumer.start();
        }
    }
}

Multiple consumer threads pulling from one shared BoundedQueue is exactly why the queue itself needs to be correctly synchronized (as above) — with a single consumer, a simpler design might get away with less rigor, but concurrent take() calls from several consumer threads racing against put() calls from producers is precisely the scenario the while+notifyAll() combination is required for.

Graceful shutdown: the poison pill technique

The question "how do consumers know to stop when no more tasks are coming" has a clean, idiomatic answer: don't rely on a shared boolean flag alone (a consumer blocked inside take()'s wait() won't notice a flag change until it wakes up for an unrelated reason) — instead, enqueue a special poison pill sentinel value, one per consumer thread, once no more real tasks will be submitted:

void shutdown(int consumerCount) throws InterruptedException {
    shuttingDown = true;
    for (int i = 0; i < consumerCount; i++) {
        queue.put(POISON_PILL); // guarantees every blocked consumer eventually wakes and sees a pill
    }
}

Each consumer, on dequeuing a poison pill, breaks its loop and exits — since the pill goes through the exact same put()/take() path as real tasks, it correctly wakes a consumer that's currently blocked waiting for work, which a plain flag check never could (nothing wakes a thread parked inside wait() except another thread calling notify()/notifyAll() on that same monitor, or interruption).

Follow-up questions this topic invites — and their answers

Q: What would go wrong if put()/take() used if instead of while around wait(), even with only ONE consumer? A: With exactly one consumer and one producer it can appear to work by coincidence, but it's still incorrect in general — a spurious wakeup (the JVM is permitted to wake a waiting thread without an actual notify(), a documented possibility for wait()) would let the thread proceed past the check without the condition actually being true, which while protects against but if does not.

Q: Why does put() release the lock during wait() instead of just busy-waiting in a loop? A: Busy-waiting would hold the monitor lock the entire time (or burn CPU checking repeatedly without holding it, depending on implementation), preventing any consumer from ever acquiring the lock to call take() and drain the queue — wait()'s defining behavior is releasing the monitor while parked, which is precisely what allows another thread to make the progress this thread is waiting on.

Q: How does this hand-rolled version compare to just using ArrayBlockingQueue directly? A: Functionally equivalent for the core put/take semantics — ArrayBlockingQueue is battle-tested, likely has better internal optimizations, and should be what real production code uses (as Connection Pool Design does). This exercise's value is purely pedagogical: understanding wait/notify's while-loop and notifyAll() requirements directly is what lets you reason correctly about ANY hand-rolled synchronization code you might encounter, not just this one example.

Q: Is one poison pill per consumer always correct, or could consumer count change dynamically? A: The one-pill-per-consumer approach assumes a fixed, known consumer count at shutdown time — a system where consumers can be added/removed dynamically would need a different signal (e.g. each consumer checking a shared shutdown flag AFTER a bounded-timeout take() rather than an indefinite one, trading a small polling delay for not needing to know the exact consumer count in advance).

Previous

Connection Pool Design

Next

Movie Ticket Booking System

AI Tutor

Lesson: Producer-Consumer Class Design

Quick actions

AI responses can be inaccurate. Verify critical information.