Skip to main content
PProfileMatch

EasyProblem 2 of 36 in the system design series

Design Dropbox: Cloud File Storage and Multi-Device Sync

Prefer watching? This article is the written companion to the video above, from the Shubham Aher YouTube channel.

A cloud file service lets someone save a document into a folder on one machine and find the same document waiting on their phone moments later. Behind that small convenience sit three demanding jobs: accepting files that may be tens of gigabytes, keeping several devices consistent with each other, and storing petabytes of user data without losing a single byte.

Interviewers pick this problem because it forces candidates to separate two very different kinds of data. Names, folders and version lists are small and relational. File contents are enormous, opaque and expensive to move. Candidates who push everything through one service and one database run into trouble quickly; those who split the control plane from the data plane find the rest of the design falls into place.

What makes it hard is the environment. Home Wi-Fi drops, laptops go offline for days, two people edit the same file on a plane, and the same popular installer gets uploaded by thousands of users. The design below for a Dropbox-like service treats each of those situations as a first-class requirement rather than an edge case.

This is an interview practice design for a Dropbox-like service, built from first principles. It is not a description of how Dropbox is actually built.

Functional Requirements

Four features define the product. Users can upload files from any device, and those files can be very large, so assume a ceiling of 50 GB per file. Users can download anything they have stored from any device they sign in on. Changes propagate on their own: edit a spreadsheet on the desktop and the phone receives the new version with no manual refresh. Finally, users can share a file or folder, either by link or by inviting specific people.

Real-time collaborative editing, where two cursors type into one document together, is explicitly left out. That is an operational-transform or CRDT problem with very different constraints, and mixing it in would blur the storage and sync story that this question is really testing.

Upload files

Upload from any device, including very large files.

up to 50 GB

Download files

Get any stored file back, on any device.

Automatic sync

Change a file on one device, see it on all the others.

no manual refresh

Share

Give other people access to a file or a folder.

link or invite

  • Out of scope today: live co-editing like Google Docs

Non-Functional Requirements

Durability ranks above everything else. A user will forgive a slow upload, but not a missing tax return, so once the system acknowledges a file it must survive disk failures, machine failures and even the loss of a data centre. Object stores with multi-zone replication exist precisely for this guarantee.

Availability comes next, and we choose it over strict consistency. If a phone briefly shows the version from a few seconds ago, nobody is harmed; if the service refuses to open files during a network partition, everybody is. Two further requirements shape the client: big uploads must be resumable, picking up from where they stopped instead of from byte zero, and sync must be quick enough that changes appear elsewhere within seconds rather than minutes.

Durability

An uploaded file must never be lost.

zero data loss

High availability

Files are reachable any time; a few seconds stale is fine.

availability first

Resumable uploads

Big uploads survive network drops and continue.

resume, not restart

Fast sync

A change reaches other devices within seconds.

seconds, not minutes

Scale Estimation

Assume 500 million registered users, of whom 100 million are active on a given day. If each active user creates or modifies two files daily, that is 200 million uploads per day, or roughly 2,300 per second averaged over the day. With a typical file of about 1 MB, the system ingests around 200 TB of fresh data every day.

The total footprint is the eye-opening figure. If the average account holds about 10 GB, then 500 million accounts amount to roughly 5 EB. No relational database could hold that, and no fleet of application servers should be streaming it. Meanwhile, the metadata describing those files, such as names, paths, sizes, owners and chunk lists, is tiny by comparison. That contrast is the most important output of the estimate: keep file metadata in a database and file contents in object storage.

100million

Daily active users

500 million total users

200million

Uploads per day

≈ 2,300 per second avg

200TB

New data per day

200M files × 1 MB avg

5EB

Total storage

500M users × 10 GB

Metadata is tiny, bytes are huge → store them separately

Core Entities & API

Four entities cover the domain: the file itself, its metadata record, the chunks its bytes are split into, and the user who owns it. Treating chunks as a first-class entity is what later enables resumable uploads and deduplication.

Uploading starts with POST /files, sending the name, the size and an array of chunk hashes. The server answers with a file_id and a set of upload URLs, one for each chunk it still needs. Downloading uses GET /files/{file_id}, which returns metadata plus a download URL instead of streaming bytes itself. Syncing relies on GET /changes?cursor={c}, which lists every change after the given position along with a fresh cursor the device stores for its next call. Note that none of these endpoints carry file contents; they only coordinate.

