Design Dropbox
Problem Statement#
"Design a file sync service like Dropbox. Users should be able to store files in the cloud and have them sync automatically across all their devices."
That's it. One sentence. The rest is yours to figure out.
Requirements Gathering#
This is where the interview actually begins. A strong candidate doesn't start drawing boxes. They ask questions. Not to stall, but because the answers fundamentally change the design.
Here's how a good requirements conversation looks:
Candidate: "Are we designing cross-device sync, or real-time collaborative editing like Google Docs? Those are very different problems."
Interviewer: "Cross-device sync. A file edited on one device shows up on the others."
Candidate: "What's the max file size? That decides whether we can treat a file as one blob or must chunk it."
Interviewer: "Files up to 10 GB. Design for large files."
Candidate: "Do we need versioning and conflict resolution when the same file is edited on two devices?"
Interviewer: "Yes. Keep version history, and handle concurrent edits sanely."
Candidate: "Mobile clients too, or desktop only? That drives how aggressive we get about bandwidth."
Interviewer: "Both. Bandwidth and battery matter on mobile."
Candidate: "Do users share files and folders with other people?"
Interviewer: "Yes, sharing with specific people, including folders."
Candidate: "What scale? Users and total storage?"
Interviewer: "700 million registered users, ~1.4 exabytes of data."
Candidate: "Out of scope: real-time co-editing, thumbnails and media transcoding, enterprise admin and compliance?"
Interviewer: "Correct, out of scope. Just sync, versioning, and sharing."
Functional and Non-Functional Requirements#
Functional Requirements#
- Users upload files from any device; files are stored durably in the cloud
- Files sync automatically to all of a user's connected devices
- Users share files and folders with specific people
- File versioning: restore previous versions (last 30 days)
- Conflict resolution when a file is edited on two devices at once
- Offline support: offline changes sync when connectivity returns
Non-Functional Requirements#
| Requirement | Target |
|---|---|
| Metadata consistency | Strong (file tree, versions) |
| Blob consistency | Eventual (briefly stale read acceptable) |
| Bandwidth | Transfer only changed bytes |
| Durability | 11 nines on blob storage (S3-class) |
| Scale | 700M users, files up to 10 GB, ~1.4 EB total |
Out of scope: Real-time collaborative editing (Google Docs OT/CRDT), thumbnails and media transcoding, enterprise admin/compliance controls
Capacity Estimates#
Work through the math. Don't just state the numbers.
The insight that reframes the whole problem: storing the bytes is solved: S3 does it. The number that matters is bandwidth. Re-uploading a 10 GB file because one paragraph changed is the failure mode, and chunking plus delta sync is what avoids it.
High-Level Design#
Two flows drive everything: a file changes locally and must go up, and a change elsewhere must come down to every other device. The sync client is the brains; the server is mostly bookkeeping and storage.
Write path (up): The client watches the filesystem, splits changed files into chunks, hashes each chunk, and asks the Sync Service which hashes are new. It uploads only the new chunks (via pre-signed S3 URLs), then commits a new version with a chunk manifest to the Metadata DB, which fires a change notification.
Read path (down): A notified device fetches the new manifest, diffs it against its local manifest, downloads only the chunks it's missing, and reassembles the file from local cache plus the new chunks.
The key insight: metadata and blobs live in different stores for different reasons. The file tree needs relational joins and strong consistency (renaming a folder is a transaction); the bytes need cheap, durable, content-addressed object storage. Deduplication then falls out for free: a chunk hash that already exists is never uploaded again.
Deep Dives#
1. Chunking#
Instead of uploading a file as one blob, the client splits it into fixed-size chunks (4 MB), hashes each with SHA-256, and represents the file as an ordered list of chunk hashes: the manifest.
Three things fall out of this one decision:
- Resumable uploads: a failed upload only retries the incomplete chunks, not the whole 10 GB
- Delta sync: editing one line in a 500 MB file re-uploads one 4 MB chunk, not 500 MB
- Deduplication: identical chunks (across versions, files, even users) share one stored copy, because identical bytes produce identical hashes
Fixed vs variable chunking. Fixed-size chunking has one weakness: insert a byte at the start of a file and every subsequent chunk boundary shifts, so every chunk hash changes and the whole file looks new. Content-defined chunking (Rabin fingerprinting) picks boundaries based on the content itself, so an insertion only disturbs the chunk it lands in. Dropbox uses this. Fixed-size is the right default to present; name variable chunking as the upgrade.
For senior interviews ↓ content-defined chunking and the boundary-shift problem
Rabin fingerprinting slides a rolling hash over the file and declares a chunk boundary whenever the low k bits of the fingerprint equal a fixed pattern. Because boundaries are anchored to byte patterns, not byte offsets, inserting or deleting bytes shifts only the boundaries local to the edit: the chunks before and after realign to the same content and keep the same hashes.
The trade-offs: chunk sizes become variable (you set a target average, plus min/max bounds to avoid pathological tiny or huge chunks), and the rolling hash costs CPU on the client. On battery-constrained mobile you might disable it and accept fixed chunking's worse delta ratio for insert-heavy files. The payoff is dramatic for exactly the workloads users hit constantly: prepending to a log, inserting a slide into a deck, editing the middle of a large document.
2. The Upload Flow and Deduplication#
The upload is a negotiation: the client proposes chunk hashes, the server says which it already has, and only the genuinely new bytes cross the wire.
Step 3 is the deduplication check and the whole trick: a chunk hash that already exists is not re-uploaded: those exact bytes are already in the block store, whether they came from this file, another version, or another user entirely. This is how Dropbox saves enormous amounts of storage and bandwidth. Uploading directly to S3 (step 5) keeps the large-payload path off the Sync Service entirely; the service only ever touches metadata.
3. Delta Sync on Download#
When a device is notified that file X changed, it does the mirror of the upload:
- Fetch the new chunk manifest from the Metadata DB
- Diff it against the locally cached manifest: which chunk hashes are new?
- Download only the new chunks from S3
- Reassemble the file from local cache + downloaded chunks
For a large file where one paragraph changed, the device pulls a single 4 MB chunk. The rest is already on disk from the previous version. This is why sync feels instant even on big files and slow connections.
4. Conflict Resolution#
Two devices edit the same file while offline. Both come online with new versions built on different manifests. The Sync Service now has a genuine conflict: neither version descends from the other.
Dropbox's actual strategy: preserve both. The later-arriving version is saved as filename (User's conflicted copy 2026-07-02).ext. Both survive; the user reconciles manually. This is the right interview answer.
Why not auto-merge? Automatic merge only works for known, structured formats (text via operational transforms, as in Google Docs). For arbitrary binary files (a PSD, a spreadsheet, a video) there is no safe automatic merge, and guessing risks corrupting data. Preserving both is the only choice that never loses work.
Last-writer-wins (rejected): simpler, but silently discards one user's changes. Tolerable for single-user-single-device files; unacceptable for shared folders, where it means one person's edits vanish without a trace.
For senior interviews ↓ detecting conflicts without trusting wall clocks
The subtlety is detecting the conflict correctly. You cannot use wall-clock timestamps: device clocks skew, and "later timestamp wins" both loses data and produces false conflicts when a slow clock makes a fresh edit look old.
Use a version vector (or a server-issued monotonic version number per file). Each device tracks the version it last synced from. On upload, the server checks: does the client's edit descend from the current server version? If yes, fast-forward: no conflict. If the client based its edit on version 5 but the server is already at version 7 (another device got there first), the histories have diverged and it's a true conflict → create the conflict copy. This is causal detection: a conflict is precisely two edits neither of which happened-before the other, and version vectors capture happened-before without any clock.
5. Offline Mode#
The client keeps a local SQLite mirror of the metadata for synced folders. Offline changes are recorded as pending operations. On reconnect:
- Ask the server for changes since the last synced version
- No server changes → upload local pending changes
- Server changes exist → run conflict detection (version vectors, per above) and resolve
- Apply everything, advance the local synced version
The sync anchor is a server-issued version number (or vector clock) per file, never a wall clock. This is what keeps offline edits from silently clobbering or falsely conflicting with edits that happened elsewhere while the device was dark.
Data Model#
ref_count drives garbage collection: when no surviving file version references a chunk, it can be deleted from S3. Decrementing it is what makes 30-day version pruning actually reclaim space.
API Design#
Note: owner_id and access checks are derived from the auth token server-side; the client never asserts ownership. Pre-signed URLs are scoped to a single chunk and expire quickly, so the large-payload path bypasses the Sync Service without opening a hole.
Failure Scenarios and Edge Cases#
For senior interviews ↓ what breaks and how the system recovers
Upload dies at chunk 7 of 10
Chunking makes this cheap. Chunks 1-6 are already in S3 (idempotent, since re-PUTting the same content hash is a no-op). On retry the client re-negotiates, the server reports only 7-10 as still needed, and the upload resumes. The version isn't committed until upload/complete, so a half-upload never produces a visible corrupt version.
Metadata DB write throughput
Every change writes a version row; at 1,000 uploads/sec with ~10 chunks each that's ~10k chunk-manifest writes/sec. Partition the metadata by owner_id for horizontal scale, batch chunk writes, and keep the hot path (dedup check + version insert) small. The bytes never touch this DB.
S3 request-rate hot prefix
S3 rate-limits per key prefix (~3,500 PUT / 5,500 GET per prefix). Because chunk keys are content hashes, they're already uniformly distributed across the keyspace, so requests spread naturally with no hot prefix. Note that modern S3 also auto-scales and repartitions hot prefixes on its own, so the old "inject a random prefix" trick is largely unnecessary here: the content-hash key already gives you the distribution for free. (It still matters if you ever key objects by something sequential like a timestamp or incrementing ID.)
Notification fan-out for a shared folder
A folder shared with 100 users means every change notifies 100 devices. Fan out asynchronously through a queue (Kafka/SQS) so the upload path isn't blocked waiting on 100 notifications; devices pull the change on their own schedule.
Concurrent edits (the conflict path)
Two offline devices edit the same file. Version-vector detection catches that neither version descends from the other, and the later arrival becomes a conflict copy. No silent data loss; the user reconciles.
Garbage-collecting shared chunks
A chunk is referenced by version 3 of file A and version 5 of file B. Deleting version 3 must not delete the chunk. ref_count guards this: the chunk is only removed from S3 when its ref count hits zero, and GC runs as a background sweep, not inline with delete.
Clock skew producing false conflicts
If you ordered by wall clock, a device with a slow clock would make fresh edits look stale and trigger phantom conflicts. Version numbers / vector clocks remove wall clocks from the decision entirely.
Code#
Chunked Upload with Deduplication#
Trade-off Summary#
| Decision | Chosen | Alternative | Why |
|---|---|---|---|
| Transfer unit | 4 MB chunks | Whole-file upload | Resumable, delta sync, and dedup all fall out of chunking |
| Chunk size | Fixed 4 MB | Variable (Rabin) | Simpler default; variable handles insertions better, offer as upgrade |
| Chunk addressing | Content hash (SHA-256) | Random UUID | Identical bytes → identical key → free deduplication across users |
| Conflict strategy | Preserve both (conflict copy) | Last-writer-wins | Never silently lose a user's work; only safe choice for binary files |
| Conflict detection | Version vectors | Wall-clock timestamps | Causal, skew-proof; wall clocks lose data and fake conflicts |
| Metadata store | PostgreSQL | DynamoDB | Relational joins + transactions for the file tree; strong consistency |
| Blob store | S3 | Self-hosted HDFS | Managed, durable, globally available, no ops overhead |
Follow-up Questions#
Basic
Q: Why chunk files instead of uploading them whole?
Three payoffs from one decision: resumable uploads (retry only the failed chunk), delta sync (edit one line in a 500 MB file, upload one 4 MB chunk), and deduplication (identical chunks share one stored copy because their hashes match). Whole-file upload gives you none of these and re-sends gigabytes for a one-byte change.
Q: How does deduplication work, and where does the saving come from?
Each chunk is addressed by the SHA-256 of its contents. Before uploading, the client asks the server which hashes already exist; existing ones aren't re-uploaded because those exact bytes are already in the block store. The same 4 MB block appearing in two files (or two users' accounts) is stored once. Industry benchmarks put duplicate chunks at 20-40%.
Q: Why split metadata and blobs into two different stores?
They have opposite requirements. The file tree needs relational joins and transactional consistency: renaming a folder or moving a file must be atomic. The bytes need cheap, durable, content-addressed storage at exabyte scale. Postgres is right for the first, S3 for the second; forcing either to do both job is where designs go wrong.
Q: What happens on a conflict?
Preserve both versions. The later arrival is saved as a conflict copy (filename (conflicted copy).ext) and the user reconciles. Auto-merge is only safe for structured formats like text; for arbitrary binary files, guessing risks corruption, so keeping both is the only choice that never loses work.
Senior follow-ups ↓
Q: A user inserts one byte at the very start of a 1 GB file. With fixed chunking, what happens, and how do you fix it?
With fixed 4 MB chunks, every boundary shifts by one byte, so every chunk hash changes and the client re-uploads the entire 1 GB, and dedup buys nothing. The fix is content-defined chunking (Rabin fingerprinting): boundaries are anchored to byte patterns, not offsets, so only the first chunk changes and the rest realign to their old hashes. You'd re-upload ~4 MB instead of 1 GB. It costs client CPU for the rolling hash, which is why mobile may keep fixed chunking.
Q: Two chunks from two different users happen to be identical. Is cross-user dedup safe?
Storage-wise, yes: identical bytes, one copy, guarded by ref_count so neither user's delete removes the other's data. The concern is privacy/security: naive existence checks can leak information (a client learns a chunk already exists, inferring another user has that exact content, a confirmation attack on known files). Mitigations: scope dedup per-account for sensitive tiers, or add server-side per-user encryption so identical plaintext yields different ciphertext (at the cost of losing cross-user dedup). It's a real trade-off between storage efficiency and information leakage.
Q: How do you garbage-collect chunks when versions are pruned, without deleting live data?
Reference counting. Each chunk row has a ref_count of how many file versions point at it. Pruning a version decrements the counts for its chunks; a chunk is eligible for deletion from S3 only when its count reaches zero. GC runs as a background sweep, not inline with the delete, and must handle the race where a new version references a chunk mid-sweep: typically a grace period or a mark-and-verify pass before the actual S3 delete.
Q: The interviewer asks for real-time collaborative editing on top of this. What fundamentally changes?
Chunk-level sync is too coarse: you can't merge two people typing in the same paragraph by swapping 4 MB blocks. You move to character/operation-level sync with Operational Transforms or CRDTs, a persistent connection instead of poll-and-notify, and server-side conflict resolution (merge) rather than conflict preservation (copy). It's essentially a different system layered above storage; the chunked file sync becomes the durability/snapshot layer beneath the live collaborative session.
Minute-by-Minute Interview Playbook#
0 to 5 min: Requirements Ask the questions above. The sync-vs-collaboration distinction and the max file size shape everything. Confirm versioning, sharing, and out-of-scope (no real-time co-editing). Write FR/NFR on the board.
5 to 8 min: Capacity estimates Do the math out loud, then reframe: storing bytes is solved, bandwidth is the real constraint. That reframing justifies chunking as the centerpiece rather than an afterthought.
8 to 18 min: HLD Draw the sync client, Sync Service, Metadata DB, and Block Store. Walk the happy path: edit → client chunks and hashes → dedup negotiation → upload new chunks to S3 → commit version → notify → other devices delta-download.
18 to 38 min: Deep dives Lead with chunking: it's the highest-value insight. Explain why whole-file upload is wrong, then the upload flow with the dedup check, then delta sync on download, then conflict resolution (conflict copy, and version vectors not wall clocks for detection).
38 to 45 min: Failure scenarios and wrap-up Cover resumable uploads, ref-count GC, and shared-folder notification fan-out. Summarize in one sentence: "Chunk files, address chunks by content hash for free dedup, sync only the delta, split metadata from blobs, and preserve both versions on conflict."
Green flags
- Proposes chunking immediately instead of whole-file upload
- Recognizes content-addressed storage makes dedup a free side effect
- Separates the metadata DB (file tree) from the blob store (chunks) and says why
- Has a clear, safe conflict policy and detects conflicts with version vectors, not clocks
- Mentions Rabin fingerprinting as the fix for the insertion problem
Red flags
- Stores files as BLOBs in a relational database
- No delta sync: "just re-upload the file when it changes"
- Last-writer-wins conflict resolution without acknowledging silent data loss
- Doesn't separate metadata from blob storage
- Never mentions deduplication
Further Reading#
Try a different category