File sync looks like a storage problem and is actually a metadata and conflict problem. The bytes are easy — blob storage solves that. Keeping many devices' views of a mutable tree consistent is the hard part.
Requirements
Functional
- Upload, download, delete files; sync across a user's devices.
- Share files and folders with permissions.
- Version history and restore.
- Offline edits that reconcile on reconnect.
Non-functional
- Never lose a file. Durability outranks everything else here.
- Sync should feel immediate — seconds, not minutes.
- Bandwidth-efficient: re-uploading a 2 GB file after a one-line change is unacceptable.
100M users, 10 GB average → 1 EB raw
but dedup + delta sync cut effective storage several-fold
Read:write ≈ 1:1, unusually — this is not a read-heavy systemChunking and content addressing
The single idea the whole design rests on: **split every file into fixed or content-defined chunks and name each chunk by the hash of its contents.**
Three properties fall out for free:
- Deduplication. Identical chunks are stored once — across versions, and across all
users. Ten thousand copies of the same company handbook cost one copy.
- Delta sync. Editing one page changes one chunk; the client uploads that chunk and a
new chunk list. A 2 GB file with a small edit costs a few MB.
- Integrity. The hash is the checksum — corruption is detectable by construction.
Fixed-size chunks (say 4 MB) are simple but suffer the insertion problem: adding a byte at the start shifts every boundary and invalidates every chunk. Content-defined chunking (a rolling hash such as Rabin fingerprinting picks boundaries based on content) keeps boundaries stable under insertion. Mention the trade-off; fixed-size is a fine answer if you name its weakness.
Architecture
Client (watcher + chunker + local DB of known state)
│
├─ Metadata service ──→ metadata DB (files, versions, chunk lists, ACLs)
│ │
│ └─→ notification service ──→ other devices (long poll / WebSocket)
│
└─ Block service ─────→ blob storage (chunks, content-addressed)Splitting metadata from blocks is the key structural decision. Metadata is small, highly relational (a tree, permissions, versions) and needs transactions — a relational database. Blocks are huge, immutable and need no queries — object storage. Different systems, different scaling.
The sync protocol
Upload:
1. Client chunks the file, computes hashes
2. Asks metadata service: which of these hashes do you already have?
3. Uploads only the missing chunks to blob storage
4. Commits the new chunk list as a new version (one transaction)
5. Metadata service notifies the user's other devices
Download:
1. Device receives "file X changed to version N"
2. Fetches the new chunk list, diffs against its local list
3. Downloads only the chunks it lacks, reassemblesStep 2 is what makes uploading an already-known file instantaneous.
Long polling or WebSockets push changes to online devices; a cursor-based change feed ("give me everything after revision 4,318") lets a device that has been offline for a week catch up in one request. Do not have clients poll for the full tree.
Conflicts
Two devices edit the same file while offline. Options:
- Last-writer-wins — simple, and silently destroys work. Rarely acceptable for files.
- Keep both — the standard file-sync answer: the loser is saved as
report (conflicted copy, Ana's MacBook).pdf and the user resolves it. Dropbox and Drive
both do this.
- Merge — only possible with structured content, which is what
collaborative editing solves with OT or CRDTs. For opaque binaries it is impossible.
Detect the conflict with a version vector or revision number per file: a client committing version N+1 must state which version it was based on. If that is no longer the head, it is a conflict, not an update.
Sharing and permissions
Permissions are set on a node and inherited down the tree, so a naive check walks to the root on every access. Two standard mitigations: cache the resolved ACL per node, and denormalise inherited permissions onto descendants, recomputed asynchronously when a share changes. Say which you would pick and why — a share change on a huge folder is a big asynchronous job either way.
Follow-ups to expect
- Encryption — at rest and in transit; note that end-to-end encryption breaks
cross-user dedup, since identical plaintext no longer produces identical ciphertext.
- Large-file upload — resumable, chunk-parallel, with a session that survives restarts.
- Trash and retention — soft delete, then garbage-collect chunks no version references.
- Storage tiering — move cold chunks to cheaper archival storage.
- Mobile — sync metadata always, download file content lazily on open.
- Quota — count logical bytes per user, not deduplicated physical bytes.
Track this problem on the System Design sheet.
Frequently asked
How does file sync avoid re-uploading an entire file after a small edit?
By splitting files into chunks addressed by the hash of their contents. An edit changes only the chunks it touches, so the client uploads those and commits a new chunk list. The server can also tell the client which hashes it already has, so uploading a file that exists anywhere in the system costs nothing but the metadata.
How do you resolve sync conflicts?
Detect them with a per-file revision number or version vector — a client must state which version its edit was based on, and if that is no longer the head, it is a conflict. For opaque files the standard resolution is to keep both, saving the loser as a conflicted copy for the user to reconcile. Automatic merging is only possible for structured content.
Why separate the metadata service from the block storage?
They have opposite characteristics. Metadata is small, relational and transactional — a tree with permissions and versions — which suits a relational database. Blocks are large, immutable and never queried, which suits object storage. Splitting them lets each scale on its own terms and keeps huge payloads off the transactional path.