Definition
- Circular queue (ring buffer) – a fixed-size array where
frontandrearindex pointers wrap around using the modulo operator, reusing freed slots. - Enqueue – writes the element at
items[rear], then advancesrear = (rear + 1) % capacity. - Dequeue – reads the element at
items[front], then advancesfront = (front + 1) % capacity. - O(1) enqueue and dequeue – no shifting of elements is ever needed.
- Count variable – a separate
countresolves the ambiguity whenfront == rear, which could mean empty or completely full.
Think of a row of numbered pigeon-holes on the wall at the National Bank of India. Rajesh's request goes into hole 0, Priya's into hole 1. A "front" sticky note points to the oldest unprocessed hole and a "rear" sticky note points to the next empty hole. When a teller processes the oldest request, the front note moves forward. When the rear reaches the end, it wraps back to the start if earlier holes are free. Nobody physically moves the papers, only the sticky notes move.
Understanding the Concepts
What is an array-backed queue?
A queue is a first-in, first-out (FIFO) data structure. An array-backed queue stores elements in a fixed-size array and uses two index pointers, front and rear, to track the head and tail of the line. Enqueue adds at the rear; dequeue removes from the front.
Why use a circular design instead of shifting?
A naive array queue removes from the front by shifting every remaining element one step left, which costs O(n) per dequeue. The circular queue avoids shifting by advancing the front pointer instead, wrapping around with modulo so freed slots at the front are reused by the rear.
Naive (shift on dequeue): [_, B, C, D] → shift → [B, C, D, _] O(n) Circular (advance pointer): front moves 0→1→2→...→wrap O(1)
How do you implement it? (with code)
package com.bank.service;
import java.math.BigDecimal;
import java.util.NoSuchElementException;
public class ChequeQueue {
private final BigDecimal[] items;
private final int capacity;
private int front;
private int rear;
private int count;
public ChequeQueue(int capacity) {
this.capacity = capacity;
this.items = new BigDecimal[capacity];
this.front = 0;
this.rear = 0;
this.count = 0;
}
public void enqueue(BigDecimal amount) {
if (count == capacity) {
throw new IllegalStateException("Queue full: cannot accept more cheques");
}
items[rear] = amount;
rear = (rear + 1) % capacity;
count++;
}
public BigDecimal dequeue() {
if (count == 0) {
throw new NoSuchElementException("Queue empty: no cheques to process");
}
BigDecimal value = items[front];
items[front] = null; // help GC
front = (front + 1) % capacity;
count--;
return value;
}
public BigDecimal peek() {
if (count == 0) {
throw new NoSuchElementException("Queue empty");
}
return items[front];
}
public boolean isEmpty() {
return count == 0;
}
public boolean isFull() {
return count == capacity;
}
public int size() {
return count;
}
public static void main(String[] args) {
ChequeQueue queue = new ChequeQueue(3);
queue.enqueue(new BigDecimal("5000"));
queue.enqueue(new BigDecimal("12000"));
queue.enqueue(new BigDecimal("7500"));
System.out.println("Dequeued: " + queue.dequeue());
System.out.println("Peek: " + queue.peek());
queue.enqueue(new BigDecimal("9000")); // reuses freed slot
System.out.println("Size: " + queue.size());
}
}
Dequeued: 5000
Peek: 12000
Size: 3
Why keep a separate count variable?
When front == rear, you cannot tell if the queue is empty or completely full just from the pointers. A separate count removes that ambiguity cleanly. An alternative is to waste one slot (capacity minus one usable slots), but the count approach is simpler and wastes no space.
| Approach | Empty check | Full check | Slots usable |
|---|---|---|---|
| Count variable | count == 0 | count == capacity | All slots |
| Waste one slot | front == rear | (rear+1) % cap == front | capacity - 1 |
How does the modulo wrap-around work?
The modulo operator % maps any integer back into the range [0, capacity). When rear is at the last slot (index capacity - 1), (rear + 1) % capacity wraps it back to 0. This is what makes the array behave like a ring.
Capacity = 4, slots: [0, 1, 2, 3] rear at 3: (3 + 1) % 4 = 0 → wraps to slot 0 front at 3: (3 + 1) % 4 = 0 → wraps to slot 0
What about a resizable queue?
If the queue must grow beyond the fixed capacity, double the backing array and copy elements in their logical order (from front to rear, unwrapping the ring). This gives amortized O(1) enqueue, similar to ArrayList's growth strategy.
What are the edge cases?
- Enqueue on full queue – throw
IllegalStateException. - Dequeue on empty queue – throw
NoSuchElementException. - Single element – enqueue and dequeue work; front and rear both advance.
- Wrap-around – after several enqueue/dequeue cycles, front and rear cross the array boundary correctly.
What should a senior engineer know?
In production, this exact ring buffer design is used in logging frameworks (like LMAX Disruptor) and network buffers because it never allocates after construction, which avoids garbage collection pauses. Setting the element to null on dequeue helps the garbage collector reclaim the object, which matters for long-lived queues holding large objects.
What are common interview follow-up questions?
- Can you implement a deque (double-ended queue)? – Yes, add methods to enqueue at front and dequeue from rear, both using modulo.
- What about thread safety? – Use locks or atomic operations on the count, front, and rear.
- Why not use LinkedList? – Array is more cache-friendly and avoids per-node allocation overhead.
Visual Overview
┌────────────────────────────────────────────────────┐ │ Circular Queue (Ring Buffer) │ ├────────────────────────────────────────────────────┤ │ │ │ Slots: [ 0 ] [ 1 ] [ 2 ] [ 3 ] │ │ ↑ ↑ │ │ front rear │ │ │ │ Enqueue 5000: items[1]=5000, rear=(1+1)%4=2 │ │ Enqueue 7500: items[2]=7500, rear=(2+1)%4=3 │ │ Dequeue: read items[0], front=(0+1)%4=1 │ │ Enqueue 9000: items[3]=9000, rear=(3+1)%4=0 │ │ ↑ wraps! │ │ │ │ front chases rear around the ring │ │ count tracks how many slots are occupied │ └────────────────────────────────────────────────────┘
Key Takeaway
To implement a queue using an array, use a fixed-size array together with two index pointers, front pointing to the oldest element and rear pointing to the next free slot, plus a count of how many elements are stored. Enqueue writes at the rear and advances it; dequeue reads from the front and advances it. Both operations are O(1).
The key trick is making the queue circular. A naive array queue shifts elements on every dequeue, costing O(n). Instead, advance the pointers and wrap them with the modulo operator, so freed slots at the front get reused by the rear. This makes both operations constant time.
The separate count variable resolves the ambiguity when front equals rear, which could mean the queue is empty or completely full. Without it, you would need to waste one slot or add a boolean flag.
In a banking project at the National Bank of India, a bounded circular queue is used for the cheque-clearing station that can hold only a fixed number of cheques at once. Cheques enter at the rear and the clerk processes them from the front, and when the tray is full the system applies back-pressure.
Remember one sentence: A circular array queue wraps front and rear pointers with modulo, making enqueue and dequeue both O(1) without ever shifting elements.
---