We use cookies for site analytics. Accept to help us understand how the site is used. See our Privacy Policy for details.
Namespace vs bytes, erasure coding vs replication, the eleven-nines durability math, multipart upload, and why LIST is the hardest API in the system.
Design a planet-scale object store: clients PUT an immutable blob under a key inside a bucket, GET it back by key, DELETE it, and LIST keys by prefix. Objects range from bytes to terabytes; the system stores trillions of them with eleven nines of durability and serves millions of requests per second.
This is the storage-infrastructure interview at its purest. The API surface is four verbs; everything interesting is beneath it: how the namespace (keys, metadata) is separated from the bytes, how durability is engineered rather than hoped for, how uploads of multi-GB objects survive flaky networks, and what consistency guarantees the metadata layer can honestly make. Strong candidates do the durability math explicitly and treat LIST - not GET - as the API that shapes the metadata design.
Asking these before diving into a solution is the difference between a "hire" and a "no signal" rating. Pick the questions whose answers would change your design.
Metadata scale (the namespace)
Physical bytes and the erasure-coding overhead
Disk failures are constant, not exceptional
Request throughput
Bandwidth math for a single large GET
LIST pressure
The foundational split: a metadata service that owns the namespace (bucket/key → object record → blob pointers) and a blob store that owns raw bytes on disks. They scale independently, fail independently, and have opposite designs - the metadata service is a strongly-consistent sharded database of small records; the blob store is a huge append-oriented shard farm optimized for sequential I/O and background repair.
Write path: the API front-end authenticates and streams the body to the blob layer (erasure-coded to 14 shards across failure domains), computing checksums as it goes; only after the shards are durably acknowledged does it commit the metadata record (key → shard map). The metadata commit is the atomic "the object now exists" point - a crash before it leaves orphaned shards for GC, never a visible-but-missing object.
Read path: front-end resolves key → shard map from metadata (heavily cached), reads any 10 of 14 shards, verifies checksums, streams to the client. Every layer verifies checksums - corruption is detected on read and repaired from parity, never silently returned.
Stateless HTTP layer: auth (signature verification), bucket policy / ACL evaluation, request routing, streaming pass-through with checksum computation. Horizontally scaled behind DNS + L4 load balancing.
Sharded, replicated database mapping (bucket, key, version) → object record (size, checksum, storage class, shard map). Serves strong read-after-write, powers LIST via range scans over lexicographically-partitioned keys.
Store erasure-coded shards in large append-only extent files on raw disks (avoids per-object filesystem overhead for billions of small shards). Serve shard reads/writes, run continuous scrubbing.
Decides which 14 nodes/disks receive a new object's shards, spreading across racks and power domains, balancing fill level and load. Maintains the cluster map that front-ends cache.
Detects lost or corrupt shards (node heartbeats + background checksum scrubs) and reconstructs them from surviving shards onto healthy disks. Repair throughput is a first-class durability metric.
Tracks in-progress multipart uploads (upload ID → committed parts + ETags) and performs the atomic complete step that assembles parts into the final object record.
Apply lifecycle transitions (hot → cold storage class), expire objects, collect orphaned shards from failed uploads and deleted objects, and compact extent files.
The subsystems where the interview is actually decided. Skim if you're running short; own these if you want a strong signal.
The single most load-bearing decision is separating the namespace from the bytes. Once an object's record points at immutable, checksummed shards, the blob layer becomes a dumb (and therefore reliable) byte farm - all the hard semantics concentrate in the metadata service.
What a metadata record holds
(bucket, key, version) → { size, content-type, checksum/ETag, storage class, creation time, ACL reference, shard map: [(shard_id, node, disk, extent, offset), ...] }. ~500 bytes. One trillion of these is a 500 TB strongly-consistent database serving millions of lookups per second - this is the system's beating heart.
Sharding the namespace
Partition by (bucket, key) ranges, lexicographically - not by hash. Hash-partitioning spreads load beautifully and destroys LIST (a prefix scan would touch every partition). Range-partitioning keeps LIST a local scan but concentrates sequentially-named keys on one partition, so the system must split hot partitions dynamically and move ranges between servers - the same design pressure that shaped Bigtable/DynamoDB-style stores. S3 exposes this reality directly: its guidance to "add key prefix entropy for high request rates" is the metadata partitioning showing through the API.
Why PUT commits metadata last
Stream bytes first, commit the record second. The metadata write is the linearization point: before it, the object doesn't exist (crash = orphaned shards, GC'd later); after it, the object fully exists. Inverting the order (metadata first) can expose a key whose bytes aren't durable - a catastrophic contract violation - while orphaned shards are merely wasted space with a background janitor.
Overwrites and versioning
Objects are immutable; an overwrite PUT writes new shards and swaps the record's pointer atomically (or, with versioning enabled, prepends a new version). Readers mid-flight on the old shard map finish safely because old shards are only GC'd after a grace period. Immutability is what makes "strong consistency" cheap: there's no partially-updated blob state to reason about, only an atomic pointer swap in one metadata partition.
Caching the metadata path
Every GET does a metadata lookup, so front-ends cache records aggressively. The classic trap: serving a GET from a stale cached record after an overwrite breaks read-after-write. Fixes: invalidation pushed on write (fragile at this fan-out) or - the honest answer - cache with short TTLs plus a version check against the authoritative partition for correctness-critical paths, accepting one cheap indexed lookup per GET. Metadata partitions serve point lookups from memory; the budget exists.
The blob layer stays dumb on purpose
Storage nodes know nothing about buckets, keys, or ACLs - only (shard_id → bytes at extent offset, checksum). Small shards pack into large append-only extent files, so a billion 100 KB objects don't become a billion filesystem inodes. All policy, naming, and consistency live above. Systems that let semantics leak into the byte layer (e.g., encoding keys into file paths) discover they've built a filesystem with a REST API - and inherited every filesystem scaling wall.
"Eleven nines" (99.999999999% annual durability) means: of a trillion objects, you expect to lose about one per decade... roughly. Interviewers love candidates who can derive why that's achievable rather than recite it.
Start with a single disk
A disk with 1-2% annualized failure rate (AFR) gives roughly two nines. Any real durability comes from redundancy plus repair - durability is a race between correlated failures and your repair speed.
Replication math (3x)
An object dies if all 3 replicas die before repair. With AFR ~1.5% and a repair window W after the first failure, the probability the two survivors both die within W (say 24 hours: 1.5% × 24/8766 ≈ 4×10⁻⁵ each) is ~(4×10⁻⁵)² ≈ 1.6×10⁻⁹ per initial failure - and each object's replica set sees ~4.5% initial-failure events/year. Net ≈ 7×10⁻¹¹/object-year: about ten nines, shy of the target, and it burned 3x the storage.
Erasure coding math (10+4)
Split the object into 10 data shards, compute 4 parity shards (Reed-Solomon); any 10 of the 14 reconstruct it. Losing the object requires 5 of 14 shards gone before repair - with independent daily-scale failure odds of ~4×10⁻⁵ per shard, a 5-way coincidence lands around 10⁻¹⁴-10⁻¹⁶/object-year. Comfortably past eleven nines, at 1.4x overhead instead of 3x. This is why every hyperscale store (S3, GCS, Azure, Ceph EC pools) erasure-codes bulk data: more durable and 2x cheaper than 3x replication.
The two assumptions doing all the work
Silent corruption: the sneakier enemy
Disks also lie - bit rot, misdirected writes, firmware bugs - at rates that matter at 47K-disk scale. Defenses: end-to-end checksums stored in metadata and verified on every read, plus continuous background scrubbing that reads every shard on a fixed cadence and repairs mismatches from parity. Without scrubbing, corruption hides until the moment you need that shard for a rebuild - the worst possible time to discover it.
Availability is a different number on purpose
11 nines durability with 4 nines availability: a partition or node crash can make an object briefly unreadable (availability event) while its bytes remain safe on 14 disks (no durability event). Conflating the two - or promising availability from the durability design - is a classic interview miss.
The durability math says "erasure code everything." Production says: the code's real costs surface on the read path, the repair path, and small objects. Managing those is the practice.
Reads under failure get amplified
Replication repair copies one replica (1:1 reads to bytes recovered). A 10+4 code must read 10 shards to rebuild one - 10x read amplification. When a 20 TB disk dies, the fleet reads ~200 TB to restore ~20 TB of shards. Do that naively fast and repair traffic starves foreground GETs; do it politely slow and you stretch the durability-critical vulnerability window. Production stores run repair under a token-bucket budget with priority by stripe risk: stripes missing 2+ shards jump the queue and repair at maximum speed, single-shard losses trickle - which keeps the (window)ⁿ math strong exactly where it's weakest.
Local Reconstruction Codes: paying storage to shrink repair
The dominant failure is a single lost shard, so optimize for it: LRC (Azure's 12+2+2) adds local parity over sub-groups, so a single-shard rebuild reads ~6 local shards instead of 12, roughly halving repair I/O for ~1.33x overhead. Wide global parity still covers multi-failure stripes. The general lesson: code choice is a three-way dial between storage overhead, repair cost, and fault tolerance - "Reed-Solomon 10+4" is a fine interview default, and naming LRC as the repair-cost refinement is a strong senior signal.
Degraded reads
A GET whose primary shards are missing reconstructs inline from any 10 survivors - correct, but latency jumps from one disk read to a 10-node fan-out plus decode, and p99 inherits the slowest of ten disks. Standard mitigations: request 11-12 shards speculatively and decode from the first 10 to arrive (tail-hedging inside a stripe), and repair proactively so degraded stripes are rare rather than routine.
Small objects don't want wide stripes
A 100 KB object in 10+4 means 10 KB shards - 14 disk ops per read, seek-dominated, plus per-shard metadata that can rival the data. Two production answers:
Write-path fan-out
Each PUT streams through an encoder to 14 nodes and waits for all 14 durable acks before the metadata commit (a straggler node is handled by writing its shard to a fallback node and recording the substitution - never by acking at 13/14 and hoping). Encoding is cheap (Reed-Solomon with SIMD runs GB/s per core); the p99 cost is the 14-way straggler problem, which the fallback-write mechanism converts from a latency wall into a placement footnote.
A single-stream 5 TB PUT at 100 MB/s runs ~14 hours; one TCP reset at hour 13 and a naive design restarts from byte zero. Multipart upload is the protocol answer, and its design decisions echo through the whole write path.
The protocol
Why parts are the unit of everything
Each part is durably stored (erasure-coded) when uploaded, so a network failure costs at most one part's progress. Parallelism turns the 14-hour single stream into ~1.5 hours over 10 streams - bounded by client uplink, not server design. And parts map naturally onto blob-layer stripes: a part is the erasure-coding unit, so "assemble the object" is pure metadata - no bytes move at Complete time. That's what makes Complete fast and atomic even for a 5 TB object.
ETags and integrity
Per-part checksums are verified on upload; Complete verifies the client's part list matches what the server holds (catching lost/duplicated/reordered parts from client bugs). This is also why multipart objects' ETags aren't a simple content MD5 (S3 uses hash-of-part-hashes) - a detail that bites naive client-side integrity checks and a nice bit of realism to mention.
Concurrent completes and idempotency
Two clients completing the same upload ID, or a retry of a Complete whose response was lost: the coordinator makes Complete idempotent (same part list → same result, recorded against the upload ID) and last-write-wins at the key level, consistent with ordinary PUT semantics.
Range GETs: the read-path mirror
GET with a byte range resolves which parts/shards cover the range and reads only those - the shard map's offset index makes a 1 MB read from a 5 TB object cost one or two shard touches, not a scan. Large-object consumers (video players seeking, download managers resuming, analytics engines reading column footers) live on range reads; a design that can't serve them cheaply has hidden a full-object read behind a friendly API.
Interview signal: candidates who specify Complete's atomicity, the orphaned-part GC, and Complete-as-pure-metadata have designed the feature; candidates who say "split the file and upload chunks" have described the brochure.
For 14 years S3 was eventually consistent for overwrites and LIST, and an entire generation of data-engineering scar tissue formed around it. In 2020 it switched to strong read-after-write - for free, with no latency regression. Both halves of that story are interview gold.
What eventual consistency actually broke
What "strong" means here (and what it doesn't)
After a successful PUT (new or overwrite), any subsequent GET/HEAD/LIST reflects it - linearizable per-key read-after-write, immediately and globally within the region. It does not mean cross-object transactions, compare-and-swap, or rename; concurrent PUTs to one key still resolve last-writer-wins, and mutual exclusion remains the client's problem (or a conditional-write feature bolted on later). Scoping the guarantee precisely is exactly the kind of statement that separates senior candidates.
How you build it
Strong consistency was never impossible - it's a per-key linearizability problem over the metadata service:
The honest reason it was eventual for so long: at trillion-object scale, cache coherence across a planet-sized front-end fleet is brutally hard, and eventual consistency let the caches be sloppy. The 2020 change was AWS paying that engineering cost centrally so every customer could delete their workaround code.
Immutability is the enabler
Note what made this tractable: blobs never mutate. Consistency is only ever about which record version a key resolves to - an atomic pointer swap in one partition - never about torn 5 TB writes. A design that allowed in-place blob mutation would need to solve consistency at the byte layer too, and would deserve what happened to it.
Multi-region is a different contract
Cross-region replication remains asynchronous (seconds-to-minutes lag, monitored per-rule): strong consistency within a region, eventual between regions. Global-strong would put a cross-continent consensus round-trip inside every PUT - the correct design refusal, and a good moment to say why.
GET and PUT are point operations - shard them and go home. LIST is an ordered range scan with pagination over a trillion-row keyspace, and it quietly dictates the metadata service's entire partitioning story.
The contract
LIST(bucket, prefix, start-after, max-keys) returns up to 1,000 keys, lexicographically ordered, with a continuation token. The ordering guarantee is the expensive part: it forces the metadata layer to keep keys range-partitioned (sorted runs per partition), because hash partitioning would turn every LIST into a scatter-gather across all partitions with a giant merge - a non-starter at this scale.
Range partitioning's tax: the hot partition
Sorted keys mean workloads with sequential names - logs/2026-07-08/14-32-01.json, invoice-000123456 - aim every PUT and the "what's new" LIST at the same tail partition. This is the classic time-series hot-shard problem relocated into a storage API. Mitigations, in the order a real system deploys them:
Delimiter listing: directories that don't exist
LIST with delimiter="/" returns keys under the prefix plus CommonPrefixes - synthesized "folders". There are no directories; the metadata layer fakes them with a range-scan trick: on hitting photos/2024/x.jpg, emit common prefix photos/2024/ and seek directly to the successor key of that prefix (the first key after the entire photos/2024/ subtree), skipping the rest of it. One seek per synthesized folder instead of scanning millions of member keys - without the skip-scan, listing a bucket's top level with a million-object "folder" reads a million rows to emit one line. This little algorithm is the difference between a usable console and a metadata DDoS.
Pagination as a cursor, not an offset
Continuation tokens encode the last key returned (an index seek position), never a numeric offset - OFFSET 500000 would re-scan half a million rows per page. Token-based pagination also makes LIST naturally resumable and (with versioned reads within a page) consistent per-page even as the namespace churns underneath; cross-page phantom keys are inherent and documented, not a bug to fix.
Versioning and delete markers
With versioning on, LIST reads the newest version per key and must skip keys whose newest version is a delete marker - so the scan cost tracks versions examined, not keys returned. A bucket with heavy churn (millions of delete markers from a lifecycle rule) can make an innocent LIST crawl; ListObjectVersions exposes the raw truth. Mentioning marker-skipping cost flags real operational literacy.
When LIST is the wrong tool
At a trillion objects, "enumerate the bucket" is a batch job measured in days. Real systems stop paginating and go async: a manifest/inventory export (daily listing to objects the customer reads with analytics tools) and event notifications (per-PUT events into a queue) replace polling LIST for pipelines. Recognizing when to demote LIST from the hot path entirely is the final senior move this topic offers.
Erasure coding vs replication
EC gives more durability per byte at 1.4x overhead vs 3x, but costs 10x read amplification on repair, degraded-read latency, and per-object shard fan-out. Replicate small/hot objects, erasure-code the bulk bytes - the hybrid is the answer, not either pole.
Range-partitioned metadata vs hash-partitioned
Range partitioning makes ordered LIST cheap and creates hot partitions under sequential key names; hash partitioning balances perfectly and makes ordered LIST a full scatter-gather. The LIST contract forces range partitioning plus dynamic splitting - accept the hot-shard machinery as the price of the API.
Strong vs eventual consistency
Eventual consistency lets front-end caches be sloppy and scales trivially; it exports silent data-loss patterns (LIST lag) to every client. Strong read-after-write costs a coherent invalidation/lease layer over the metadata caches, paid once centrally. Within a region, strong is the modern bar; cross-region stays async replication honestly labeled.
Wide stripes vs narrow stripes (and LRC)
Wider codes (14+4) cut overhead but widen the straggler fan-out and raise repair reads; LRC-style local parity spends ~5% more storage to halve single-shard repair cost. Choose by your dominant failure mode - which is single-disk loss, which is why LRC exists.
Pack small objects vs store individually
Packing thousands of small objects into erasure-coded extents amortizes shard overhead and makes reads one disk touch, but imports LSM-style GC/compaction as objects die. Storing individually is simpler and drowns in per-object shard and metadata costs. At S3 scale, packing wins and you staff the GC.
Repair speed vs foreground latency
Fast repair shrinks the durability vulnerability window; it also steals disk and network from customer GETs. Budget repair with risk-based priority: stripes near the fault-tolerance edge repair at wire speed, single-shard losses yield to foreground traffic.
Availability vs durability spend
Both come from redundancy but different kinds: durability wants shards spread across failure domains; availability wants replicas of the serving path (metadata leaders, front-ends) and graceful degraded reads. Fund them separately and report them separately - a system that conflates them will discover the difference during its first regional partition.
Be ready for at least three of these. The first one is almost always asked.
Consistent hashing, eviction, replication, and what really happens when a single hot key takes down the cluster.
Edge cache hierarchies, cache key design, invalidation, origin shield, and edge compute - the system every other system relies on without thinking about it.
Encoding ladders, adaptive bitrate, CDN economics, and the difference between live and VOD. Petabyte-scale storage meets millisecond-scale playback.
Raft leader election, log replication, snapshots - and the CAP theorem in operational practice. The substrate every other distributed system stands on.
Reading is the floor. The interview signal is in walking through this live with someone probing follow-ups. Use the AI mock interview to practice talking through requirements, architecture, and trade-offs out loud.
Start an AI mock interview →