Core entities

FileFile metadataChunkUser

API

  • POST /files { name, size, chunk_hashes[] }→ { file_id, upload_urls[] }
  • GET /files/{file_id}→ { metadata, download_url }
  • GET /changes?cursor={c}→ { changes[], next_cursor }

High-Level Design

Desktop and mobile clients talk to an API gateway that authenticates requests and enforces rate limits. Behind it, the file service owns metadata: file names, sizes, owners, versions and ordered chunk lists, kept in a relational store such as Postgres where folder hierarchies and sharing permissions are easy to express with joins and constraints.

File bytes take a different route entirely. The client uploads chunks directly to object storage such as S3 using short-lived presigned URLs handed out by the file service, and downloads are served through a CDN. Our application servers therefore stay small and cheap, since they never touch the hundreds of terabytes moving each day.

Every metadata change is also appended to a per-user change log, for example a Kafka topic partitioned by user so events for one account stay in order. A sync service consumes those events and nudges the user's other connected devices with a small notification. The guiding split is easy to remember: coordination flows through our services, heavy content flows straight to storage.

save metadatachunks, directchange eventspush: changed!Client appdesktop · mobileAPI gatewayauth · rate limitsFile servicemetadata · URLsMetadata DBPostgres · filesObject storageS3 · CDN for readsChange logKafka · per userSync serviceWebSocket · poll
High-level design. Dashed arrows are taken only on a cache miss or as a fallback. Scroll sideways on small screens.

Metadata through our servers · bytes straight to storage

Deep Dive 1 — Chunked Resumable Uploads

A single 2 GB upload over a home connection will almost certainly be interrupted at some point, so the client breaks the file into fixed pieces of about 4 MB each, turning that video into roughly 500 chunks. It then computes a SHA-256 digest of each piece. The digest doubles as the chunk identifier, which means any two identical pieces of data share the same name everywhere in the system.

Next, the client registers the file by sending its list of digests. The file service creates a record with an uploading status and returns a presigned URL only for chunks that storage does not already hold. A presigned URL is a time-limited permission to write exactly one object, so the client can upload without our servers proxying the data. Several chunks go up in parallel to use the available bandwidth.

If the connection breaks at 90 percent, the client simply asks the service which chunks have arrived and sends the remainder; nothing already stored is repeated. When every chunk is confirmed, the file service flips the record to ready and emits a change event so other devices learn about the new file.

  1. 1Split into chunksClient cuts the file into 4 MB pieces
  2. 2Fingerprint each chunkSHA-256 of the bytes becomes the chunk ID
  3. 3Get presigned URLsOne short-lived upload URL per missing chunk
  4. 4Upload in parallelSeveral chunks at once, straight to S3
  5. 5Resume after a dropAsk what arrived; send only the rest
  6. 6Commit the fileAll chunks present → status = ready
  • Presigned URL = time-limited permission to write one object

Deep Dive 2 — Sync Across Devices

The naive way to learn about remote changes is for every device to poll on a timer. A long interval makes sync feel sluggish, while a short one multiplies requests across 100 million users into a constant storm of empty responses. Neither setting is acceptable.

The better approach keeps one lightweight persistent connection per device, using a WebSocket or a long poll. When the change log records an update, the sync service sends only a tiny signal. The device then calls the changes endpoint with its stored cursor, which acts like a bookmark into that user's log. Whether the laptop was offline for ten seconds or ten days, it receives exactly the events it missed and nothing more. The cost is holding many open connections, which a dedicated, horizontally scaled sync tier can absorb.

Applying a change is efficient too. The device compares the new chunk list with what it already has and fetches only the differing chunks, so editing one page of a large document moves a few megabytes, not the whole file. If two devices edited the same file while offline, the system does not attempt a clever merge: the first version to commit wins and the other is preserved as a conflicted copy, so no edits disappear.

Client polling

  • + Very simple
  • − Slow, or wasteful
  • − Huge load at 100M users

WebSocket / long pollChosen

  • + Changes in seconds
  • + Only a tiny ping is sent
  • − Many open connections
  • Cursor = bookmark in the user's change log → fetch only what you missed
  • Delta sync: download only the chunks that changed
  • Offline edit conflict → keep both as a 'conflicted copy'

