System design interview: design file storage like Dropbox

A file storage and sync service splits every file into fixed-size blocks named by their content hash, stores the blocks in object storage and keeps a separate, strongly consistent metadata database that says which blocks make up which version of which file. Clients upload only the blocks the server does not already have, commit a new version against the version they started from, and learn about other devices' changes through a change journal and a lightweight notification channel.

By Published 9 min read

1. Clarify the requirements

"Design Dropbox" can mean a backup service, a sync client or a collaboration product. Pin down which before you draw anything.

  • Functional: upload and download files of any size; sync a folder across a user's devices; keep previous versions; share files and folders with other users at view or edit level; work offline and catch up later.
  • Non-functional: never lose or corrupt an acknowledged file (durability matters more than anything else); changes appear on other devices within seconds; large uploads survive flaky networks; storage cost per user stays low.
  • Out of scope unless asked: real-time co-editing inside a document (a different problem, see the collaborative editor walk-through), full-text search, previews and thumbnails.

2. Estimate the scale

The figures below are assumed round numbers for a large consumer service, not any real company's traffic. Their job is to show which part of the system is big.

Back-of-envelope estimate for a large file sync service (assumed figures)
QuantityAssumptionResult
Users500 million registered, 50 million active a daySize metadata for 500 million, traffic for 50 million
File changes2 new or edited files per active user a day100 million commits a day, about 1,160 a second; plan for 3x at peak, about 3,500
Upload volume1 MB of changed data per commit on average100 TB a day before dedup, about 1.2 GB/s (roughly 9 Gbit/s) on average
Stored data2 GB per registered user on average500 million x 2 GB = 1 EB before redundancy
Metadata200 files per user, about 1 KB per file row100 billion rows, about 100 TB before version history
Block index1 EB in 4 MB blocks, about 100 bytes per index entryat least 250 billion blocks, about 25 TB of index
Open sync connections25 million devices online at peak, 50,000 per notification serverabout 500 notification servers
Back-of-envelope estimate for a large file sync service (assumed figures). Scroll sideways on a small screen.

The conclusions: bytes dwarf everything else, so blob storage cost drives the design; the metadata is far too large for one database, so it must be partitioned; and the commit rate is modest, which means the metadata database can afford real transactions.

3. Define the API

POST /api/v1/uploads
  body:    { "path": "/Reports/q3.pdf", "size": 9437184,
             "blocks": ["sha256:9f2c...", "sha256:41aa...", "sha256:c07e..."] }
  returns: 200 { "uploadId": "u_81k", "missing": [
             { "hash": "sha256:c07e...", "putUrl": "https://blobs.example/...&expires=900" } ] }

PUT {putUrl}                     (client to object storage, one request per missing block)

POST /api/v1/commits
  body:    { "uploadId": "u_81k", "path": "/Reports/q3.pdf", "baseRevision": 41 }
  returns: 200 { "fileId": "f_77", "revision": 42 }
           409 { "latestRevision": 42 }   (someone else committed first)

GET /api/v1/changes?cursor=ns_12:1882   returns changes after the cursor and a new cursor
GET /api/v1/changes/wait?cursor=...     long-poll: returns when there is something to fetch
GET /api/v1/files/{fileId}/revisions/{rev}   returns the block list and short-lived download URLs
POST /api/v1/shares   body: { "folderId": "f_9", "email": "ana@example.com", "role": "editor" }
Uploads go straight to object storage through pre-signed URLs; the API servers only handle metadata.

4. Sketch the data model

Group files into namespaces: a user's private root is one namespace and every shared folder is another. A namespace is the unit of partitioning, of ordering and of permission, which keeps every commit inside one shard.

Metadata tables, all partitioned by namespace except the block index
TableKey fieldsWhy it is there
filesnamespace_id, file_id, parent_id, name, latest_rev, deletedThe folder tree and the current version pointer
revisionsfile_id, rev, ordered list of block hashes, size, device_id, created_atEvery version of a file; restoring a version is just pointing back at an old block list
journalnamespace_id, seq (increasing per namespace), file_id, opThe change feed that clients read from their cursor
aclnamespace_id, principal_id, roleWho can read or write a namespace
blockshash (key), storage location, size, ref_count, tierThe block index: does this block exist, where is it, is it still used
Metadata tables, all partitioned by namespace except the block index. Scroll sideways on a small screen.

5. The core decision: blocks, hashes and commits

