InterviewKata
System design · Worked design D35 of 46 · Social and content · Long form

Design an object storage service

Design the service that stores files for everyone else: Amazon S3, Google Cloud Storage, Azure Blob Storage, or an in-house equivalent. The interface is just put, get, delete and list. Behind it are hundreds of petabytes on disks that fail every day, and a promise that nothing written is ever lost.

1. The prompt

"Design a service like Amazon S3: users create buckets and store objects of any size by key, read them back, list them and delete them."

What makes it hard:

2. Frame the problem

How much data, and how many objects?

About 100 PB today, growing 50% a year, in roughly 100 billion objects, from a few bytes to terabytes each.

Billions of keys means metadata needs its own sharded store; petabytes means the byte layout and redundancy scheme decide the cost.

Traffic?

About 1 million reads and 100,000 writes a second at peak, mostly small objects.

Request rate stresses metadata; bandwidth stresses storage nodes.

Consistency?

A read after a successful write returns the new object; a list after a write includes it.

Strong read-after-write consistency, which S3 has offered since 2020. The metadata commit becomes the single point that decides "written".

Durability?

Losing an acknowledged object should be essentially impossible, even with whole-zone failures.

Redundancy spread across failure domains, plus constant checking and repair.

Large objects?

Up to 5 TB; uploads must survive network blips.

Multipart upload: parts uploaded independently, in parallel, retried alone, then committed together.

Versioning and lifecycle?

Optional versions per bucket; rules to move old data to cheaper tiers or delete it.

Old versions and cold tiers change the storage layout and the cost model.

Access control?

Per bucket and object policies; temporary signed URLs for browsers.

Every request is authenticated before metadata or data is touched.

3. What it must do

  1. A user can put an object (any size) into a bucket under a key, and get it back.
  2. A user can list objects in a bucket by key prefix, page by page.
  3. A user can delete objects, and optionally keep old versions.

Not now: a file-system interface (rename, append, directories), search by content, cross-region replication (a follow-up), billing.

4. How well it must do it

TargetWhy this number
Durability: no acknowledged object lost even if a whole zone and some disks elsewhere fail data safetyIt's the one promise customers can't check themselves.
Availability: 99.99% for reads and writes surviving failuresOther systems depend on it; a zone outage must not stop it.
Consistency: strong read-after-write for objects and listings consistencyClients shouldn't need retries or delays to see their own writes.
Latency: first byte of a small object within ~100 ms at p99 speedWeb pages and apps read from it directly.
Cost: raw capacity well under 2× the stored data costAt 100 PB, every 0.1× of overhead is 10 PB of disks.

5. Numbers that change the design

QuantityWorking→ so we…
Metadata100 billion objects × ~500 bytes ≈ 50 TBToo big for one database → so metadata is a separate, sharded, replicated store
Redundancy3 copies of 100 PB = 300 PB raw; erasure coding with 6 data + 3 parity pieces = 150 PBErasure coding saves ~150 PB → so we erasure-code, and keep full copies only where speed matters
Disk failures1 million drives × ~1% annual failure ≈ 27 a dayFailure is constant → so repair is automatic and continuous, not an incident
Small objectsHalf the objects are under 100 KB; a file per object means billions of filesFile systems choke → so storage nodes pack many objects into large append-only volume files

Illustrative numbers. Skipped: bucket count (small), and per-user numbers.

6. The nouns

bucket     name, owner, region, versioning, policy, lifecycle_rules
object     (bucket, key, version) → size, etag/checksum, content_type, created_at, state, chunks[]
chunk      chunk_id, placement_group, fragments[(node, volume, offset, length)], ec_scheme (6+3)
volume     node, volume_id, size, sealed?                     (fragments appended, never edited)

7. The contract

PUT    /{bucket}/{key}                      body = bytes    → 200 {etag, version}
GET    /{bucket}/{key}?version=…            Range: bytes=…  → bytes
DELETE /{bucket}/{key}                                       → 204
GET    /{bucket}?prefix=photos/&continuation-token=…&max-keys=1000
       → {keys[], next_token}
