Two planes: the index says where, storage holds what

Metadata and bytes scale on different axes, so the store splits into an index plane (key → fragment locations) and a data plane (dumb nodes holding fragments) — and a GET resolves the key, reads k fragments, reconstructs if any are missing, verifies, and streams.

Previously

Fragments are now spread across failure domains for safety — so something has to remember which storage node holds which fragment. That something is the index plane, and wiring it to the storage nodes gives us the whole read path.

Scene 06

Two planes: the index says where, storage holds what

  1. Watch
  2. Try it
  3. Predict
  4. Capture
GET PATH1. authenticate2. index lookup3. read k fragments4. reconstruct5. verify checksum6. stream bytesFrontendGETauthenticatedindex planekey → fragment locations · WHEREphotos/cat.jpgsn1:d1 sn2:d2 sn3:d3 sn4:p1logs/2026-06-03sn2:d1 sn3:d2 sn4:p1db/backup.tarsn1:d2 sn3:d4 sn4:d1read kdata planedumb storage nodes hold fragments · WHATnode-1d1node-2d2node-3d3node-4p1authenticate
What to watch for

A GET arrives. The Frontend authenticates the request, asks the index plane where this key's fragments live, reads k fragments from the storage nodes in parallel, verifies a checksum, and streams the bytes back. Watch the ribbon walk the path.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

Frontend.get
resolve the key, read k fragments, rebuild if any are missing
def get(bucket, key):
authenticate(request)
# ask the index plane WHERE the fragments live
meta = index.lookup(bucket, key)
if meta is None:
return 404 # nothing points at any bytes
frags = read_parallel(meta.locations) # data plane
if any_missing(frags):
# degraded read: pull parity, solve for the gap
frags = reconstruct(frags, meta.parity)
obj = concat(frags)
verify(checksum(obj), meta.checksum)
return stream(obj)
IndexPlane.lookup
the keymap: key → fragment locations, scaled apart from bytes
# the index plane — one tiny row per object
# rows scale with OBJECT COUNT, not byte volume
def lookup(bucket, key):
shard = route_by_key(bucket, key) # flat keyspace
row = shard.get(key)
if row is None:
return None # source of truth for existence
return row # locations, parity, checksum, size

Where this sits in Build an S3-style distributed object store

Scene 06 of 12, in the Keep it alive act — Erasure coding, placement, the two planes, and repair.. Index plane vs data plane; trace a GET (read k, reconstruct, verify, stream) and a PUT's atomic index commit.

Up next. We can now read and write an object — but we still haven't answered the opening mystery of how the nines survive weekly disk death; the plane split is exactly what lets a background loop fix it.

All 12 scenes in Build an S3-style distributed object store · Every curriculum

Built with Arqly
Every scene in Build an S3-style distributed object store builds on the one before it.All 12 Build an S3-style distributed object store scenes