Why this matters: systems designs are graded on invariants — the properties your structure never violates — and the arena is the cleanest place to learn to state them. The whole allocator is three pointers and a linked list; what makes it an interview answer is saying precisely what must always be true of those pointers, and why the contract from lesson 01 makes that enough.
The lifetime argument, stated up front
Before any layout, say the sentence that justifies the design: an arena ties allocation lifetime to a phase, not to an object. A request handler allocates freely while serving the request, and everything dies when the response is sent. A frame loop allocates during the frame and clears at the next one. A parser builds an AST and discards it whole.
In all of these, per-object bookkeeping would be paying for a capability nobody uses — no one frees mid-phase, so tracking what could be freed is pure overhead. Delete the capability and allocation has almost nothing left to do: check space, hand out the current position, advance. That is the design; everything below is arranging bytes so that sentence stays true.
The block: three pointers of state
An arena is a chain of blocks. Each block is one contiguous slab with a cursor:
Block
base start of the slab
cursor next free byte (base <= cursor <= end)
end one past the last byte
next the block allocated after this one (chain)
Arena
head first block in the chain
current block being bumped right now
blockSize standard slab size (64 KiB)
Allocation touches only current: align the cursor up, check it fits before end, advance, return. No headers on allocations, no free list, no size records — the contract says nobody will ever hand memory back individually, so recording anything per object would violate the lifetime argument above.
Alignment lives inside the bump
Every allocation must come back at least 8-byte aligned, and callers may ask for stricter powers of two. Because the arena hands out addresses directly off the cursor, alignment is simply part of the bump arithmetic: round the cursor up to the requested boundary first, then check fit, then advance. The bytes skipped by rounding are the arena's only per-allocation overhead, and they cost nothing to track — they're just unclaimed space behind the cursor.
This is worth a sentence in the interview: alignment in a general-purpose allocator is a bookkeeping problem; in an arena it degenerates to one line of integer math, because there is no bookkeeping to keep consistent with it.
Growth: chaining, and the oversized escape hatch
Exhaustion must not fail, so when the current block can't fit a request, the arena chains a fresh block and bumps there. The subtle requirement is the oversized one: a request larger than a whole standard block must still succeed. The clean answer is a dedicated block sized to the request, spliced into the chain. The standard path stays untouched — current keeps pointing at a normal block with normal bump behavior — and the oversized allocation simply lives in its own link, reclaimed with everything else at destroy.
The alternative — growing the standard block size to fit the largest request seen — punishes every future block for one outlier. Splicing keeps the cost where the cause is.
reset() versus destroy(): two different promises
reset() is the phase boundary: rewind every block's cursor to its base, keep the memory. The next phase reuses warm blocks with zero calls to the system allocator — which is much of the arena's practical speed in servers and frame loops. destroy() is the true end: walk the chain and release the slabs.
Keeping these as separate operations is the contract from lesson 01 made API: "reclaimed all at once" happens often and cheaply; "given back to the OS" happens once.
The invariants
State them explicitly — this list is the systems-flavor equivalent of the parking lot's ownership rules:
- base <= cursor <= end for every block, at all times - every returned pointer lies inside exactly one block's [base, end) - returned pointers are aligned as requested (>= 8) - bytes below a block's cursor are owned by callers until reset/destroy; bytes at or above it are the arena's to hand out - reset() moves every cursor to base and releases nothing - destroy() releases every block exactly once
Notice what's absent: nothing about per-object state, because there is none. When an interviewer probes the design, most answers reduce to pointing at one of these lines.
What we rejected, and why
Per-allocation headers. A general allocator records a size before every payload so free can find it. Here that record would never be read — no-free means no lookup — so headers would burn bytes on the arena's hottest path for a capability the contract excludes. Rejecting them is the pay-only-for-what-the-contract-uses principle, stated at the decision.
Tracking free regions. Any structure that remembers "these bytes came back" is rebuilding a general-purpose allocator inside the arena, and with it, all the costs the arena exists to avoid. The contract already answers reclamation: all at once, at the phase boundary.
Key takeaway
The arena's design is the lifetime argument made structural: allocation lifetime equals phase lifetime, so no per-object state exists at all. Three pointers per block, alignment folded into the bump, chained growth with a dedicated-block splice for oversized requests, reset that rewinds cursors while keeping warm memory — and a short invariant list that makes every one of those claims checkable.