Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

24 chapters
⌕
Beginner7›
B1The mental modelB2Client-server basicsB3Vertical vs horizontalB4Databases in designB5Caching fundamentalsB6APIs & communicationB7Walkthrough: URL shortener
Intermediate9›
I1Load balancing in depthI2CDNsI3Message queues & asyncI4Consistency modelsI5Rate limiting & throttlingI6Designing for availabilityI7Storage systemsI8Search systemsI9Walkthrough: feed/chat
Advanced8›
A1CAP theorem in depthA2Sharding at scaleA3Distributed consensusA4Fault toleranceA5Observability at scaleA6Capacity estimationA7Case studiesA8Tradeoff thinking
/ search[ ] chaptert top

System Design levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
I7

Storage systems

Block, file, object — and why the bytes of a user upload should never touch your app servers.

Three storage abstractions, and the one you should default to

"Where do the files go?" appears in nearly every design that touches photos, video, documents or backups, and it is a question with a boring correct answer that a surprising number of candidates miss. The three abstractions differ in what the storage layer knows about your data: block storage knows nothing but offsets, file storage knows a directory tree, object storage knows an immutable blob plus metadata and nothing else.

BlockFileObject
UnitFixed-size blocks, no structureFiles in a POSIX hierarchyImmutable blob + key + metadata, flat namespace
AccessAttached to one machine as a deviceMounted by many machines (NFS/SMB)HTTP GET/PUT, from anywhere
MutationRandom read/write at any offsetRandom read/write, plus locking semanticsReplace whole object; no partial in-place edit
Scale limitOne volume, typically up to ~64 TBPetabytes, but metadata operations get slowEffectively unbounded, per-object up to ~5 TB
LatencySub-millisecond~1-5 ms~20-100 ms first byte
Cost / GB-month~$0.08 (SSD-backed)~$0.30 (managed NFS)~$0.023, dropping to ~$0.001 archived
Reach for this when…A database's data directory, or anything needing real random writesLegacy software that insists on a filesystem, or shared build/render scratch spaceUser uploads, media, backups, logs, static assets — the default

Notice the cost column spans two orders of magnitude for storing the same bytes. That gap is not a discount for being clever; it is what you pay for random-write latency you probably do not need. A 500 KB profile photo does not need sub-millisecond random access at any offset. It needs to be fetched whole, over HTTP, from a CDN.

Why object storage is the default, and what the durability number means

S3-class object stores advertise something like eleven nines of durability (99.999999999%). That is not marketing noise, but it also does not mean what people assume. It is an annual expected-loss figure: store ten million objects and you would statistically expect to lose one roughly every ten thousand years. It is achieved by erasure coding — the object is split into k data fragments plus m parity fragments spread across independent failure domains, and any k of the k+m fragments reconstruct it. Losing several disks, or an entire facility, loses nothing.

⚠ Durability and availability are different properties, and conflating them is a tell Durability is "the bytes still exist." Availability is "you can read them right now." S3 Standard offers eleven nines of durability but only four nines of availability, backed by a three-nines SLA. Those are not in tension — a regional API outage means you cannot reach a perfectly intact object. And critically, durability protects against hardware, not against you. Eleven nines does nothing about a bad deploy that deletes a prefix, or a compromised credential. That is what versioning, object lock and cross-region replication are for. Say this distinction out loud; it is a cheap, high-signal moment.

The other property to name explicitly is consistency. Modern S3 is strongly read-after-write consistent for new objects and overwrites, which removed a class of bug that used to bite everyone. But two caveats survive and are worth knowing: listing a bucket can still lag behind individual object writes at scale, and any CDN or client cache in front of the bucket reintroduces staleness that the storage layer's guarantee says nothing about. Cache-busting by putting a content hash in the key — writing new objects instead of overwriting them — sidesteps both.

Presigned URLs: get your app servers out of the byte path

This is the single most reliably-asked storage detail in an interview, and the naive design fails on it hard. If uploads are POSTed to your API, then every byte of every file traverses your app servers: they buffer it, hold a worker thread or event-loop turn for the whole transfer, consume bandwidth twice (in from the client, out to storage), and now your autoscaling is driven by upload volume rather than request volume. A single user on a slow connection uploading a 2 GB video occupies a server slot for minutes.

