Design Dropbox
Dropbox is a file hosting and synchronization service. A user installs a client on multiple devices (laptop, phone, web), drops files into a magic folder, and those files appear on every other device within seconds. The same abstraction also supports sharing, version history, and collaboration.
What makes the problem interesting is the combination of huge blob payloads, tight sync latency, and multi-device consistency. You have to hold a durable petabyte-scale file store, a strongly consistent metadata database, and a low-latency push channel to every connected client, all at once.
This walkthrough follows the Interview Framework and is sized for a 45 to 60 minute interview.
Phase 1: Requirements
Functional Requirements
- Users can upload and download files of up to a few GB each through a desktop client, mobile app, or web app.
- Files sync across a user's devices. A change made on one device propagates to the others within seconds.
- Users can share files and folders with other users, with read or read-write permissions.
- Version history. Users can view and restore previous versions of a file for a configured retention window (say 30 days).
- Conflict handling. When two devices edit the same file while offline, both edits are preserved (typically as a "conflicted copy").
Out of Scope for the MVP
Mention and defer: full-text search, collaborative in-place editing (that is Paper or Docs), trash/recycle bin, mobile thumbnails, team admin console, enterprise audit logs.
Keep the narrative tight: upload, download, sync, share. Everything else is a follow-up. Interviewers will often push you toward sharing or conflict resolution, so have answers ready, but do not lead with them.
Non-Functional Requirements
- Scale: 500M+ users, on the order of a trillion files, hundreds of PB of stored bytes.
- Durability: at least 99.999999999% (11 nines). Losing a customer file is unacceptable.
- Availability: 99.99% on the metadata and notification paths; 99.9% on the bulk transfer path.
- Sync latency: p95 under 5 seconds from "file saved on device A" to "file visible on device B" for files under a few MB.
- Consistency: metadata is strongly consistent per user. Block storage is eventually consistent across regions but durable on first write.
- Bandwidth efficiency: only changed parts of a file should transfer, not the whole file.
The two properties that shape the whole design are block-level deduplication and incremental sync. These are what let Dropbox ship a 1 GB file after a 10 KB edit without uploading 1 GB.
Capacity Estimation
Assume a large consumer service:
| Metric | Assumption |
|---|---|
| Total users | 500M, 100M DAU |
| Files per user | 2,000 average, 50 new/modified per day |
| Average file size | 500 KB median, 10 MB mean (heavy tail from photos and videos) |
| Daily change events | 100M DAU x 50 = 5B/day, ~60K/sec average, ~500K/sec peak |
| Daily new bytes, pre-dedup | ~2.5 PB/day using the 500 KB median; the 10 MB mean implies ~50 PB/day if every event were an average-size file, which is a loose upper bound |
| After block dedup + compression | 2x to 4x reduction depending on workload mix (media dedupes less, documents more) |
| Metadata request rate | dominated by idle sync clients (delta polls, cursor checks), not commit rate |
The planes scale on different shapes, not just different magnitudes. Metadata request rate is driven by idle sync clients (every online device polls or listens for changes). Byte throughput is driven only by real edits. That is why the metadata path and the block path get scaled, partitioned, and operated independently. Treat the exact numbers above as order-of-magnitude anchors, not forecasts.
Phase 2: Data Model
Core Entities
User(
user_id UUID PK,
email VARCHAR UNIQUE,
created_at TIMESTAMP
)
Device(
device_id UUID PK,
user_id UUID FK,
platform ENUM('desktop','ios','android','web'),
last_seen_at TIMESTAMP,
push_endpoint VARCHAR
)
Namespace(
namespace_id BIGINT PK, -- a root folder, owned by a user or a shared folder
owner_user_id UUID,
shared BOOLEAN,
quota_bytes BIGINT
)
File(
file_id UUID PK,
namespace_id BIGINT FK,
parent_path TEXT, -- logical folder path
name TEXT,
current_revision_id UUID,
is_deleted BOOLEAN,
created_at TIMESTAMP,
updated_at TIMESTAMP,
UNIQUE(namespace_id, parent_path, name)
)
FileRevision(
revision_id UUID PK,
file_id UUID FK,
size_bytes BIGINT,
content_hash CHAR(64), -- SHA-256 of full file
block_manifest JSONB, -- ordered list of block hashes
modified_by_device_id UUID,
created_at TIMESTAMP
)
Block(
block_hash CHAR(64) PK, -- content-addressed: SHA-256 of block bytes
size_bytes INT,
storage_ref TEXT, -- pointer into blob store
refcount BIGINT, -- number of revisions referring to this block
created_at TIMESTAMP
)
Share(
share_id UUID PK,
namespace_id BIGINT,
path TEXT, -- file or folder being shared
shared_with_user_id UUID,
permission ENUM('read','write'),
created_at TIMESTAMP
)
SyncCursor(
device_id UUID,
namespace_id BIGINT,
last_delta_seq BIGINT, -- monotonic per-namespace change counter
(device_id, namespace_id)
)
Relationships
- One User owns one personal Namespace and may be a member of many shared namespaces.
- One Namespace contains many File rows organized by
parent_pathandname. - One File has a linear chain of FileRevision rows; only the latest is "current."
- One FileRevision references many Block rows through its manifest; blocks are shared across revisions and across users.
- SyncCursor tracks what each device has seen, so the server can ship only deltas since the last sync.
Call out the metadata/blob separation: file metadata lives in a strongly consistent relational store sharded by user, while file bytes live in a content-addressed blob store. They scale on completely different curves.
Block Size
Dropbox historically uses 4 MB blocks. The trade-off:
- Smaller blocks (e.g. 256 KB): better dedup granularity, finer delta sync, but more metadata rows and more round trips per file.
- Larger blocks (e.g. 16 MB): cheaper metadata, worse dedup on small edits, more network waste when a block changes.
4 MB is the common sweet spot for mixed office-document and media workloads.
Phase 3: API Design
Protocol Choice
- Client to API: REST over HTTPS for uploads, downloads, and metadata CRUD. Large uploads use chunked, resumable transfer.
- Push channel: a long-lived connection (WebSocket or long-poll) for sync notifications.
- Internal: gRPC between metadata, block, and notification services for typed, low-latency calls.
Upload
POST /v1/files/commit_batch
Body:
{
"namespace_id": 123,
"path": "/projects/report.docx",
"block_manifest": [
"sha256:aaa...",
"sha256:bbb...",
"sha256:ccc..."
],
"size_bytes": 9500000,
"client_modified_at": "2026-04-21T12:00:00Z",
"parent_revision_id": "rev-abc"
}
Before calling commit, the client runs a "have these blocks?" check and only uploads missing ones:
POST /v1/blocks/missing
Body: { "block_hashes": ["sha256:aaa...", "sha256:bbb...", "sha256:ccc..."] }
Response: { "missing": ["sha256:ccc..."] }
PUT /v1/blocks/sha256:ccc...
Body: <raw block bytes>
Response: 201 Created
Then commit creates the new FileRevision referencing the manifest.
Content-addressed blocks with a separate "missing check" API are the core of bandwidth efficiency. An unchanged block is referenced, not re-uploaded. This also dedupes across users when the same file is shared (everyone sends the same hash).
Download
GET /v1/files/{file_id}?revision_id=...
Response: { "block_manifest": [...], "size_bytes": ... }
GET /v1/blocks/{block_hash}
Response: raw bytes, served from CDN when possible
Sync Delta
POST /v1/sync/list_delta
Body: { "namespace_id": 123, "cursor": "seq:9_817_234" }
Response:
{
"changes": [
{ "type": "add", "path": "/a.txt", "revision_id": "..." },
{ "type": "modify", "path": "/b.txt", "revision_id": "..." },
{ "type": "delete", "path": "/old/c.txt" }
],
"cursor": "seq:9_817_301",
"has_more": false
}
Push Notifications
WS /v1/sync/listen
Server sends: { "namespace_id": 123, "latest_seq": 9817301 }
Client reacts by calling list_delta.
Sharing
POST /v1/sharing/invite
Body: { "path": "/projects/report.docx", "invitee_email": "a@b.com", "permission": "write" }
Do not push file contents over the WebSocket. The notification only says "there is something new in namespace 123 since sequence 9_817_234." The client decides what to fetch. This keeps the push channel cheap and stateless.
Phase 4: High-Level Design
Architecture
Write Flow (Desktop Sync Engine)
- The local watcher detects a changed file. The sync engine splits the file into 4 MB blocks and hashes each block with SHA-256.
- The engine computes a diff against the last known revision: a list of block hashes, some already present on the server, some new.
- Engine calls
blocks/missingto learn which hashes are new and uploads only those viaPUT /v1/blocks/{hash}. Uploads run in parallel with a small pool (say 4 to 8 connections). - Once all new blocks are durable, the engine calls
files/commit_batchwith the full manifest, the previous revision ID (for optimistic concurrency), and timestamps. - The metadata service writes a new
FileRevision, updatesFile.current_revision_id, bumps the per-namespaceDeltaLogsequence, and enqueues refcount deltas for each referenced hash (see the GC section below on why refcount updates are batched rather than transactional). - The metadata service publishes a notification (namespace + new sequence) to the notification gateway.
Push + Pull Read Flow
- Every online device holds an idle connection to the notification gateway. On sign-in, the client registers with its last-known cursor per namespace.
- When the DeltaLog ticks, the gateway finds all connected devices subscribed to that namespace and pushes a tiny "new seq available" message.
- The client calls
sync/list_deltawith its cursor and receives only the changed paths. - For each changed path, if content is needed locally, the client fetches the blocks it does not already have via
GET /v1/blocks/{hash}, preferring the CDN edge. Blocks are immutable by hash, so CDN entries can carry very long TTLs and evict purely on capacity, not on invalidation. - The client reassembles the file and persists it to disk, then advances its cursor.
Conflict Resolution
Concurrent edits while offline are the hardest part. Dropbox uses optimistic concurrency with conflicted copies:
- Each commit carries
parent_revision_id. If that ID is no longer the current revision server-side, the commit fails with a conflict. - The losing client re-commits its local revision under a derived name such as
report (Alice's conflicted copy 2026-04-21).docx. The server materializes this as a real entry in the namespace (newFilerow, newFileRevision, newDeltaLogsequence), so every device converges to seeing both files. - Both users now see both versions in their normal sync views and can merge manually. Nothing is lost.
Do not reach for CRDTs or OT here. Dropbox files are opaque bytes (Word docs, photos, zip archives). CRDT-style per-character merge only applies to a collaborative editor like Docs. For an opaque-file sync service, "last writer wins + conflicted copy" is the right answer.
Sharing Flow
- User A invites User B to a folder. A new
Sharerow links the folder's namespace to User B. - User B's next
sync/list_deltaon the shared namespace picks up every file that is currently in the folder. - Because all existing revisions already reference content-addressed blocks, no bytes re-upload and no new refcounts are needed. Adding the
Sharerow is a single metadata write. This is what makes "share a 50 GB folder instantly" possible: granting access is O(1), regardless of folder size.
Phase 5: Scaling & Trade-offs
How the Design Meets NFRs
- Durability: blocks are written to multi-AZ erasure-coded storage with per-block checksums; background scrubbers re-verify and repair. Metadata uses synchronous multi-replica commit.
- Availability: metadata and block planes scale and fail independently. An outage on the block plane still lets the metadata plane serve sync-list queries (clients just cannot fetch bytes yet). An outage on the notification gateway degrades to client-side polling.
- Sync latency: push notification carries only a cursor, so server-side fan-out (subscriber lookup + enqueue) is sub-millisecond. Actual delivery latency is bounded by each subscribing device's WebSocket RTT. The bulk of end-to-end latency is the client upload itself, which is bounded by block count and bandwidth, not server logic.
- Bandwidth efficiency: block-level dedup + incremental sync makes most edits O(diff size), not O(file size).
- Scale: metadata is sharded by
user_id(ornamespace_idfor shared folders). Block store is flat and infinitely horizontal because keys are content hashes.
Scaling Each Layer
| Layer | Bottleneck | Fix |
|---|---|---|
| Metadata DB | Per-user write hot keys (power users, bots) | Shard by user; within a shard, partition by namespace. Cap ops/sec per user; throttle abusive clients |
| DeltaLog | Single-partition writes per namespace | Per-namespace append-only log; a single shared folder with millions of members is still a single writer path, so cap shared-folder fan-out |
| Notification gateway | Millions of idle TCP connections | Use edge gateways terminating WebSocket close to users; each gateway holds ~1M connections with low RAM. Forward new-seq events from a pub/sub bus |
| Block service | Upload throughput | Stateless; horizontally scale. Route by block hash prefix to distribute load |
| Block store | Storage cost and tail latency on large GETs | Multi-region replication for hot blocks; erasure coding for cold; CDN for popular blocks (e.g. shared public files) |
| CDN | Cache miss storm on a viral public share | Two-tier cache (edge + origin shield); origin shield absorbs the thundering herd |
Reducing Sync Latency
- Upload in parallel. 4 to 8 concurrent block PUTs saturate most consumer links.
- Pipelined commit. The client can start
commit_batchthe moment the last block acknowledges; no need to wait for a separate confirmation step. - Nearest region upload. DNS or a signed upload-host endpoint routes the client to the closest ingress. Blocks are durably persisted in at least one region before commit; cross-region replication for locality and DR happens asynchronously afterwards.
- Fast-path push. The notification gateway sits in front of a pub/sub bus fed directly from the metadata commit path. Server-side hop from commit to push is sub-millisecond; per-device delivery is bounded only by the subscriber's WebSocket RTT.
- Keep the block plane out of the critical metadata path. A slow block upload should not delay metadata reads for other users.
Ask yourself "what is the minimum number of round trips between 'file saved' and 'peer sees change'?" In this design: one block-missing check, N block uploads in parallel, one commit, one push fan-out, one delta list, and N block downloads in parallel. Anything beyond that is wasted latency.
Garbage Collection of Blocks
Because blocks are deduped, you cannot delete a block just because its last referring revision was deleted. You need reference counting:
- On commit, enqueue increments for every hash in the manifest. Hot templates (empty-file hash, common boilerplate) would otherwise drive heavy write amplification, so refcount deltas are batched and applied asynchronously rather than transactionally with the metadata commit.
- On revision deletion (user deletes a file, or version-history retention expires), enqueue decrements for each referenced hash.
- A background sweeper looks for blocks with
refcount = 0older than a grace period (to protect against races) and deletes them from the blob store. - The manifest set is the source of truth. A periodic full recount over the manifest table reconciles incremental refcounts and catches drift. Incremental refcount is an advisory index, not the ground truth.
Getting refcount wrong is how you lose customer data. Always apply decrements only after the deletion is durable; always delete blocks with a grace period; always run the periodic manifest-based recount as the authoritative safety net.
Consistency and Trade-offs
| Decision | Option A | Option B | Chosen |
|---|---|---|---|
| Metadata consistency | Strong, single-region | Eventual, multi-region | Strong within region, async cross-region for DR |
| Conflict model | CRDT/OT merging | Last-writer-wins + conflicted copy | Last-writer-wins + conflicted copy (files are opaque) |
| Sync delivery | WebSocket push | Client polling | WebSocket push with poll fallback |
| Dedup scope | Per-user | Global across all users | Global for storage savings, with a proof-of-possession step to close the confirmation-oracle side channel (see Security Notes) |
| Block size | 256 KB | 4 MB | 4 MB, matches typical-file access patterns |
Security Notes
- Authorization is metadata-level, not block-level. A user's ability to read block
His governed by the manifest they are authorized to read. The block service still checks that the caller has a signed short-lived token tied to that specific hash and revision. - Dedup confirmation oracle. Global dedup has a well-known side channel: an attacker who already has a candidate file can probe
blocks/missingand learn whether any user in the fleet stores it. The mitigation is to require a lightweight proof-of-possession on dedup hits (for example, a server-issued nonce the client must HMAC with the block bytes) before crediting the block reference. For sensitive tiers, scope dedup per-namespace instead of globally. - Client-side encryption is optional and changes the dedup story: identical plaintext encrypted by different users produces different ciphertext, so cross-user dedup disappears. Enterprise tiers sometimes accept this trade.
- Shared link tokens are capability URLs; rotate them on permission revocation.
Deep Dive: Why Not Store Files as Single Blobs?
If you store a file as one opaque object, a 10 KB edit on a 1 GB file forces a full re-upload and a full new copy. Block-level chunking is what unlocks:
- Delta uploads: only changed blocks move.
- Cross-user dedup: duplicate content (forwarded attachments, shared templates) costs one copy across the fleet.
- Range reads for video/large-PDF preview: fetch only the needed blocks.
The cost is more metadata rows and more small GETs. For the Dropbox workload, the trade-off pays off.
Other Probes a Strong Interviewer Will Push On
Rename and move. These are metadata-only operations. The FileRevision manifest is unchanged, so no blocks move and no bandwidth is spent. The sync delta carries a rename event with both old and new paths so clients update locally without redownloading.
First-sync storm. When a user joins a 500K-file shared folder, the initial sync/list_delta returns pages of entries, and the client fetches blocks it does not already have. Pagination is mandatory; rate-limit the client's parallelism so one new member does not hot-spot the block plane.
Watcher reliability. FSEvents and inotify drop events under load and miss changes that happen while the client is stopped. The sync engine cannot trust the watcher alone: it runs a periodic full-tree hash scan to reconcile against the server manifest, and treats the watcher as a low-latency hint on top of that scan.
Permission revocation. Removing User B from a shared folder stops new sync but does nothing about bytes already on their disk. This is an accepted limitation, not a bug. Enterprise tiers layer on remote-wipe or DRM if they need stricter semantics.
LAN sync. Two desktops on the same network can exchange blocks directly using a local-discovery protocol, skipping the WAN for the block plane. Metadata still flows through the server so cursors and conflicts stay authoritative.
Bandwidth-aware throttling. The client caps upload and download rate per network (home vs office vs tether) and backs off under detected congestion. Otherwise a single large upload saturates the user's link and makes the product feel broken.
Common Pitfalls
Treating the notification channel as the data channel. Push messages should be tiny and carry only cursors. Sending bytes over WebSocket couples delivery to connection health and blocks independent scaling of the block plane.
Global metadata sharding instead of per-namespace. Each commit must atomically write a new FileRevision, bump the per-namespace DeltaLog sequence, and enqueue refcount deltas. Sharding by file ID or random hash scatters a namespace's rows across shards and forces distributed transactions on every commit. Sharding by namespace (or by user, with shared namespaces handled as their own shard-owners) keeps the commit path single-shard.
Forgetting block garbage collection. Refcounting is easy to implement wrong and silent to break. Always include a background periodic recount from manifests, not just incremental updates.
Promising real-time collaborative editing. This is a different product (Google Docs). For Dropbox-style sync, "seconds, not millis, with last-writer-wins" is the right promise.
Hand-waving conflict resolution. Interviewers push here. Be specific: optimistic concurrency on commit, conflicted copies on failure, and no server-side merging of opaque bytes.
Interview Checklist
Requirements
- Stated upload, download, sync, share, versioning as the core functional set
- Named 11-nines durability and 5-second sync latency as the pivotal NFRs
- Back-of-envelope: PB/day ingest, and the insight that metadata request rate is driven by idle clients while byte throughput is driven by real edits
Data Model
- User, Namespace, File, FileRevision, Block (content-addressed), Share, SyncCursor
- Explained the metadata / blob separation
API
- Block-missing + block PUT + manifest commit for upload
- Cursor-based sync list and a WebSocket push channel
- Sharing invite endpoint
High-Level Design
- Drew metadata plane, block plane, notification gateway, CDN
- Walked through write flow, read flow, conflict resolution
- Showed how sharing a 50 GB folder is a single metadata write, because existing revisions already reference the content-addressed blocks
Scaling & Trade-offs
- Per-layer bottleneck + fix table
- Sync latency reduction techniques (parallel blocks, nearest region, fast push)
- Block refcount GC and its failure modes
- Explicit trade-offs on consistency, conflict model, block size
Summary
| Concern | Decision |
|---|---|
| Storage model | Content-addressed blocks (SHA-256), 4 MB default |
| Metadata | Sharded SQL by user/namespace, strongly consistent per shard |
| Sync mechanism | Monotonic per-namespace delta log + WebSocket push + delta list |
| Upload path | "Missing blocks?" check, parallel block PUTs, commit with parent revision for optimistic concurrency |
| Download path | Manifest fetch, parallel block GETs, CDN-accelerated |
| Conflict model | Last-writer-wins with conflicted-copy files |
| Sharing | Share rows on namespaces; existing revisions already reference the blocks, so granting access is a single metadata write and costs no bytes |
| Durability | Multi-AZ erasure coding + checksums + scrub/repair |
| Global strategy | Strong consistency in-region, async cross-region replication for DR |
The defining properties of this design: metadata and bytes scale independently, blocks are content-addressed and globally deduped, and the push channel carries cursors not data. Everything else follows from those three choices.