Dropbox
Walk through a complete Dropbox design: content-addressed chunking for delta sync, conflict copy resolution, and petabyte-scale chunk deduplication for 500M users.
What is Dropbox?
Dropbox is a cloud file-sync service: upload a file on one device and it appears on every other device you own within seconds. Three hard problems hide behind that simplicity: avoiding re-upload of unchanged file content on every edit, preserving both versions when two devices edit the same file offline, and deduplicating 500 petabytes of data where most bytes are identical across users. Content-addressed chunking is the architectural mechanism that solves all three at once.
This makes Dropbox a rich interview question. It touches content addressing and chunked storage, sync conflict resolution in an eventually-consistent system, hierarchical metadata modeling, and at-scale notification delivery. I'd recommend leading with the chunking insight early in the interview, because every other design decision (sync, dedup, versioning) falls out of it naturally.
Functional Requirements
Core Requirements
- Users can upload files from any device.
- Files automatically sync to all of a user's connected devices after an upload.
- Users can share folders and collaborate with other users.
- Users can view and restore previous versions of files.
Below the Line (out of scope)
- Public file sharing via link
- Real-time collaborative editing (that's Google Docs territory)
- Advanced team permissions, audit logs, and compliance reporting
- Media preview and thumbnail generation
The hardest part in scope: Efficiently syncing large files without re-uploading them in full on every edit. The mechanism behind this, content-addressed chunking with client-side delta computation, is the architectural keystone of the entire system.
Public link sharing is below the line because it introduces its own access-control and abuse-prevention surface without changing the upload or sync architecture. To add it, I would generate a short-lived signed token for the file, store it in Redis with a configurable TTL, and serve downloads via a CDN edge with token validation. The file data path is identical to authenticated download.
Real-time collaborative editing is below the line because it requires operational transformation or CRDT logic at the application layer. Dropbox is a sync system, not an editor. Integrating collaborative editing would effectively mean building Google Docs alongside Dropbox.
Advanced team permissions are below the line because they introduce a role and policy evaluation tree that sits on top of, but does not change, the underlying file storage and sync architecture. To add them, I would replace the simple folder_members table with a full RBAC model and evaluate policies at the API Gateway level on every request.
Media thumbnails are below the line because they are a read-side enhancement. To add them, I would trigger an asynchronous job on each confirmed upload that generates thumbnails and stores them in S3 under a predictable key, then serve them via CDN with a separate GET endpoint.
Non-Functional Requirements
Core Requirements
- Durability: Files must never be lost. 11-nines durability, matching S3's guarantee for underlying chunk storage.
- Availability: 99.99% uptime. Availability over consistency: a sync delay of a few seconds is acceptable; losing a file or a confirmed upload is not.
- Consistency: Online devices receive sync notifications within 5 seconds of a change. Offline devices sync on reconnect.
- Latency: Upload begins streaming within 1 second of a file change being detected. Chunk uploads are parallel with no queuing at the application tier.
- Scale: 500M users, 100M DAU. Average 1GB stored per user = 500 PB total. 5 file changes per DAU per day = 500M changes/day = ~5,800 writes/second at average load, peaking at 3x (~17,000/second) during business hours. Max file size: 5GB.
Below the Line
- Sub-100ms sync latency via edge routing
- HIPAA-compliant encryption key management per user
Read/write ratio: Roughly 5:1 reads to writes. Users download and open files far more often than they upload them. Reads are further amplified by multi-device sync: a single upload triggers downloads on all of a user's other devices. Write throughput is the harder engineering constraint at 17,000 peak writes/second; read throughput is handled by CDN caching of S3 chunks.
I target 5-second end-to-end sync for online devices. A filesystem event propagates in under 100ms, chunk hashing takes 1-2 seconds for a typical file, and the network upload of only changed chunks takes the bulk of that budget. The 5-second window is generous but bounded by client-side hashing time, not server-side architecture. I'd state this latency budget upfront in an interview because it immediately tells the interviewer you understand where the bottleneck actually lives.
Core Entities
- File: Metadata record for a file:
file_id,name,owner_id,folder_id,size,content_hash(SHA-256 of the full file),created_at,updated_at,current_version_id. - Chunk: A fixed-size (4MB) block of file data identified by its SHA-256 hash. Multiple files may reference the same chunk if they share content. Chunk data lives in S3; the registry lives in PostgreSQL.
- Folder: A container for files and sub-folders. Has
folder_id,name,parent_folder_id,owner_id, and a materialisedpathstring encoding ancestor folder IDs. - FileVersion: Every confirmed upload creates a new
FileVersionrecord linking to the chunk manifest at that point in time. Restoring a version is a metadata operation, not a data movement. - User: Account with
user_id, device list, and storage quota. Schema details are deferred to deep dive 4.
Schema details, indexes, and partition key design are covered in the metadata storage deep dive. The five entities above are sufficient to drive the API design and High-Level Design.
API Design
Start with one endpoint per functional requirement, then evolve where the naive shape breaks down.
FR 1: Upload a file (naive):
POST /files
Body: { name, folder_id, size, file_bytes }
Response: { file_id }
FR 1: Download a file:
GET /files/{file_id}
Response: { file_id, name, size, download_url }
These two endpoints satisfy the upload requirement on paper, but POST /files with raw bytes means the Upload Service must receive every byte of every file. For a 5GB file at scale, that is a bandwidth bottleneck that cannot be horizontally scaled away (you would need to buffer the entire file before writing to S3). The upload endpoint must be redesigned as a three-step protocol: declare the manifest, upload chunks directly to S3, confirm.
FR 1 (evolved): Declare chunk manifest:
POST /files
Body: { name, folder_id, size, content_hash, chunks: [{ index, chunk_hash }] }
Response: { file_id, missing_chunks: [chunk_hash], upload_urls: { chunk_hash: presigned_url } }
FR 1 (evolved): Upload a chunk directly to S3:
PUT {presigned_url}
Body: raw chunk bytes
Response: HTTP 200
FR 1 (evolved): Confirm upload complete:
PUT /files/{file_id}/confirm
Body: { chunks: [{ index, chunk_hash }], device_last_sync_token }
Response: { file_id, version_id }
Why three steps instead of one: The manifest step lets the server identify which chunks are already stored before any bytes transfer. The pre-signed URL step offloads bulk data transfer from the Upload Service directly to S3, so the Upload Service only handles lightweight metadata coordination regardless of file size. The confirm step is the authoritative write: metadata and version records are only committed once all chunks are durably in S3.
device_last_sync_token in confirm: The timestamp of the last sync event the uploading device processed. The server compares it to
file.updated_atto detect whether another device modified the file while this device was offline. If so, a conflict copy is created rather than overwriting. Covered in deep dive 2.
FR 2: Poll for sync changes (long-poll):
GET /sync/changes?cursor={timestamp}&timeout=30
Response: { changes: [{ file_id, event_type, version_id }], new_cursor }
The cursor is the updated_at timestamp of the last delivered change. Long-poll with a 30-second timeout means the connection hangs open until an event fires or the timeout expires, then the client immediately re-connects. This avoids a persistent WebSocket per device while still delivering changes within seconds.
FR 3: List folder contents:
GET /folders/{folder_id}/contents
Response: { files: [...], folders: [...] }
FR 3: Share a folder:
POST /folders/{folder_id}/members
Body: { user_id, permission: "view" | "edit" }
Response: { share_id }
FR 4: List file versions:
GET /files/{file_id}/versions
Response: { versions: [{ version_id, size, created_at, created_by_device }] }
FR 4: Restore a version:
POST /files/{file_id}/restore/{version_id}
Response: { file_id, version_id }
Restore is a POST, not a PUT, because it creates a new version record pointing to the historical chunk manifest. No file bytes move; the metadata pointer changes. The response triggers a FileChangedEvent that syncs all connected devices.
High-Level Design
1. Users can upload files
Start simple: the client detects a file change, reads the file, and POSTs the raw bytes to the Upload Service, which writes them to S3 and records metadata in PostgreSQL.
Components:
- Client Sync Engine: Monitors the filesystem for changes and POSTs the full file bytes to the Upload Service.
- Upload Service: Receives file bytes, writes them to S3 under a generated key, records metadata in PostgreSQL.
- Object Store (S3): Stores file data, one object per upload.
- Metadata DB (PostgreSQL): Stores file records:
file_id,name,owner_id,folder_id,size,s3_key.
Request walkthrough:
- Client detects a modified file via filesystem watcher.
- Client reads the entire file.
- Client POSTs the raw bytes to
POST /files. - Upload Service writes the bytes to S3 under a generated key.
- Upload Service inserts file metadata into PostgreSQL and returns
file_id.
This works for small files. It breaks immediately on the scale NFR: editing one paragraph of a 1GB file re-uploads 1GB. At 100M DAU making 5 edits per day on 50MB average files, that is 25 PB of daily upload traffic, most of it unchanged bytes. The Upload Service becomes a bandwidth bottleneck routing every byte through itself, and two users uploading the same Node.js installer store two full copies with zero sharing.
I always start with this naive design in interviews, not because it's viable, but because the specific way it breaks is what motivates every subsequent design decision.
Evolving the design: content-addressed chunking
The key insight is that most of a file's content does not change between edits. If each block of bytes is identified by its SHA-256 hash, the server can answer "which blocks are new?" without reading any file content (just check whether the hash is in the registry). The client then uploads only the missing blocks, in parallel, directly to S3.
Components (evolved):
- Client Sync Engine: Splits modified files into 4MB chunks, computes SHA-256 per chunk, and orchestrates a three-step upload: declare the manifest, upload only missing chunks directly to S3, confirm.
- Upload Service: Receives the chunk manifest, queries the Chunk Registry for missing hashes, returns pre-signed S3 PUT URLs for missing chunks, and writes metadata on confirm. Never handles raw chunk bytes.
- Chunk Registry (PostgreSQL): Maps
chunk_hash β s3_keywith a reference count. O(1) existence check per hash. - Object Store (S3): Stores immutable content-addressed chunk data keyed by SHA-256 hash. Shared across users and versions.
- Metadata DB (PostgreSQL): Stores file and folder records, version history, and the chunk-to-file mapping.
Request walkthrough (evolved):
- Client detects a new or modified file via filesystem watcher.
- Client splits the file into 4MB chunks and computes SHA-256 for each chunk.
- Client sends
POST /fileswith the full chunk manifest (list of hashes). - Upload Service queries the Chunk Registry:
SELECT chunk_hash FROM chunks WHERE chunk_hash IN (?). - Upload Service returns
missing_chunksand pre-signed S3 PUT URLs for each missing chunk. - Client uploads missing chunks in parallel directly to S3 using the pre-signed URLs.
- Client sends
PUT /files/{file_id}/confirmonce all parallel uploads complete. - Upload Service writes file metadata and chunk references to PostgreSQL and publishes a
FileChangedEvent.
The client uploads chunks directly to S3. The Upload Service handles only lightweight metadata coordination, not bulk data transfer. At 5GB max file size and 4MB chunks, that means the Upload Service processes at most 1,280 hash lookups per upload, each a point-read O(1) query.
I always write to PostgreSQL before publishing the change event. If those two operations were reversed, a device could receive a sync notification for a file whose metadata doesn't exist yet in the DB.
2. Files automatically sync to all connected devices
The sync path: after a confirmed upload, the Upload Service publishes a FileChangedEvent; the Notification Service delivers it to all of the owner's waiting long-poll connections; each device downloads only the chunks it does not have locally.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.