client app server issues presigned URL metadata DB row: pending object storage bucket event queue object-created worker scan + transcode 1 init 2 signed URL 3 pending row 4 PUT bytes straight to the bucket 5 event 6 marks it ready
The green path carries every byte and never touches the app tier. The app tier only ever handles two small JSON requests per upload, whatever the file size.

A presigned URL is a normal storage URL with a signature, an expiry and a set of constraints baked into the query string. Your app server holds the storage credentials; the client never does. Signing is a local cryptographic operation — no network call to the storage provider — so issuing them is essentially free.

The constraints matter, and interviewers probe them. A presigned PUT should pin: expiry (5-15 minutes, not 7 days), a maximum content length so a client cannot upload 500 GB, the exact object key which your server generates so clients cannot overwrite each other's objects, and ideally content type. The key should be server-chosen and unguessable — a UUID or content hash, never a user-supplied filename, which is both a collision problem and a path traversal problem.

⚠ Trusting the client to tell you the upload finished If the client PUTs to storage and then calls your API saying "done," a malicious or merely buggy client can mark a nonexistent or half-written object as ready. Drive the state transition from the storage layer's own object-created event instead, which is what the queue in the diagram is for. That also gives you a natural place to run the things you must never skip: virus scanning, content moderation, size and format validation, and metadata extraction — all before the object is visible to anyone. Objects sitting in the pending state past their expiry get swept by a lifecycle rule.

Downloads get the same treatment in reverse. Serving private media through your API means proxying gigabytes; a presigned GET (or a signed CDN URL with a short TTL) lets the CDN serve it while still enforcing authorization, because the signature is the authorization and it expires.

Large media: chunked, resumable, and parallel

A single PUT is fine to about 100 MB and is capped around 5 GB. Beyond that, and on any mobile network, one long-lived request is the wrong shape: a dropped connection at 95% costs you the whole transfer.

Multipart upload solves this. The client initiates an upload and gets an upload ID, then uploads independent parts (minimum 5 MB each, up to 10,000 parts, which is what gets you to a 5 TB object), each returning an ETag. When all parts are in, the client sends a complete request listing the parts and their ETags, and the storage service assembles the object. Three properties fall out of that design and each is worth stating:

  • Resumability. A failed part is retried alone. The client can query which parts already landed and resume after an app restart or a change of network.
  • Parallelism. Parts are independent, so a client can run 4-8 concurrent uploads and saturate the link rather than being limited by a single TCP stream's throughput.
  • Integrity. Per-part checksums catch corruption at part granularity instead of after the whole 5 GB transfer.

Each part can have its own presigned URL, so the bytes still bypass your app entirely. The one thing you must remember operationally: incomplete multipart uploads consume storage you are billed for and are invisible in a normal object listing. A lifecycle rule to abort uploads older than seven days is not optional, and mentioning it reads as operational experience.

Transcoding: the pipeline behind every video product

Raw uploads are unservable. A 4K phone recording is the wrong codec, bitrate and container for most viewers, so you generate a ladder of renditions and let the player adapt to the network.

source object 4K, 3 GB job queue per segment worker worker worker 1080p / 5 Mbps 720p / 3 Mbps 360p / 0.8 Mbps encode is CPU-bound at roughly 1-3× realtime per rendition — split the source into 10-second segments and fan those out
The parallelism unit is the segment, not the file. Fanning out by rendition alone caps a two-hour film at the speed of one CPU.

Three design points to raise. First, transcoding is asynchronous by definition — the upload response returns immediately with a processing status and the client polls or gets pushed a notification, which is the message-queue chapter applied directly. Second, jobs must be idempotent and keyed on the source object plus rendition, so a worker crashing halfway just gets retried without producing duplicates. Third, the output is not one file per rendition but hundreds of small HLS/DASH segments plus a manifest, because that is what lets a player switch bitrate mid-stream — and it means the CDN serves a huge number of small cacheable objects rather than a few enormous ones.

Where not to put blobs: your relational database

