A strong answer
A ring buffer is a fixed-size array used as a queue. It keeps the index of the oldest item, head, and the number of items, size. Enqueue writes at (head + size) % capacity and increments size; dequeue reads buf[head], advances head = (head + 1) % capacity and decrements size. The remainder makes the end of the array wrap around to its start, so slots freed by dequeues are reused. Every operation is O(1), nothing is allocated after creation, and items never move once written.
If the buffer stores head and tail instead, head == tail means either empty or full; implementations either keep a count or leave one slot unused. A power-of-two capacity lets % capacity become & (capacity − 1), one instruction.
When the buffer is full, the policy is a design choice: grow it (allocate double and copy the items in queue order, as Java's ArrayDeque does), block the producer (backpressure), overwrite the oldest item (logs of recent events, audio), or reject the new item.
Systems use ring buffers because they are predictable: fixed memory, no allocation, cache-friendly sequential access, and a natural fit for a producer and a consumer working at different paces. Network cards receive packets into rings shared with the kernel, Linux's io_uring uses submission and completion rings, audio drivers stream samples through them, and the LMAX Disruptor passes messages between threads with one. With exactly one producer and one consumer, each index is written by only one side, so the buffer can be made thread-safe with atomic loads and stores instead of a lock.
What the interviewer is listening for
- The index arithmetic and why nothing shifts.
- The full-versus-empty ambiguity and its fixes.
- Overflow policies and backpressure.
- Real uses, and why fixed memory matters there.
Common mistakes
- Shifting items on dequeue, which defeats the purpose.
- Copying a wrapped buffer straight into a larger array, which scrambles the order.
- Forgetting to define what happens when the buffer is full.