Index/Dropbox / File Sync

SponsorGitHub
Design15 min

Dropbox / File Sync

Syncing files across devices without re-uploading a two gigabyte file because one paragraph changed.

Primary concepts and the hard part

Concepts
content-defined chunkingdeduplicationdelta syncobject storagemetadata vs bytes separationversioningconflict resolutionlong-polling/notification for syncMerkle-style hashing
The hard part they’re probing
Bandwidth efficiency. Re-uploading a 2 GB file because someone changed one paragraph is the naive design. They want chunking, hashing, and delta sync — plus a coherent story for what happens when two clients edit offline and both come back.

Requirements

Functionalwhat it must do
Upload/download files, sync across a user's devices
Sync only what changed (not whole files)
File versioning and restore
Share files/folders with other users, with permissions
Work offline; reconcile on reconnect
Non-functionalhow well it must do it
Bandwidth-efficient (mobile, metered connections)
Sync latency: changes propagate to other devices within seconds
Durable: never lose a file. Availability high but a failed sync must be retryable, not destructive
Support large files (GBs) over unreliable connections
Out of scopedeliberately left out
Real-time collaborative editing (that's design 15)
Full-text search of contents
Scale
Users:            500M, avg 3 devices
Files:            100B files
Daily uploads:    1 PB/day
Dedupe savings:   ~50% cross-user (same PDFs, installers, media everywhere)
Chunk size:       ~4 MB average (variable, content-defined)
Metadata ops:     far higher QPS than byte transfers — the metadata service
                  is the real high-traffic service
Conclusion
Bytes go to object storage and scale trivially; metadata is the hot path. Most sync traffic is "what changed?" polling, not file transfer. Design the metadata service for QPS and the storage layer for volume — they're different problems.

API / Model

POST
/v1/files/prepare
{path, chunk_hashes[]}
dedupe check
200OK{missing_chunks[]}
PUT
<presigned url>
upload only the missing chunks, in parallel
200OK
POST
/v1/files/commit
{path, chunk_hashes[], size, mtime}
201Created{file_id, version}
GET
/v1/delta?cursor=
the sync primitive
200OKchanges since cursor
GET
/v1/files/{id}/versions
200OKversion list
POST
/v1/shares
{file_id, user_id, permission}
201Created
primary or partition keysort keyforeign key → referenced columnnullableHover a table or column to trace its keys

The chunks table being global and keyed by content hash is the whole dedupe story: if any user anywhere has already uploaded a chunk with that hash, nobody uploads it again.


High-level architecture

DatabaseObject storageFocusClick a node for details

Sync is split three ways: the client turns file changes into hashed chunks, the Metadata Service decides which chunks are missing and records versions, and object storage holds the bytes. Other devices find out about changes through the user's journal and a notification poke.

  1. The filesystem watcher sees a change. The content-defined chunker splits the file into chunks of about 4 MB, the client hashes each one with SHA-256 and updates its local index, and then it sends the hashes with POST /prepare. The Metadata Service checks auth, quota and the path, and returns only the hashes it doesn't already have.
  2. For those missing chunks it hands out presigned URLs, and the client uploads the bytes straight to object storage, where each chunk is stored under its chunk_hash.
  3. The client commits the ordered chunk list. The Metadata Service writes the new version to the Metadata DB, on the user's shard, and appends an entry with the next seq to the user's journal.
  4. The journal entry triggers the notification service, which pokes the user's other devices over long-poll or WebSocket without sending any payload.
  5. Each device calls GET /delta?cursor=last_seq, compares the changes with its local index, downloads only the chunks it lacks, reassembles the file and advances its cursor.

The conflict branch applies to a device that edited the same file while offline. Its commit carries the base version it started from. If that is still the current version, the change is applied. If not, both files are kept: report.docx and report (conflicted copy).docx. In the background, object storage keeps a refcount on every chunk and deletes unreferenced chunks lazily.


Trade-offs and deep dives

01
Content-defined chunking is the central idea

With fixed-size chunks, inserting a single byte at the start of a file shifts every subsequent boundary, so every chunk hash changes and you re-upload the entire file. Content-defined chunking (a rolling Rabin fingerprint that declares a boundary when the hash of a sliding window matches a pattern) makes boundaries depend on content, so an insertion only disturbs the chunk it lands in. One edited paragraph in a 2 GB file means uploading ~4 MB. This is the difference between a usable product and an unusable one, and it's the single detail most worth getting right in this interview.

02
Deduplication operates at three levels
  • Within a file: repeated blocks stored once.
  • Across a user's versions: v5 and v6 of a document share nearly all chunks, so versioning is nearly free.
  • Across all users: the same Ubuntu ISO uploaded by 10,000 people is stored once, because the key is the content hash.

Global dedupe typically halves storage. The privacy caveat worth raising: content-addressed global dedupe leaks information — an attacker who can observe "this chunk already exists" can confirm whether a specific file exists in the system. Mitigate by scoping dedupe per user or per organization, or by adding a per-tenant salt to the hash. Raising this unprompted is a strong signal, because it shows you think about the security consequences of an optimization.

03
Metadata and bytes scale separately

The metadata service handles far more QPS than the byte pipeline (every client polls for deltas constantly, but most polls find nothing). Shard metadata by user_id so all of a user's files, versions, and journal entries live on one shard, making a commit a single-shard transaction and the delta query a single-shard range scan. Bytes go to object storage, which scales without your involvement.

04
The journal is the sync primitive

Rather than diffing whole file trees, each user has a monotonically increasing sequence of changes. A client stores its cursor and asks "what's happened since N?" This is cheap, incremental, and resumable — a client that's been offline for a month makes one request, not a full tree walk. It's the same idea as a replication log.

05
Notification is a poke, not a payload

The notification service tells devices "something changed"; the device then pulls the delta. This keeps the notification path tiny and stateless, and means a missed notification is harmless — the next poll catches up. Long-polling is usually sufficient and cheaper than WebSockets here, since the update rate per user is low. Choosing long-poll over WebSockets with that reasoning is a good judgment signal.

06
Conflict resolution: keep both, always

You cannot merge two versions of a binary file, and silently picking a winner destroys someone's work. Detect conflicts by having the client send the base version it edited from; if the server's current version differs, it's a conflict. Then create a conflicted copy with both preserved and let the human decide. Last-write-wins is the wrong answer here and saying why is worth more than proposing a clever merge algorithm that can't exist.

07
Uploads are direct, parallel, and resumable

Presigned URLs so bytes never traverse your servers, parallel chunk uploads to saturate bandwidth, and per-chunk retry so a dropped connection costs one chunk rather than the whole file. The client can also resume by re-running /prepare — the server tells it which chunks are still missing, which is inherently idempotent.

08
Reference counting and garbage collection

Chunks are shared, so you cannot delete a chunk when one file referencing it is deleted. Maintain a refcount, decrement on delete, and run a background GC for zero-refcount chunks after a grace period. Do the deletion lazily and conservatively — deleting a still-referenced chunk is unrecoverable data loss, so err toward retaining garbage.

09
Deletes must be soft

Mark deleted, keep for 30 days, then purge. Users restore files constantly, and an immediate hard delete of a chunk that's still referenced by an old version is catastrophic.

10
Sharing crosses shard boundaries

A file shared from user A to user B lives on A's shard but must appear in B's tree. Store a lightweight reference in B's namespace pointing at A's file, and include shared-file changes in B's journal by having the share create a subscription. This is a fan-out problem again, bounded by the number of collaborators.

11
Bandwidth-adjacent optimizations

Compress chunks before upload where the content is compressible (skip already-compressed formats — detect by entropy or extension). Use a CDN for downloads of widely-shared files. Throttle background sync so it doesn't saturate a user's connection while they're working.


Possible follow-up questions

7 questions·try answering before you reveal