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 Evin JohnPublished 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.
| Quantity | Assumption | Result |
|---|---|---|
| Users | 500 million registered, 50 million active a day | Size metadata for 500 million, traffic for 50 million |
| File changes | 2 new or edited files per active user a day | 100 million commits a day, about 1,160 a second; plan for 3x at peak, about 3,500 |
| Upload volume | 1 MB of changed data per commit on average | 100 TB a day before dedup, about 1.2 GB/s (roughly 9 Gbit/s) on average |
| Stored data | 2 GB per registered user on average | 500 million x 2 GB = 1 EB before redundancy |
| Metadata | 200 files per user, about 1 KB per file row | 100 billion rows, about 100 TB before version history |
| Block index | 1 EB in 4 MB blocks, about 100 bytes per index entry | at least 250 billion blocks, about 25 TB of index |
| Open sync connections | 25 million devices online at peak, 50,000 per notification server | about 500 notification servers |
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" }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.
| Table | Key fields | Why it is there |
|---|---|---|
files | namespace_id, file_id, parent_id, name, latest_rev, deleted | The folder tree and the current version pointer |
revisions | file_id, rev, ordered list of block hashes, size, device_id, created_at | Every version of a file; restoring a version is just pointing back at an old block list |
journal | namespace_id, seq (increasing per namespace), file_id, op | The change feed that clients read from their cursor |
acl | namespace_id, principal_id, role | Who can read or write a namespace |
blocks | hash (key), storage location, size, ref_count, tier | The block index: does this block exist, where is it, is it still used |
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
- Clients
- Desktop sync client (chunker, hasher, local index)
- Mobile and web clients
- Edge
- Load balancer
- API gateway (auth, rate limits)
- Services
- Metadata service (uploads, commits, journal)
- Sharing and permission service
- Notification servers (long-poll / WebSocket)
- Storage
- Metadata DB, sharded by namespace
- Block index
- Object storage: hot tier
- Object storage: cold and archive tiers
- Async
- Namespace-changed events (pub/sub)
- Garbage collector
- Tiering job
- Checksum scrubber
- 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.
- 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.
- NotifyAfter the transaction commits, the service publishes "namespace 12 changed". Notification servers wake every device subscribed to namespace 12.
- PullEach device calls
GET /changeswith 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 | Effect | Mitigation |
|---|---|---|
| Client crashes mid-upload | Some blocks stored, no commit, orphan blocks | Resume by asking for missing blocks; garbage-collect uncommitted blocks after a grace period |
| Garbage collector races a commit | A block is deleted just as a new revision references it: data loss | Two-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 restarts | Other devices are stale | Periodic journal poll as a fallback; cursor-based reads make catch-up exact |
| Metadata shard primary fails | Commits for those namespaces pause | Synchronous replica promoted by failover; uploads to object storage continue meanwhile |
| Silent corruption on disk | A block no longer matches its hash | Checksum on read and a background scrubber; repair from replicas or erasure-coded fragments |
| Two devices edit the same file | Second commit is stale | Return 409; the client keeps its version as a conflicted copy (next section) |
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
- 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.
