Design Dropbox
Keep a folder identical on every device: upload huge files in resumable chunks, push only what changed, and reconcile two machines that both edited the same file offline.
Last updated 2026-09-22. Difficulty: hard. Patterns: chunking, dedupe, sync, object-storage, conflict-resolution. Reported at Dropbox, Google, Microsoft, Amazon, Meta, Atlassian.
Sit this as an AI interview and be asked it one question at a time; Study shows every answer, and Practice hides them until you have produced your own.
Functional requirements
- Upload and download files of any size. A 20 GB video and a 2 KB note go through the same path. Uploads resume after a dropped connection rather than starting again.
- Sync every change to every device. A file saved on a laptop appears on the phone without the user asking. The unit of sync is a change, not a folder scan.
- Only transfer what changed. Appending a row to a 2 GB CSV must not upload 2 GB. The client sends the chunks that differ and nothing else.
- Share files and folders. With another account, or as a public link. Permissions are viewer or editor, and revoking one takes effect on the next request.
- Version history and restore. Previous versions of a file for 30 days, restorable to any device, including after a delete.
- Work offline. Edits made with no network are queued locally and reconciled on reconnect, including when the same file changed on the server.
- Out of scope. Real-time collaborative editing of a document (that is a different design — see Google Docs), full-text search of file contents, and desktop-class file locking.
Non-functional requirements
- Durability (11 nines). Losing a user's only copy of a file is the one unrecoverable failure. Everything else is an inconvenience.
- Sync latency (< 5 s for a small change). From save on one online device to visible on another. Perceived as "instant"; anything over about ten seconds is perceived as broken.
- Upload throughput (saturate the client link). Parallel chunk uploads, so a fast connection is the limit rather than our ingest path.
- Availability (99.9 % writes, 99.99 % reads). A failed sync is retried later and is survivable. A download that fails during a presentation is not.
- Metadata consistency (read-your-writes per namespace). A device that just committed a change must see it in its own next listing, or the client will loop uploading it again.
- Storage efficiency (> 2× from dedupe). The same installer, deck or photo exists in thousands of accounts. Storing it once is the difference between a viable and an unviable business.
- Scale (100 M accounts, 10 M daily active). Metadata operations dominate request count; bytes dominate cost.
Back-of-envelope estimates
- Stored bytes: ~1 EB raw, ~400 PB after dedupe. 100 M accounts × 10 GB average stored = 1 EB logical. Global chunk dedupe plus compression typically removes 55–65 % on a consumer corpus, so ~400 PB physical.
- New bytes per day: ~300 TB. 10 M daily active × 30 MB of genuinely new content each = 300 TB/day ≈ 3.5 GB/s average, and peak is 3–4× that in the evening. Sized against that, not the average.
- Chunk size and count: 4 MB · ~100 B of index per chunk. 4 MB balances two costs: small enough that a one-line edit re-uploads little, large enough that a 10 GB file is 2 500 chunks rather than 10 million. 400 PB ÷ 4 MB = ~10^11 chunks × 100 B of index = ~10 TB of chunk metadata.
- Metadata operations per second: ~200 k/s. 10 M daily active devices, each polling or holding a connection and committing a handful of changes: assume 20 metadata calls per device per active hour over an 8-hour spread → ~200 k/s at peak. This is the number the metadata store must serve, and it is why metadata and bytes are separate systems.
- Long-lived connections: ~3 M concurrent. Peak-concurrent devices ≈ 30 % of daily active = 3 M. At ~30 k sockets per notification server that is ~100 servers; each connection is idle almost all the time, so memory per connection is what matters, not CPU.
- Egress at peak: ~40 GB/s. Downloads outweigh uploads roughly 4:1 on a sync product (every change is uploaded once and downloaded once per other device). Peak upload ~10 GB/s → ~40 GB/s egress, which is why chunk downloads go through a CDN and never through the API tier.
- Version history cost: +15 %. Keeping 30 days of versions stores only the chunks that changed, not whole files. On a corpus where a few percent of bytes change each month, history costs ~10–20 % extra rather than 30×.
Components
- Sync client (desktop · mobile agent): Watches the folder with an OS file-system event API, chunks changed files, keeps a local SQLite database of (path, chunk hashes, server version), and reconciles against the server. Most of this design's cleverness lives here: the server is mostly a metadata store and a bucket.
- Load balancer: Terminates TLS and spreads API calls. Separate pools for metadata calls (small, chatty) and for notification connections (long-lived, idle), because they scale on different resources.
- Metadata service: The authority on what exists: namespaces, paths, file versions, the chunk list of each version, and permissions. Every mutation is a transaction that bumps a monotonic cursor for the namespace. Never touches file bytes.
- Metadata DB (sharded SQL, by namespace): Files, versions, chunk lists and the per-namespace change journal. Sharded by namespace id so one user's (or one shared folder's) changes are a single-shard transaction and the journal is naturally ordered.
- Chunk service: Answers "which of these hashes do you already have?" and hands out short-lived presigned URLs for the ones we do not. Records chunk → storage location and a reference count. Bytes flow past it, not through it.
- Chunk index (key-value, hash-sharded): content_hash → (location, size, refcount). Roughly 10^11 rows, uniformly distributed by hash, read on every upload and written once per new chunk. The single biggest table in the system.
- Block storage (S3-class object store): Encrypted, erasure-coded chunks addressed by content hash. Immutable: a chunk is written once and never edited, which is what makes dedupe, history and caching all safe.
- CDN: Serves chunk downloads from the edge against signed URLs. Chunks are immutable and content-addressed, so they are infinitely cacheable — the ideal CDN workload.
- Notification service (long-poll / WebSocket): Holds a connection per online device and tells it "namespace 42 has changed past cursor 9971". It sends no content, only a nudge, so a missed message costs one extra poll and nothing else.
- Change bus (Kafka, partitioned by namespace): Every committed metadata change is published here. The notification service, the sharing service and the background jobs all read it, so adding a consumer never touches the write path.
- Sharing service: Owns membership and roles for shared namespaces, and public links with their expiry and password. A shared folder is a namespace mounted into several users' trees rather than a copy per user.
- GC & lifecycle: Expires versions past the retention window, decrements chunk reference counts, and deletes chunks whose count has been zero for a grace period. Runs deliberately late: deleting a chunk that is still referenced is data loss.
- Search & previews: Consumes the change bus to build filename search and to render thumbnails and document previews asynchronously. Off the critical path; a missing preview is cosmetic.
User flows
- A new file is saved on a laptop. The write path: chunk locally, ask what is missing, upload only that to storage directly, then commit metadata last so a file never half-exists.
- The client sees a file-system event, waits for the file to settle, and chunks it.
- Client asks the chunk service which of those hashes the server already has.
- Client uploads the missing chunks straight to block storage, several at a time.
- Client commits the new file version: path, size, ordered chunk list, and the version it believes it is replacing.
- The commit is published to the change bus; devices are notified and the previews job picks it up.
- The change reaches the user's phone. The read path: a nudge, a delta by cursor, and chunk downloads from the edge. The server never pushes content.
- The notification service reads the change from the bus and finds the connected devices for that namespace.
- Each online device is nudged over its held connection with nothing but the new cursor.
- The device asks for everything after the cursor it last processed.
- The device works out which chunks it is missing and fetches them from the CDN.
- The device assembles the file, writes it atomically, and records the new cursor.
- One row is appended to a 2 GB CSV. The case that decides whether the product is usable on a real connection. Nothing but the tail should move.
- The client re-chunks the file with a content-defined boundary, not a fixed offset.
- The probe returns exactly one missing chunk out of ~500.
- Four megabytes are uploaded instead of two gigabytes.
- Commit records a new version whose chunk list is 499 old hashes and one new one.
- Other devices download the one new chunk and splice it into their local copy.
- Two laptops edit the same file offline. The failure case a sync product is judged on. The rule: never silently lose a byte a user typed.
- Both devices are offline and each saves a different edit to /notes/plan.md at version 7.
- The first device reconnects and commits; the file becomes version 8.
- The second device commits with parent_version 7 and is rejected.
- The client resolves by creating a conflicted copy, and commits that as a new path.
- Both files sync to every device and the conflict is surfaced in the UI.
- A folder is shared with a colleague. A shared folder is one namespace mounted in two trees, not a copy. That choice decides the whole permission model.
- The owner shares /Work/Q4 with another account as editor.
- The namespace is mounted into the invitee's tree.
- The invitee's devices are nudged and pull the namespace from cursor zero.
- Either member's edits now fan out to both members' devices from the one namespace journal.
- Access is revoked; the mount is removed, and any chunks left unreferenced are collected later.
Deep dives
- How files are split. Fixed-size blocks, content-defined chunking, or whole-file storage? Content-defined chunking with a rolling hash, 1 MB minimum, 4 MB average, 8 MB maximum, chunks identified by SHA-256 of their content and stored immutably. A file version is an ordered list of chunk hashes. Small files under the minimum are stored as a single chunk, and files under about 64 KB are inlined into the metadata row so a folder of text notes does not cost a storage round trip each.
- Global dedupe and the leak it creates. Deduplicate chunks across all users, per user, or not at all? Global chunk dedupe, with the possession check done server-side rather than by trusting the client. A client that claims to have a chunk it did not upload must prove it by returning a challenge over a server-chosen byte range, and the first upload of a chunk by a given account is always performed in full. Reference counts are per chunk, incremented inside the commit transaction and decremented by the GC job, with a grace period before any chunk is actually deleted. Enterprise tenants can be pinned to per-tenant dedupe by policy.
- The metadata store and its shard key. How do you shard a file tree, and what does that cost you? Sharded relational storage keyed by namespace id: personal root, plus one namespace per shared folder. Each shard holds files, versions, chunk lists and an append-only journal with a monotonic per-namespace cursor. Commits are single-shard transactions using compare-and-set on the file version, with an outbox row published to the change bus by a relay. Cross-namespace moves are a copy plus a delete, which is honest about not being atomic and is rare enough not to matter.
- Bytes past the API, not through it. Should uploads and downloads go through your servers or straight to storage? Presigned, per-chunk, write-only URLs valid for fifteen minutes, with the object key derived from the content hash so a client cannot write a chunk under a name it does not own. Downloads go through the CDN against signed URLs with the same scoping. Quota is checked optimistically at probe time and enforced at commit; a user who exceeds it has the commit rejected and the orphaned chunks collected. Malware scanning consumes the change bus and quarantines the file version after the fact, which is the honest trade for this architecture.
- How a device learns something changed. Polling, long polling, WebSockets, or push notifications? A long-lived connection per online device (WebSocket, falling back to long poll) carrying only "namespace N is at cursor C", plus a cursor-based delta API that is the single source of truth. Mobile clients in the background are woken by platform push instead. Every client also polls on a long interval — a few minutes — so that a device whose connection silently died still converges. Nudges for a busy namespace are coalesced to at most one per second.
- History, deletes and what "delete" means. How long do you keep versions, and what happens when a user deletes a file? Versions are immutable and retained 30 days on consumer plans, longer on business plans. Delete writes a tombstone version: the file disappears from listings and from every device, and is restorable from trash for the retention window. A lifecycle job expires old versions, decrements chunk reference counts, and a separate GC deletes chunks whose count has been zero for a grace period. An explicit "delete permanently" request skips the trash but still goes through refcounting, because the chunk may not be ours alone to delete.