Circular Buffer / Ring Buffer
debt(d7/e5/b3/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints note automated=no and the only listed tool is Blackfire (a profiler), meaning misuse — such as using array_shift() on large arrays causing O(n) reindexes, or silently overwriting unread data — won't be caught by a compiler, linter, or SAST tool. It surfaces only under profiling or careful review of the data-structure choice.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix points to swapping in SplFixedArray or modulo-indexed array, but the common_mistakes reveal multiple interacting concerns: handling the full/overflow condition, power-of-2 sizing for bitwise optimisation, and adding synchronisation for concurrent use. Correcting a naïve growing-array implementation to a proper circular buffer requires reworking the data-structure logic and all call sites that assume unbounded growth, typically spanning one component but with meaningful refactor depth.
Closest to 'localised tax' (b3). The applies_to contexts are web and cli, but a circular buffer is a contained data-structure choice — it doesn't impose architectural gravitational pull on the wider codebase. Its burden is paid by the component that owns it (e.g. a logging subsystem), while the rest of the codebase remains unaffected.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field explicitly identifies the trap: developers equate a circular buffer with a queue (unbounded growth), missing that the defining property is fixed memory with overflow semantics (block or overwrite). This directly contradicts the intuition built from standard queue usage, and the silent-overwrite common mistake means data loss without any error signal — a serious cognitive hazard.
Also Known As
TL;DR
Explanation
A circular buffer has a head (read) and tail (write) pointer. Write: place at tail, advance tail % capacity. Read: take from head, advance head % capacity. Full: tail + 1 == head. Empty: head == tail. O(1) read and write. Fixed memory — no allocation after creation. Overwrite mode: when full, advance head (discard oldest) without blocking. Lock-free single-producer/single-consumer implementations exist using atomic operations. Used in: kernel ring buffers, audio/video streaming, network packet queues, and rolling log windows.
Common Misconception
Why It Matters
Common Mistakes
- Not handling the full condition — writing to a full buffer silently overwrites unread data.
- Using modulo on non-power-of-2 sizes in tight loops — use bitwise AND with power-of-2 size for speed.
- Not making the buffer size a power of 2 — % operation can be replaced with & (size-1).
- Shared circular buffer without synchronisation — concurrent read/write requires locks or atomic operations.
Code Examples
// Dynamic array as log buffer — unbounded memory:
$logs = [];
while (true) {
$logs[] = readEvent(); // Grows forever — OOM after enough events
}
// Circular buffer — fixed memory, O(1) operations:
class CircularBuffer {
private array $buffer;
private int $head = 0;
private int $tail = 0;
private int $size = 0;
public function __construct(private int $capacity) {
$this->buffer = array_fill(0, $capacity, null);
}
public function write(mixed $item): void {
$this->buffer[$this->tail] = $item;
$this->tail = ($this->tail + 1) % $this->capacity;
if ($this->size < $this->capacity) $this->size++;
else $this->head = ($this->head + 1) % $this->capacity; // Overwrite oldest
}
public function read(): mixed {
if ($this->size === 0) return null;
$item = $this->buffer[$this->head];
$this->head = ($this->head + 1) % $this->capacity;
$this->size--;
return $item;
}
}