intermediate10 min read·Updated September 22, 2026
Circular Queue Full vs Empty Explained: Capacity 3 Ring Buffer Trace
Master circular queue full vs empty conditions using a capacity 3 ring buffer trace. Learn modulo arithmetic, wasted slots, and edge cases with Learnisim.
By Learnisim AI·Published September 22, 2026
LEARNING ARCSetup → Model in the example → Full trace → Twist → Edge cases → Apply
PREREQUISITES
- Basic array indexing
- Modulo arithmetic
- Linear queue fundamentals
Why
Why Linear Queues Fail and the Circular Buffer Solution
Phase 1: The Linear Queue Trap
Imagine you are building a fixed-size event log in memory with a capacity of just 3 items. You start with an empty array of size 3. As you enqueue items 1, 2, and 3, your buffer fills up completely. Now, you dequeue the first item to process it. In a naive linear queue, your
front pointer moves forward, leaving an empty slot at index 0.When you try to enqueue item 4, your
rear pointer has hit the physical end of the array (index 2). Even though you just freed up space by dequeuing item 1, your code thinks the queue is overflowing because rear == capacity. You are trapped at the end of your own array with unused memory sitting right behind you.To see this frustration in action, consider our locked working example: capacity 3, enqueue 1, 2, 3 (reaching maximum capacity), dequeue one item, and then attempt to enqueue 4. Without wrapping around to reuse that freed slot at index 0, your data structure breaks prematurely or forces you to shift every single element down—an expensive operation that destroys queue performance.
We need a way to make the end of the array wrap around to the beginning seamlessly.
Model
Mapping the Ring Buffer: Front, Rear, and Modulo Arithmetic
Phase 2: The Model
To prevent our queue from crawling toward the right edge of memory, we bend the array into a ring. In our capacity 3 buffer scenario, we allocate a fixed array of size 3 and manage it using two integer pointers: front and rear. Instead of letting these pointers grow infinitely, we wrap them around using modulo arithmetic. When a pointer reaches the end of the array, the modulo operator resets it back to index 0.
In this model, enqueue operations advance the rear pointer, while dequeue operations advance the front pointer. Let us visualize the index layout for our capacity 3 buffer:
When we begin our sequence by enqueuing and 3, the rear pointer marches across each index until the buffer is entirely populated. The core challenge in any circular queue full vs empty explained discussion is figuring out how to distinguish a state where the buffer has zero items from a state where it is packed to the brim, since both conditions can cause front and rear pointers to align.
Try this: Given a buffer of capacity 3 and rear = 2, calculate the new rear index after one enqueue operation using modulo arithmetic: rear = (rear + 1) % 3.
Compare
Comparing Fullness Strategies: Wasted Slot vs. Explicit Size Variable
Phase 3: Comparing Fullness Strategies
When implementing our capacity 3 ring buffer for the sequence enq 1, 2, 3, we hit the critical design question: how do we definitively know when the queue is full versus when it is empty? Because both conditions rely on , raw pointer values alone are ambiguous.
The first approach is the wasted slot method (often called the buffer-wrap method). Here, an array of size 4 is allocated to store 3 actual elements, reserving one cell as a permanent spacer. The queue is empty when , and it is full when . The trade-off is sacrificing one storage slot in exchange for avoiding a separate state counter.
The second approach is the explicit size counter method. Here, we keep a strict integer that increments on every enqueue and decrements on every dequeue. The queue is empty when , and it is full when . This requires zero wasted array slots, but introduces an extra variable to maintain across concurrent operations or multi-threaded environments.
Worked example
Tracing the Capacity 3 Ring Buffer: Enqueue, Dequeue, and Wrap
Phase 4: Worked Example
Let us trace our locked example from start to finish using a capacity 3 ring buffer with an explicit size variable. We maintain an array of size 3, with pointers
front = 0, rear = 0, and size = 0.Given
•Array capacity: 3
- Operations:
enqueue(1), enqueue(2), enqueue(3), dequeue(), enqueue(4), dequeue(), dequeue(), dequeue()Steps
1.enqueue(1): buffer[0] = 1. rear moves to . size becomes 1. State: buffer = [1, _, _], front = 0, rear = 1, size = 1.2.
enqueue(2): buffer[1] = 2. rear moves to . size becomes 2. State: buffer = [1, 2, _], front = 0, rear = 2, size = 2.3.
enqueue(3): buffer[2] = 3. rear moves to . size becomes 3. State: buffer = [1, 2, 3], front = 0, rear = 0, size = 3.4.
enqueue(4): Since size == capacity (), the queue is full. This operation is rejected or throws an overflow error.5.
dequeue(): Removes element at front (0), which is 1. front moves to . size decreases to 2. State: buffer = [_, 2, 3], front = 1, rear = 0, size = 2.6.
enqueue(4): Since size (2) capacity (3), buffer[0] = 4 (at index rear). rear moves to . size increases to 3. State: buffer = [4, 2, 3], front = 1, rear = 1, size = 3.7. Remaining
dequeue() calls: - First dequeue returns
2 (buffer[1]), front becomes 2, size becomes 2.- Second dequeue returns
3 (buffer[2]), front becomes 0 (wrapped around), size becomes 1.- Third dequeue returns
4 (buffer[0]), front becomes 1, size becomes 0.Result
The values are dequeued in the exact order . Notice how the fourth element (4) successfully reused index 0 after 1 was dequeued, wrapping rear back around without allocating new memory.Practice
Practice: Predicting State After Interleaved Operations
Phase 5: Practice
Now that you have traced the standard capacity 3 ring buffer execution where we enqueued , dequeued once, enqueued 4, and drained the buffer, it is time to test your mental model. Consider a slight variation on our running example using an explicit size variable strategy with capacity 3. Suppose we start with an empty buffer, enqueue (bringing size to 3, so it is full), then dequeue twice, and finally attempt to enqueue 40.
Walking through these steps carefully requires keeping track of how
front, rear, and size mutate after each operation. Remember that every successful enqueue advances rear using (rear + 1) % 3 and increments size, while every dequeue advances front using (front + 1) % 3 and decrements size. Working through this mini-trace will cement your ability to spot when a circular queue is full versus when it can accept new items without overwriting unread data.Buffer capacity = 3 (using explicit size variable)
Initial state: front = 0, rear = 0, size = 0, array = [_, _, _]
Initial state: front = 0, rear = 0, size = 0, array = [_, _, _]
Operations to trace:
1.enqueue(10)
2.enqueue(20)
3.enqueue(30)
4.dequeue()
5.dequeue()
6.enqueue(40)
Question: What are the exact values of front, rear, size, and the contents of the array after operation #6 completes?
Apply
Applying Circular Buffer Mechanics to Event Streaming and Rate Limiters
Phase 6: Apply
We have successfully traced our capacity 3 ring buffer through enqueue 1, 2, 3, a dequeue, enqueue 4, and final drainage. The core lesson from this walkthrough is that memory recycling via modulo arithmetic eliminates shifting costs entirely. But where does this exact fixed-size pattern appear outside of toy arrays? Consider a real-time rate limiter or an audio streaming packet buffer. In these systems, memory cannot be reallocated on the fly without causing latency spikes or garbage collection pauses.
Suppose you are designing a sliding-window rate limiter for an API endpoint that tracks a client's last 3 request timestamps. When a new request arrives, if the buffer is full (holding 3 recent timestamps), the oldest timestamp is overwritten or discarded to make room for the new one, exactly like our ring buffer wrapping around. Rather than shifting an array of timestamps in memory—which takes time—you maintain a
front pointer, a rear pointer, and advance them using (pointer + 1) % capacity.To test your mastery of this pattern, consider a system where your buffer capacity is strictly limited to 3 items, and you receive a burst of 5 sequential requests: . If you adapt our circular buffer logic to overwrite the oldest entry when the buffer is already full instead of rejecting the write, determine the final contents of the buffer array and the positions of
front and rear after all 5 requests have arrived.FAQ
How do you distinguish between a full and an empty circular queue?
An empty queue occurs when front equals rear. A full queue typically happens when (rear + 1) % capacity equals front (if using a wasted slot strategy), or when an explicit size counter equals capacity.
In our capacity 3 example, what happens when we enqueue 1, 2, and 3?
The queue fills up completely. Front points to 1 and rear points to 3. Attempting to enqueue a fourth element while full will result in a queue overflow condition.
Why use modulo arithmetic in a circular queue?
Modulo arithmetic (like
index % capacity) wraps pointers back to index 0 when they reach the end of the backing array, enabling continuous reuse of space.What is the time complexity of enqueue and dequeue operations in a circular queue?
Both enqueue and dequeue operate in O(1) time complexity because they only involve updating pointer indices and array assignments.