The client splits a file into blocks of about 4 MB and computes a SHA-256 hash of each one. The hash is the block's name. Because the name depends only on the content, three things follow for free.

  • Delta sync at block level. Editing one page of a 200 MB video project changes a few blocks, and only those are uploaded.
  • Resumable uploads. If the connection drops after 30 of 50 blocks, the client asks again which blocks are missing and sends the other 20. No upload session state needs to survive on the server.
  • Deduplication. Two identical blocks have the same hash, so the second upload is skipped and the block is stored once.

The order of writes is what keeps data safe: blocks first, metadata second. A commit is accepted only if every block it references is present in the block index, so a revision can never point at bytes that do not exist. The commit itself is one transaction in the namespace's shard: check that latest_rev still equals baseRevision, insert the new revision, move latest_rev, increment block reference counts and append a journal row. If latest_rev has moved on, return 409.

Two details separate a good answer from an average one. First, never trust a hash the client claims: if the server skips an upload because a user says "I have block X", anyone who learns a hash can add someone else's data to their own account. Verify the checksum on upload, and limit dedup to blocks the user or namespace has already proved it holds, or run cross-user dedup only on the server side. Second, fixed 4 MB boundaries shift when a byte is inserted near the start of a file, which changes every later block. Content-defined chunking (cutting where a rolling hash of the data matches a pattern) keeps boundaries stable at the price of variable block sizes.

6. The architecture

  1. Clients
    • Desktop sync client (chunker, hasher, local index)
    • Mobile and web clients
  2. Edge
    • Load balancer
    • API gateway (auth, rate limits)
  3. Services
    • Metadata service (uploads, commits, journal)
    • Sharing and permission service
    • Notification servers (long-poll / WebSocket)
  4. Storage
    • Metadata DB, sharded by namespace
    • Block index
    • Object storage: hot tier
    • Object storage: cold and archive tiers
  5. Async
    • Namespace-changed events (pub/sub)
    • Garbage collector
    • Tiering job
    • Checksum scrubber
Upload, commit and sync paths for a file storage service. Desktop, mobile and web clients reach a load balancer and API gateway. The metadata service handles upload planning, commits, the change journal and sharing checks, and writes to a metadata database sharded by namespace, plus a block index. Clients upload and download block bytes directly to object storage with short-lived pre-signed URLs. After each commit the metadata service publishes a namespace-changed event; notification servers holding long-poll or WebSocket connections nudge subscribed devices, which then read the journal. Background jobs collect unreferenced blocks, move cold blocks to cheaper tiers and verify checksums.
  1. UploadThe client chunks and hashes the file, sends the hash list, receives pre-signed PUT URLs for missing blocks only, and uploads them in parallel straight to object storage. The URLs expire in minutes and name the exact object key.
  2. CommitThe client commits the block list against its base revision. The metadata service checks permissions, verifies the blocks exist and runs the transaction described above.
  3. NotifyAfter the transaction commits, the service publishes "namespace 12 changed". Notification servers wake every device subscribed to namespace 12.
  4. PullEach device calls GET /changes with its cursor, receives the journal rows after it, downloads any blocks it lacks, rebuilds the files locally and stores the new cursor.

The notification carries no file data, only a hint. That makes it safe to lose: a device that misses a nudge catches up on its next periodic poll, because the journal and the cursor are the source of truth. Long-polling is simpler to run through proxies; WebSockets cost less per idle device. Either works.

7. Scale it

  • Shard metadata by namespace. Every commit touches one namespace, so transactions stay on one shard. A very large team folder can become a hot shard; split it into sub-namespaces if you must. See sharding strategies.
  • Keep metadata relational. Commits need multi-row transactions and a conditional check, which is exactly what a sharded relational database does well. The block index is a plain key lookup and can live in a key-value store. SQL vs NoSQL covers the reasoning.
  • Keep API servers out of the byte path. Pre-signed URLs let object storage absorb the roughly 1.2 GB/s of uploads directly.
  • Version cheaply. A new revision is a new block list; unchanged blocks are shared with the old revision. Keep old revisions for a retention period (say 30 days, an assumed policy) and let the garbage collector drop blocks whose reference count reaches zero.
  • Tier by access. Blocks not read for 90 days move to an infrequent-access tier, and old revisions to an archive tier. Reads become slower and may cost a retrieval fee, so keep current revisions of recently opened files hot.
  • Coalesce notifications. A folder shared with thousands of people should send one nudge per device per short window, not one per commit.

8. What breaks, and what you do about it