Deep Dive 3 — Deduplication

Because each chunk is named by the hash of its own contents, deduplication comes almost for free. Identical chunks produce identical identifiers, so storage keeps a single copy. The upload flow already checks digests before sending data, which means copying a large file into another folder, or re-uploading something already stored, completes nearly instantly because no bytes need to travel.

Shared chunks complicate deletion, so each chunk carries a reference count of the file versions that point at it, and garbage collection removes a chunk only when that count reaches zero. Chunks are also compressed, and data that nobody has opened in months can move to colder, cheaper storage tiers. One caution: deduplicating across different accounts saves the most space but can leak information, since an instant upload reveals that someone else already holds that exact file. Scoping deduplication to a single account avoids that side channel.

  • Content-addressed chunks

    A chunk's ID is the hash of its bytes, so identical chunks are stored once.

  • Check before uploading

    Client sends hashes first; chunks we already have are skipped.

  • Reference counting

    Delete a chunk only when no file version points to it anymore.

  • Compress and tier

    Compress chunks; move files nobody opens to colder, cheaper storage.

  • Mind privacy

    Cross-user dedup can reveal that a file exists, so scope it per user.

Trade-offs

Chunk size is a balancing act. Smaller chunks improve deduplication and make resumes cheaper, while larger chunks reduce request counts and the size of each file's chunk list. Favouring availability keeps files reachable at all times, at the cost of a device occasionally showing a slightly older version for a few seconds.

Sending uploads directly to S3 keeps application servers lean, but the file service must now track the status of each chunk and make sure presigned URLs expire. Saving conflicted copies rather than merging guarantees no work is lost, though the user has to reconcile the two versions manually. Taken together, the design rests on three habits: metadata lives in a database, bytes live in object storage, and only the parts that changed ever move across the network.

  • Chunk size

    Small: better dedup and resume. Large: fewer requests, less metadata.

  • Availability over consistency

    Always reachable — but a device may see an old version for a few seconds.

  • Direct uploads to S3

    Light, cheap servers — but we must track chunk status and expire URLs.

  • Conflicted copies, not merging

    No work is ever lost — but the user resolves the conflict by hand.

Metadata in a DB. Bytes in object storage. Move only what changed.

Key takeaways

  • Keep small metadata in a relational database and the exabytes of file bytes in object storage.
  • Split files into hashed chunks so uploads can resume and run in parallel.
  • Presigned URLs let clients write straight to S3, keeping application servers light.
  • A per-user change log plus a cursor lets any device catch up on exactly what it missed.
  • Content-hashed chunks give deduplication; reference counts make deletion safe.
  • Prefer conflicted copies over merging so offline edits are never silently lost.

Interview questions

How does Dropbox-style chunked upload work for large files?

The client splits the file into fixed-size pieces, often around 4 MB, and hashes each one. It sends the list of hashes to the server, which returns presigned upload URLs for chunks it does not already have. The client uploads those chunks in parallel directly to object storage, and the server marks the file ready once every chunk is confirmed.

How do you resume a failed file upload in a cloud storage system?

Track the status of every chunk on the server. After a dropped connection, the client asks which chunks were received and uploads only the missing ones, using fresh presigned URLs if the old ones expired. Because each chunk is small and identified by its hash, a failure costs at most one chunk of rework instead of restarting the entire file.

Why upload files directly to S3 instead of through the API server?

At hundreds of terabytes per day, routing bytes through application servers would require a huge fleet whose only job is copying data. Presigned URLs give the client a short-lived permission to write one object straight to storage, so the servers handle only lightweight metadata calls. This cuts cost, reduces latency and removes a large failure surface.

How does a file sync service notify other devices of changes?

Each device keeps a WebSocket or long-poll connection to a sync service. When the per-user change log records an update, the service sends a small notification. The device then requests all changes after its saved cursor, diffs the old and new chunk lists, and pulls just the pieces that differ. The cursor lets a device catch up correctly even after a long offline period.

What happens when two devices edit the same file offline?

Automatic merging of arbitrary binary files is unreliable, so a common policy is first commit wins. Whichever device syncs first becomes the current version, and the other device's edit is saved alongside it as a conflicted copy. No changes are lost, and the user decides how to combine the two versions instead of the system guessing.

Keep practising