File sync services like Google Drive and Dropbox let hundreds of millions of users store files in the cloud and keep them identical across every device. The interesting engineering is not “store a file in S3” — it’s doing so efficiently (don’t re-upload a 2 GB file because one byte changed), keeping many devices in sync in near real-time, and resolving conflicts when two devices edit offline. Let’s design it.
Understanding the Problem
Functional Requirements
- Upload / download — Store and retrieve files of any size.
- Sync — Changes on one device appear on all the user’s other devices automatically.
- Sharing — Share files/folders with other users, with permissions.
- Versioning — Keep a history of file versions; restore old ones.
- Offline edits — Work offline; sync when reconnected.
Non-Functional Requirements
| Requirement | Target |
|---|---|
| Durability | 99.999999999% (eleven nines) — never lose a file |
| Sync latency | Changes propagate to other devices in seconds |
| Bandwidth efficiency | Transfer only what changed, not whole files |
| Storage efficiency | Don’t store identical data many times |
| Availability | 99.99% for the metadata/sync service |
| Large files | Support multi-GB files reliably over flaky networks |
Scale Estimation
- 1 billion users, 100 million daily active.
- Average 100 GB stored per user → ~100 EB of raw data (deduplication and tiering matter enormously at this scale).
- Say 50 file changes/user/day across actives → ~5 billion change events/day ≈ ~60K/sec, bursty.
- Metadata (file tree, versions, chunk lists) is small per file but queried constantly during sync.
Two early conclusions: the bytes belong in cheap, durable object storage (S3-class), and the metadata belongs in a fast, queryable database — and the two scale very differently, so keep them separate.
Core Entities and APIs
The central idea is to stop thinking about “files” as opaque blobs and start thinking about chunks. A file is an ordered list of fixed-size chunks; a chunk is addressed by the hash of its content.
Data Model
-- Metadata DB (e.g. sharded relational / Spanner-like)
-- A logical file/folder in a user's namespace
CREATE TABLE files (
id UUID PRIMARY KEY,
owner_id UUID,
parent_id UUID, -- folder tree
name VARCHAR(255),
is_folder BOOLEAN,
latest_version BIGINT,
deleted BOOLEAN DEFAULT false,
updated_at TIMESTAMP
);
-- Each save creates a new immutable version
CREATE TABLE file_versions (
file_id UUID,
version BIGINT,
size BIGINT,
chunk_hashes TEXT[], -- ordered list of chunk content-hashes
created_at TIMESTAMP,
device_id UUID,
PRIMARY KEY (file_id, version)
);
-- A chunk is stored once, referenced by its content hash
CREATE TABLE chunks (
hash CHAR(64) PRIMARY KEY, -- SHA-256 of chunk bytes
size INT,
block_url TEXT, -- location in object storage
ref_count BIGINT -- how many versions reference it
);The chunk_hashes array is the heart of it: a file version is just an ordered list of content hashes. Two files (or two versions) that share content share chunk hashes, and therefore share stored blocks.
API Design
# Upload is a negotiation: tell the server your chunk hashes first
POST /files/{id}/prepare
body: { chunk_hashes: ["a1b2...", "c3d4...", ...] }
response: { missing: ["c3d4..."] } # server only wants chunks it lacks
PUT /blocks/{hash} # upload only the missing chunks
body: <chunk bytes>
POST /files/{id}/commit
body: { version, chunk_hashes: [...], size }
response: { version }
# Download
GET /files/{id} # metadata + chunk list
GET /blocks/{hash} # fetch a chunk (often from CDN)
# Sync — long-poll / push for changes since a cursor
GET /changes?cursor={last_seen}
response: { changes: [...], cursor: {new} }The prepare → missing → upload handshake is what makes sync cheap: the client announces the hashes it wants to store, and the server replies with only the ones it doesn’t already have.
High-Level Design
- Client (with a file watcher) detects local changes, chunks files, computes hashes, and talks to the server. It keeps a local database of file→chunk mappings so it knows what changed.
- Chunker splits files into chunks and hashes them (on the client for upload efficiency; conceptually a shared step).
- Metadata Service is the source of truth for the namespace: the file/folder tree, versions, and each version’s chunk list. Backed by a sharded, strongly-consistent database.
- Block Store is object storage (S3 / GCS), content-addressed: the object key is the chunk hash. Immutable, cheap, eleven-nines durable, and fronted by a CDN for downloads.
- Notification Service tells a user’s other online devices “something changed, pull the delta,” via long-poll or a persistent push connection.
Separating metadata from blocks is the key architectural move. Metadata is small, relational, and needs strong consistency (you must never show a file tree that points at a version that isn’t committed). Blocks are huge, immutable, and belong in object storage. They scale, fail, and cost very differently.
Deep Dive: Chunking and Content-Addressed Storage
Why chunk at all?
Storing whole files is wasteful. If you edit page 300 of a 500-page document, re-uploading the entire file wastes bandwidth, and storing the new copy wholesale wastes storage. Chunking fixes both.
Split each file into chunks (Dropbox famously uses ~4 MB blocks). Hash each chunk with SHA-256. The hash becomes the chunk’s identity and its storage key:
def chunk_file(path, chunk_size=4 * 1024 * 1024):
chunks = []
with open(path, "rb") as f:
while block := f.read(chunk_size):
h = sha256(block).hexdigest()
chunks.append((h, block))
return chunks # ordered list of (hash, bytes)Because the key is the content hash, storage is automatically deduplicated: writing a chunk that already exists is a no-op. Identical chunks — across versions, across files, even across different users (subject to privacy/encryption policy) — are stored exactly once. At Drive scale, dedup is the difference between 100 EB and a small multiple of that.
Fixed vs content-defined chunking
Fixed 4 MB boundaries are simple but suffer from the boundary-shift problem: insert one byte at the start of a file and every subsequent chunk boundary shifts, so every chunk’s hash changes and dedup fails. Production systems often use content-defined chunking (a rolling hash like Rabin fingerprinting sets boundaries based on content), so an insertion only changes the one chunk around it. Mention both in an interview; fixed-size is fine for a first pass, content-defined is the refinement.
Deep Dive: Delta Sync and Deduplication
This is where the design pays off. When a client saves a change:
- The client re-chunks the file and computes the new list of hashes.
- It compares against the previous version’s hashes — most chunks are identical.
- It calls
preparewith the full hash list; the server replies with only the missing hashes (the genuinely new chunks). - The client uploads just those few chunks, then commits the new version (which is just a new ordered hash list).
Editing one paragraph of a large document uploads a single 4 MB chunk, not the whole file. This delta sync cuts upload bandwidth by orders of magnitude for typical edits, and the download path is symmetric — a syncing device fetches only the chunks it’s missing.
def upload_new_version(file_id, local_chunks):
hashes = [h for h, _ in local_chunks]
missing = api.prepare(file_id, hashes) # server: which do you lack?
for h, block in local_chunks:
if h in missing:
api.put_block(h, block) # upload only new chunks
return api.commit(file_id, chunk_hashes=hashes) # metadata points at the listDedup via ref_count also makes deletion safe: a chunk’s block is only garbage-collected when no version references it anymore.
Deep Dive: Multi-Device Sync and Conflict Handling
Pushing changes to other devices
When a device commits a new version, the user’s other online devices must learn about it quickly. Each device holds a long-poll or persistent connection to the Notification Service and tracks a cursor (its last-seen change). On any change, the service nudges the device, which then calls /changes?cursor=... to pull the deltas and download the missing chunks. This keeps sync near real-time without devices polling constantly.
Because the metadata service is the source of truth, the protocol is: upload blocks first (they’re immutable and idempotent — safe to retry), then commit the metadata version last. If an upload fails midway, no half-written file is ever visible; the commit either happens with all chunks present or not at all.
Conflicts
Two devices edit the same file offline, then both reconnect. You cannot silently pick one and discard the other — that’s data loss. Options:
- Last-writer-wins is simple but loses an edit; unacceptable for documents.
- Conflicted copy (Dropbox’s approach): both versions are kept. One becomes the canonical version; the other is saved as
report (Alice's conflicted copy).docx. No data is lost, and the user resolves it. - Operational transforms / CRDTs are used by real-time collaborative editors (Google Docs), where edits merge at the character level — a different, heavier problem than file sync.
For a Drive/Dropbox design, conflicted copies are the expected answer: detect divergence by comparing the parent version each device edited from, and if they differ, keep both.
Large files and flaky networks
Chunking also makes large uploads robust: each 4 MB block uploads independently and is resumable, so a dropped connection only costs the in-flight chunk, not the whole file. Blocks are immutable, so retries are naturally idempotent.
Security
Chunks are compressed and encrypted at rest; many designs encrypt per-chunk. Note the tension with cross-user dedup: global dedup of encrypted data requires convergent encryption (the key derived from content), which has its own privacy trade-offs — worth a sentence in a senior interview.
Key Takeaways
| Decision | Choice | Why |
|---|---|---|
| File representation | Ordered list of content-addressed chunks | Enables dedup and delta sync |
| Metadata vs blocks | Separate DB vs object storage | They scale, cost, and need consistency differently |
| Block storage | S3-class, key = content hash | Immutable, eleven-nines durable, auto-deduplicated |
| Upload efficiency | prepare → upload only missing chunks |
Delta sync cuts bandwidth by orders of magnitude |
| Storage efficiency | Dedup via content hash + ref counts | Identical chunks stored once; safe GC |
| Multi-device sync | Notification service + per-device cursor | Near real-time pull of only the deltas |
| Consistency | Metadata service is source of truth; blocks first, commit last | No half-written files ever visible |
| Conflicts | Keep both as conflicted copies | Never lose a user’s edit |
Drive/Dropbox is a masterclass in doing less work: chunk the file, address chunks by content, transfer and store only what’s new. Interviewers push hardest on why you chunk and content-address, how delta sync falls out of it, and how you handle two offline edits without losing data. Have the chunk→hash→block model and the conflicted-copy answer ready.
