Concurrency Patternshigh

Producer-Consumer Pattern

Decouple the production of data from its consumption using a shared bounded buffer. Producers enqueue work; consumers dequeue and process it. A BlockingQueue provides the coordination.

Memory anchor

Producer-Consumer = a sushi conveyor belt. The chef (producer) places plates on the belt (bounded buffer); diners (consumers) grab plates as they pass. If the belt is full, the chef waits. If it's empty, diners wait. Nobody starves, nobody drowns in fish.

Expected depth

Java: LinkedBlockingQueue with put() (blocks if full) and take() (blocks if empty) provides all the synchronization. Producers call put(); consumers call take() in a loop. Executor service with a work queue is a framework-level producer-consumer. Use cases: task queues, rate limiting (consumers process at fixed rate), log aggregation (producer logs events; consumer writes to file/network asynchronously).

Deep — senior internals

The bounded buffer solves two problems: (1) back-pressure — producer slows down if consumer is overwhelmed (queue full, put() blocks); (2) throughput — producer and consumer run at their natural rates, buffered by the queue. Unbounded queues (LinkedList) remove back-pressure and risk OutOfMemoryError under sustained overload. The design decisions: buffer size (too small → frequent blocking; too large → high latency before back-pressure kicks in); number of producers and consumers (tune empirically based on CPU vs IO bound); poison pill pattern (producer sends a sentinel value to signal consumers to shut down gracefully). At scale, the in-process BlockingQueue is replaced by a distributed message broker (Kafka, RabbitMQ) — the same producer-consumer semantics with durability, replay, and fan-out.

🎤Interview-ready answer

Producer-Consumer is the pattern for async work decoupling. I use Java's BlockingQueue: producers call queue.put(task) (blocks if queue is full, providing back-pressure); consumers call queue.take() in a loop (blocks if queue is empty, avoiding busy-waiting). In production I'd use an ExecutorService with a bounded work queue — it's the producer-consumer pattern with thread pool management built in. I always discuss: bounded vs unbounded queue (bounded gives back-pressure; unbounded risks OOM), and graceful shutdown via poison pill (producer sends N poison pills for N consumer threads, each consumer exits when it dequeues the pill).

Common trap

The most common mistake is using an unbounded queue — it works fine under normal load but accumulates tasks under overload until the JVM runs out of memory. Always bound the queue and design the back-pressure behavior explicitly.

Related concepts