POST   /{bucket}/{key}?uploads              → {upload_id}         (multipart)
PUT    /{bucket}/{key}?uploadId=…&partNumber=n  body       → {etag}
POST   /{bucket}/{key}?uploadId=…  {parts[(n, etag)]}  → complete

The caller is identified by a signature on each request (or a presigned URL that grants one operation for a limited time); never by a field in the body. Listing is paged with a continuation token, ordered by key.

Data flow (put): front end authenticates → splits into chunks → erasure-codes each chunk → writes fragments to storage nodes in different zones → all needed fragments durable → commit metadata (the moment the object exists) → 200.

8. First working version

Stateless front ends, a metadata service, and storage nodes. The separation of metadata from bytes is the first decision to say out loud.

8.1 Putting an object

The bytes are written first; the metadata commit comes last. If the front end dies halfway, the fragments are orphans with no metadata pointing at them, and a garbage collector removes them later. Readers never see half an object.

8.2 Getting an object

Metadata says where the fragments are; any 6 of the 9 rebuild the chunk. In practice the front end reads the 6 data fragments directly (no decoding needed) and only reads parity when a node is slow or down.

8.3 Listing

Metadata is partitioned by bucket and key range, not by hash, so a prefix listing reads a contiguous range from one or a few shards, in order.

Parked for later3× copies are too expensive (9.1) · disks die daily (9.2) · billions of small files (9.3) · metadata at 100 billion keys (9.4) · huge uploads (9.5) · deletes and versions (9.6).

9. Make it hold up

9.1 Redundancy: copies or erasure coding durability · cost

ApproachWhy
✗ BadOne copy on RAIDA machine or zone loss loses data; RAID rebuilds of huge disks take days.
~ GoodThree full copies in three zonesSimple, fast reads, survives a zone. Costs 3× raw storage.
✓ GreatErasure coding: split each chunk into k data fragments, compute m parity fragments (Reed-Solomon), place the k+m on different nodes and zones; any k rebuild the chunk. 6+3 costs 1.5× and survives any 3 lossesAbout half the raw storage of 3 copies for similar or better durability. Costs: CPU to encode, more nodes per read, slower repair. Small or hot objects can still use copies.

