Design Dropbox (File Storage & Sync)
1. Problem & Real-World Context
Design a file storage and sync service: upload files, sync them across a user's devices, handle edits efficiently. The defining challenge isn't storage (that's [[Blob & Object Storage]] again) — it's efficient sync of changes to a large file without re-uploading/re-downloading the whole thing every time a user changes one paragraph in a large document, plus resolving conflicts when the same file is edited offline on two devices.
2. Requirements
Functional: upload/download files; sync changes across a user's devices automatically; support folders/sharing; handle offline edits, reconciled on reconnect.
Non-functional: minimize bandwidth on sync (don't re-transfer unchanged parts of a file); handle large files (GBs) without the whole file blocking on a single transfer; eventual consistency across devices is acceptable (a few seconds' sync delay is normal and expected).
3. Capacity Estimation
- At 500M users averaging 5GB stored each ≈ 2.5EB (exabytes) total — an enormous number that only makes sense as a justification for chunking + deduplication (§5), since storing every user's files as fully independent blobs at this scale, with massive real-world file overlap (the same popular PDF, the same OS files, the same photo shared by many people), would be needlessly wasteful.
- Metadata volume: far smaller than the content itself, but a huge number of small records (file/folder hierarchy, per-chunk references, sync state per device) — this metadata is the part that needs fast, transactional-ish access; the content itself just needs to be durable and cheap.
4. High-Level Architecture
flowchart LR
C[Client] --> Sync[Sync Client<br/>watches local FS]
Sync -->|chunk + hash| API[Sync API]
API --> Meta[(Metadata DB<br/>file tree, chunk refs)]
API --> Block[(Block/Chunk Storage<br/>blob storage, content-addressed)]
API --> Notify[Notification Service]
Notify -->|other devices,<br/>long-poll/WebSocket| C2[Other Devices]
Walking an upload/edit: the sync client (running locally, watching the filesystem for changes) detects a modified file → splits it into fixed-size chunks (§5) → hashes each chunk → compares against the chunks already known to exist (server-side) → uploads only the new or changed chunks, not the whole file → updates the file's metadata record (an ordered list of chunk references representing the current version) → notifies the user's other devices that a change is available, so they can pull just the new/changed chunks too.
5. Chunking — the Core Mechanism
Splitting a file into fixed-size blocks (e.g., 4MB each) before storage/transfer is what makes efficient sync possible:
import hashlib
CHUNK_SIZE = 4 * 1024 * 1024 # 4MB
def chunk_file(file_path: str) -> list[tuple[str, bytes]]:
"""Returns a list of (content_hash, chunk_bytes)."""
chunks = []
with open(file_path, "rb") as f:
while True:
data = f.read(CHUNK_SIZE)
if not data:
break
content_hash = hashlib.sha256(data).hexdigest()
chunks.append((content_hash, data))
return chunks
def sync_upload(file_path: str, known_chunk_hashes: set[str]) -> list[str]:
"""Uploads only chunks the server doesn't already have; returns the full ordered hash list."""
all_hashes = []
for content_hash, data in chunk_file(file_path):
all_hashes.append(content_hash)
if content_hash not in known_chunk_hashes:
upload_chunk(content_hash, data) # only new/changed content goes over the wire
return all_hashes
Why hash-addressed chunks, not chunk position/index: identifying a chunk by the hash of its content (content-addressable storage), rather than by its position in the file, is what enables two powerful things at once — resumable uploads (a dropped connection only needs to retry the specific chunk that failed, identified by its hash, not the whole file) and deduplication (if two different files, or two versions of the same file, happen to share an identical chunk, it's stored exactly once and referenced by both — this is where the 2.5EB estimate in §3 gets substantially reduced in practice, since many chunks across many users' files are byte-identical: common OS files, popular shared documents, unchanged portions of an edited file).
Fixed-size vs. content-defined chunking: fixed-size chunking (above) is simple but has a real weakness — inserting a single byte at the start of a file shifts every subsequent chunk boundary, meaning a tiny edit can invalidate nearly every chunk's hash even though almost all the actual content is unchanged. Content-defined chunking (using a rolling hash to pick chunk boundaries based on content patterns rather than fixed byte offsets) is more robust to small edits/insertions — worth naming as the production-grade upgrade if pushed on "what if the edit is an insertion, not an append."
6. Metadata Model
File: file_id, owner_id, name, folder_id, current_version_id
Version: version_id, file_id, chunk_hash_list (ordered), created_at, device_id
Chunk: content_hash, size, storage_location, ref_count
A file's current state is just an ordered list of chunk hashes — reconstructing the file is "fetch each chunk by hash, concatenate in order." Versioning (a real Dropbox feature — "restore to an earlier version") is nearly free in this model: an old version is simply a different ordered list of the same content-addressed chunks, most of which are still shared with the current version and don't need separate storage. ref_count on each chunk tracks how many file versions across the whole system reference it — a chunk is only actually deleted from storage once its ref count hits zero (no version of any file, for any user, still needs it).
7. Sync & Conflict Resolution
When a file is edited offline on two devices and both come back online, the same conflict problem as [[Distributed Key-Value Store]]'s concurrent-write case shows up, applied to files instead of KV pairs:
- Last-write-wins by timestamp: simplest, but can silently discard a real edit if device clocks are skewed — same caveat as the KV store page's LWW discussion.
- Version vectors per file (the file-sync analogue of vector clocks): detect whether the two devices' edits are genuinely concurrent (neither is a descendant of the other) versus one being a clean continuation of the other.
- The pragmatic real-world answer (what Dropbox actually does): on a genuine conflict, keep both versions — save the incoming conflicting edit as a separate file (
document (conflicted copy from device X).docx) rather than silently picking one and destroying the other. This sidesteps needing to auto-merge arbitrary binary file contents (which is often not even meaningful — merging two edited photos isn't well-defined the way merging two text diffs might be), at the cost of pushing the actual reconciliation decision to the user.
8. Real-Time Sync Notification
The other devices need to learn "something changed" promptly, without polling the API constantly. A long-lived connection (WebSocket, or long-polling as a fallback — see [[Communication Protocols]]) notifies other online devices that new chunks/metadata are available, prompting them to pull the update — the notification itself is lightweight (just "file X changed, new version Y"), with the actual chunk data fetched afterward via the normal chunked-download path, keeping the low-latency notification channel decoupled from the potentially large data transfer.
9. Deep Dives / Follow-ups
- Large file uploads (GBs): chunking already solves this incidentally — chunks are uploaded independently and can be parallelized (multiple chunks in flight at once) and resumed individually on failure, rather than treating a multi-GB file as one atomic transfer.
- Sharing a file/folder with another user: a permissions layer on top of the same metadata model — a shared file just has multiple
owner-equivalent references (or a separate ACL table) pointing at the same underlying version/chunk chain; the content-addressed storage layer doesn't need to know or care that a file is shared. - Storage tiering: infrequently-accessed old versions can be moved to cheaper, slower storage tiers (the chunk/version model makes this transparent — the metadata still points at the chunk, only its
storage_locationchanges) — the same idea underlying "cold storage" tiers on real cloud blob services.
10. Interview Drill Questions
- Why chunk files instead of storing/syncing each file as one blob? — Chunking enables uploading/downloading only the parts of a file that actually changed (bandwidth efficiency), resumable transfers (retry one failed chunk, not the whole file), and deduplication (identical chunks across different files/versions/users are stored once) — none of which are possible when a file is treated as one indivisible unit.
- A user edits a 2GB file by inserting one byte at the very beginning. What happens to sync efficiency with fixed-size chunking, and how would you fix it? — Every subsequent chunk boundary shifts by one byte, so nearly every chunk's content (and therefore hash) changes even though almost none of the actual content changed — this defeats deduplication/incremental-sync for that edit. Content-defined chunking (boundaries chosen by a rolling hash over content, not fixed byte offsets) is robust to insertions/deletions since only chunks near the actual edit point shift.
- Two devices edit the same file offline and reconnect with conflicting changes. What does the system do? — Rather than attempting to auto-merge arbitrary file content (often not well-defined for binary files), the pragmatic real-world answer is to save the conflicting version as a separate "conflicted copy" file, preserving both edits and pushing the reconciliation decision to the user rather than risking silently discarding one person's work.
- How does file versioning ("restore to yesterday's version") work without doubling storage for every version? — A version is just an ordered list of chunk hashes; since most chunks are unchanged between adjacent versions of a file, the vast majority of chunks are shared and referenced by multiple versions (tracked via
ref_count) — only the genuinely new/changed chunks from an edit consume additional storage. - Why not just use a message queue to push full file diffs between devices instead of a chunk-hash comparison? — A queue-based diff-push model still requires computing and transmitting a diff, and doesn't naturally give you cross-user, cross-file deduplication (the same chunk existing in someone else's completely different file) — content-addressed chunking gets both incremental sync and system-wide dedup from the same mechanism, rather than solving them as two separate problems.
Cross-links
[[Blob & Object Storage]] · [[Distributed Key-Value Store]] · [[Communication Protocols]] · [[CAP Theorem & Consistency Models]] · [[SQL vs NoSQL]]