Bounded Buffering

Bounded buffering limits how many items can wait in a buffer before producers must slow down or block. In Java, a bounded queue such as `ArrayBlockingQueue` provides a practical way to apply backpressure and prevent unbounded memory growth.

A bounded buffer sits between producers and consumers and holds a limited number of pending items. When the buffer reaches capacity, a producer cannot continue adding items indefinitely; it must wait, reject the item, or use another overflow policy depending on the API and design.

This is a form of backpressure: the downstream system's limited processing capacity influences the rate at which upstream work is accepted. Without a bound, a fast producer can accumulate an ever-growing backlog and eventually cause excessive memory use or an `OutOfMemoryError`.

In Java, `BlockingQueue` implementations provide useful primitives for producer-consumer designs. `ArrayBlockingQueue` has a fixed capacity, and operations such as `put()` wait when the queue is full while `take()` waits when it is empty.

The appropriate behavior when a queue is full depends on the application. Blocking may preserve work but can tie up threads; rejection may protect latency and resources but can lose or defer work. Production systems should choose an explicit policy and monitor queue depth, processing rate, and rejection or waiting behavior.

Key points

Interview questions

How would you choose the capacity of a bounded buffer?

Capacity should reflect workload characteristics, acceptable latency, memory limits, and the rate mismatch the system needs to absorb. A larger queue can tolerate short bursts but also permits more work to wait and may increase latency. A smaller queue limits memory and backlog more aggressively but can cause producers to block or tasks to be rejected sooner. In practice, capacity should be measured and tuned using realistic workloads rather than chosen solely from a fixed rule.

Why would you use a bounded queue instead of an unbounded queue?

A bounded queue puts an explicit limit on pending work. If producers are faster than consumers for a sustained period, an unbounded queue can accumulate more and more objects and consume excessive memory. A bounded queue forces the system to apply a policy when capacity is exhausted, such as blocking or rejecting work. This makes resource usage more predictable.

What happens when `put()` is called on a full `ArrayBlockingQueue`?

`put()` waits until space becomes available in the queue. Another thread must remove an element before the blocked producer can successfully insert its item. Because `put()` is interruptible, interruption can terminate the wait by throwing `InterruptedException`. This behavior is one way a bounded queue applies backpressure.

How does bounded buffering provide backpressure?

Suppose producers generate work faster than consumers can process it. A bounded queue eventually fills, preventing producers from continuing at the same rate. With blocking insertion, producers wait for consumers to free capacity; with rejection, new work is refused according to the chosen policy. In both cases, the buffer prevents the backlog from growing without a defined limit.

What is the difference between `put()`, `offer()`, and `take()` on a `BlockingQueue`?

`put()` inserts an element and waits if necessary for space. `offer()` attempts an insertion without waiting indefinitely and can report whether the insertion succeeded; timed overloads can wait for a bounded period. `take()` removes an element and waits if the queue is empty. These operations let producers and consumers express different waiting policies.