Placement matters as much as the code: fragments of one chunk go to different disks, nodes, racks and zones, so one failure takes at most a fragment or two from any chunk. Some systems use local reconstruction codes (Azure's LRC) so a single lost fragment is rebuilt from a few neighbours instead of k.

9.2 Disks die every day durability

9.3 Billions of small objects latency · cost

Storing each object as its own file on a storage node means billions of files, file-system metadata lookups on every read, and wasted blocks. Instead, nodes append fragments to large volume files (gigabytes each) and keep an in-memory index of (fragment → volume, offset, length): one disk seek per read. Facebook's Haystack paper described the same idea for photos. Deleted fragments leave holes, reclaimed by compacting volumes in the background.

9.4 Metadata for 100 billion keys consistency · latency

ApproachWhy
✗ BadOne relational database50 TB and a million lookups a second don't fit one machine.
~ GoodShard by hash of (bucket, key)Even load, but listing a prefix must ask every shard.
✓ GreatRange-partitioned by (bucket, key), each range replicated with consensus; split hot or large ranges automaticallyOrdered listings from one range; strongly consistent writes; hot prefixes split as they grow. Costs: careful split and merge machinery.

The metadata write is the linearization point: once it commits, every reader sees the new version, which is how read-after-write consistency is achieved. Caches in front of metadata must be invalidated or versioned for this to hold.

9.5 Huge uploads availability

A 5 TB upload over one connection would almost certainly fail somewhere. Multipart upload: the client starts an upload, sends parts (say 100 MB each) in parallel, retries any part alone, then completes the upload with the list of parts. Only the completion writes the object's metadata; abandoned uploads are cleaned up by a lifecycle rule. Reads support byte ranges, so large objects are also downloaded in parallel.

9.6 Deletes, versions and lifecycle cost · rules

9.7 The whole design after the deep dives

10. Interviewer follow-ups

One prefix gets millions of requests a second.

Metadata ranges split automatically under load; a CDN or cache in front of hot public objects absorbs reads. Clients spreading keys across prefixes helps on systems that partition by prefix.

A whole zone goes down.

Fragments are placed so that losing one zone leaves at least k of k+m for every chunk (for example 9 fragments over 3 zones, 3 each, with k = 6). Reads decode from the remaining zones; repair starts only if the zone stays down.

Replicate a bucket to another region.

Asynchronous replication driven by a change log of metadata commits; the destination applies them in order per key. Report replication lag; a region failover may lose the last few seconds of writes.

A customer accidentally deletes everything.

Versioning plus delete markers make it reversible; object lock (write once, read many) prevents deletion entirely for a retention period, for compliance.

How do browsers upload directly without your servers?

Your app server issues a presigned URL for one key and one operation, valid for minutes; the browser uploads straight to storage.

Why not store objects in the metadata database?

Different access patterns and economics: metadata needs fast, consistent, small updates; bytes need cheap, dense, sequential storage. Mixing them makes both worse.

Ten times the data.

Add storage nodes; the placement service spreads new chunks onto them, and a background rebalancer moves some old ones. Metadata ranges split. Cost pressure pushes more data to erasure coding and colder tiers.

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
Disk diesNothingNode health, read errorsErasure coding; distributed repair
Silent corruptionNothingChecksums on read and scrubRebuild the fragment from others
Zone outageSlower readsZone healthPlacement across zones; decode from survivors
Metadata shard leader lostSeconds of retries for its keysConsensus heartbeatsReplicated ranges elect a new leader
Front end crash mid-uploadA failed request; client retriesError ratesMetadata committed last; orphan GC
Bug deletes dataMissing objectsAudit, delete-rate anomaliesVersioning, delayed GC, object lock

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch: chunks below full redundancy (by how many fragments missing) · repair backlog and rate · scrub coverage · metadata latency p99 per range · request errors by type · storage overhead ratio.

At 10×: more nodes and metadata ranges; durability depends on keeping repair faster than failures as disks get bigger.

A 60-minute interview

0–5Frame: size, traffic, consistency, durability
5–10Must do, how well, numbers
10–15Nouns, API, put data flow
15–25First version: metadata vs bytes, put, get, list
25–50Deep dives: erasure coding (9.1), repair (9.2), metadata (9.4)
50–60Multipart, follow-ups, wrap-up

A 90-minute interview

0–8Frame, including versioning and access
8–15Must do, how well, numbers
15–22Nouns, API, data flow
22–35First working version
35–75Deep dives 9.1–9.6 and the final picture
75–85Follow-ups: zone loss, hot prefixes, regions
85–90Trade-offs, metrics, 10×
LevelWhat a strong answer shows
Mid-levelMetadata separate from bytes, replication across zones, a put/get/list API, multipart upload.
SeniorErasure coding with placement across failure domains, commit-last writes, range-partitioned metadata for listings, scrubbing and repair.
Staff+Durability as failure rate versus repair speed, cost per byte as a design driver, small-object packing, read-after-write consistency at scale, lifecycle economics.

Common mistakes:

13. Try it, check yourself, read more

Erasure coding lab. Pick a scheme, then click storage nodes to fail them. Can the chunk still be read?

Teal = data fragment, amber = parity fragment (or a full copy for replication). Any k of the k + m fragments rebuild the chunk.

What does 6+3 erasure coding give you?

Same or better durability than three copies for half the raw storage.

When is an object's metadata committed?

Readers never see an object whose bytes aren't there.

How should metadata be partitioned so prefix listings are fast?

A prefix becomes a contiguous range.

Why scrub data that nobody is reading?

Durability is a race between failures and repair.
ResourceWhy
Building and operating a pretty big storage system called S3 (Werner Vogels)How S3's scale, hard drives and durability work in practice.
S3 strong consistency (AWS)Read-after-write consistency for all operations.
Erasure Coding in Windows Azure Storage (USENIX ATC 2012)Local reconstruction codes and their cost trade-offs.
Backblaze on Reed-SolomonA readable explanation of erasure codes in a real storage service.
Ceph architectureAn open-source object store: placement groups, CRUSH and recovery.
Multipart upload (AWS)The upload protocol for large objects.