Failure modes worth naming
FailureEffectMitigation
Client crashes mid-uploadSome blocks stored, no commit, orphan blocksResume by asking for missing blocks; garbage-collect uncommitted blocks after a grace period
Garbage collector races a commitA block is deleted just as a new revision references it: data lossTwo-phase delete: mark a block as deleting only if its count is still zero, treat marked blocks as missing when planning uploads, and remove the bytes after a grace period
Notification lost or server restartsOther devices are stalePeriodic journal poll as a fallback; cursor-based reads make catch-up exact
Metadata shard primary failsCommits for those namespaces pauseSynchronous replica promoted by failover; uploads to object storage continue meanwhile
Silent corruption on diskA block no longer matches its hashChecksum on read and a background scrubber; repair from replicas or erasure-coded fragments
Two devices edit the same fileSecond commit is staleReturn 409; the client keeps its version as a conflicted copy (next section)
Failure modes worth naming. Scroll sideways on a small screen.

9. Trade-offs to discuss

  • Conflicted copies or merging. For arbitrary binary files the server cannot merge two edits, so the losing client saves its version beside the original, for example q3 (Ana's laptop conflicted copy 2026-10-31).pdf, and downloads the winner. Nothing is lost, and the user decides. Automatic merging only makes sense for formats the system understands.
  • Block size. Larger blocks mean fewer index entries and requests but coarser delta sync; smaller blocks dedup better but multiply metadata. Around 4 MB is a common middle.
  • Fixed or content-defined chunks. Fixed blocks are simpler and fast; content-defined chunks survive insertions near the start of a file.
  • Dedup scope. Global dedup saves the most space but can reveal that a file already exists somewhere; per-user or per-namespace dedup is safer and still catches most duplication.
  • Consistency. Metadata needs strong consistency within a namespace (a commit check is meaningless otherwise); notifications and thumbnails can be eventually consistent. Consistency and CAP explains where to draw that line.

10. How to talk through it in the interview

Fictional example

Handling the conflict follow-up without losing the thread
Interviewer
Two laptops edit the same spreadsheet offline and both come online. What happens?
Candidate
Both uploaded their blocks, so no bytes are lost. Both commits name base revision 41. The first one wins and becomes 42. The second gets a 409, so that client renames its local file to a conflicted copy, commits it as a new file and downloads revision 42 of the original.
Interviewer
Why not just keep the newest one?
Candidate
Because device clocks disagree and "newest" would silently discard someone's work. The revision check is cheap and it makes the overwrite impossible.
  • Draw the blob store and the metadata store as separate boxes from the first minute.
  • Say "blocks first, then metadata" and explain why that ordering prevents dangling references.
  • Expect follow-ups on conflicts, dedup security and garbage collection, and leave time for them.

Practicing this design with Natively

Run the walk-through as a mock interview with a friend playing the interviewer, or read the prompt aloud yourself. Natively transcribes the conversation as it happens and can suggest talking points for each question as it comes in; comparing them with what you said is a quick way to spot gaps such as conflict handling or garbage collection. If you sketch the architecture, you can capture a screenshot of the diagram and ask Natively to explain it back. Afterwards, the transcript and notes show where you spent your time and what you left out.

Questions people ask

Why do file sync services split files into blocks?

Blocks of a few megabytes, each named by its content hash, let a client upload only the parts of a file that changed, resume an interrupted upload by asking which blocks are missing, and store identical blocks once. A file version then becomes an ordered list of block hashes in the metadata database.

How does Dropbox-style sync handle two devices editing the same file?

Each commit names the revision it was based on. The first commit wins; the second is rejected as stale, and that client saves its version as a conflicted copy beside the original instead of overwriting it. Nothing is lost, and the user decides which version to keep.

Should file metadata and file contents live in the same database?

No. File contents are large and immutable, so they belong in object storage, uploaded directly through short-lived pre-signed URLs. Metadata such as paths, revisions, block lists and permissions needs transactions and strong consistency, so it lives in a database sharded by namespace.

Where to go next

Practice with the same assistant you would use on a call: it listens, transcribes and suggests answers from your own resume and the job description.

Facts on this page were last checked on . Product details come from the Natively app itself; if something here is out of date, tell us at hello@heynatively.com.

Keep reading

Put it on your own machine.

A live-conversation assistant that runs locally. Your conversations stay on your machine.

macOS 12+ · Windows 10/11

© 2026 Natively AI Private Limited

Source-available under the Natively Personal Use Source License v1.0 · IP geolocation by DB-IP