Storing a file as a BYTEA or BLOB column is tempting because it gives you transactions and one backup story. It is almost always wrong at scale, for reasons worth being able to list quickly:

  • It destroys your buffer pool. The database caches pages of hot rows in RAM. A 2 MB blob evicts hundreds of useful index and row pages to serve one request. Your unrelated queries get slower.
  • Backups and restores become impossible to schedule. A 200 GB database backs up in minutes; the same database with 8 TB of images does not, and your recovery time objective quietly becomes hours.
  • Replication amplifies it. Every blob write ships to every replica over the replication stream, which is exactly the traffic you least want in a channel whose lag governs your read consistency.
  • You cannot put a CDN in front of a SQL query. Every byte comes out through your connection pool, competing with real queries for connections.
  • Cost. Database storage is priced like block storage, roughly 3-10× object storage, and you are paying it for bytes you never query on.

The correct pattern: blob in object storage, row in the database holding the key, size, content type, checksum and status. You lose atomicity across the two — which is why the pending-then-confirmed state machine in the upload diagram exists, and why an orphan-sweeping job (objects with no row, rows with no object) belongs in the design.

Say it like this → "Blobs go to object storage, metadata goes to Postgres. Uploads go direct from the client via a presigned PUT with a short expiry, a size cap and a server-generated key, so no file byte ever touches an app server. The row starts as pending and is flipped to ready by a worker consuming the bucket's object-created event, after scanning and thumbnailing. Reads are served from the CDN with signed URLs."

Hot, warm, cold: tiering and what it actually saves

Access to stored data is extraordinarily skewed. A photo is viewed heavily in its first week and then approximately never — but must still be there in ten years. Tiering prices that reality.

Tier$/GB-monthRetrievalFirst-byte latencyRight for
Standard (hot)~$0.023freetens of msAnything read this month
Infrequent access (warm)~$0.0125~$0.01/GBtens of msRead a few times a year; 30-day minimum charge
Archive instant~$0.004~$0.03/GBtens of msCompliance copies you must be able to produce immediately
Archive flexible~$0.0036~$0.01/GBminutes to hoursBackups, old media
Deep archive (cold)~$0.00099~$0.02/GBup to 12 hoursLegal retention you hope never to read

Make the saving concrete. Take 5 PB of user media where 5% is read in any given month. All-hot costs about 5,000,000 GB × $0.023 = $115,000 a month. Move the 95% cold portion to deep archive and it becomes 250,000 × $0.023 + 4,750,000 × $0.00099 ≈ 5,750 + 4,700 = about $10,500 a month — a 90% cut, with the tradeoff that a rare access takes hours and costs a retrieval fee.

⚠ Aggressive tiering can cost more than it saves Every colder tier adds a minimum storage duration (30, 90 or 180 days) and a per-GB retrieval charge. Push objects to infrequent access after 7 days and then read 20% of them again, and you pay the retrieval fee plus the early-deletion penalty — often more than staying hot. Tier on the access distribution you measured, not on age alone; intelligent-tiering that observes access patterns and moves objects automatically is the safe answer when you do not know the distribution yet.

One more line item people forget entirely: egress. Pulling data out of a cloud region runs around $0.05-0.09 per GB, which is roughly four times the monthly cost of storing it. For a media-heavy product, bandwidth out of the CDN is usually a larger bill than the storage itself, which is the real financial argument for high cache-hit ratios.

Recognizing it in an unseen problem

  • The prompt mentions photos, video, documents, avatars, attachments, backups or "user uploads" — object storage is the answer, and presigned direct upload is the detail that earns the point.
  • A naive design POSTs the file to the API and stores it in the database, then wonders why the app tier autoscales on upload traffic and backups take six hours.
  • Distinguish it from a caching question: caching is about latency for data you already have; storage tiering is about cost for data you rarely touch. They use the same vocabulary (hot/cold) and mean different things.
  • Distinguish durability from availability the moment anyone says "we can't lose the data" — and add that neither one protects against a bad deploy, which is what versioning and object lock are for.
  • Files over ~100 MB, or mobile clients, means multipart and resumable uploads; anything with video means an async transcoding pipeline with idempotent, segment-level jobs.
  • Pitfall: designing the write path beautifully and forgetting the read path. Say how the bytes get back out — signed CDN URLs, cache TTLs, and who pays the egress.
←previousDesigning for availability↑ CovernextSearch systems→