Company questionsGroundworkCRED

System design · Platforms and search

Design a Dropbox-style file storage and sync service

  • Reported once
  • SDE-2
  • 2 diagrams
  • 1 source

One candidate write-up. Treat it as a single data point.

The prompt
Design a service where users keep files in a folder that syncs across their laptop and phone. Assume 10 million users, files of a few MB, and edits from two devices that can happen offline. Large files should upload in pieces and resume after a drop.

A representative prompt in the style reported, not a quote from CRED. The answer below is written for this site.

Try it first.The answer is covered until you pick. Talk it through against a 45 minute clock, or just read it.

Ask first

Clarify the problem before you draw anything.

Questions first: is this one folder per user, or shared folders with permissions? Do we keep version history, and for how long? What is the largest file, and is the average closer to 100 KB or 100 MB? Must two devices see a change within seconds, or is a minute fine? Do we need end-to-end encryption, which rules out server-side dedup and previews? How should we handle two edits to the same file?

I would assume per-user folders, 30 days of version history, an average file of 2 MB with a long tail into gigabytes, a few seconds of sync delay, and encryption at rest handled by the platform. In scope: upload, download, sync, versions, conflicts, dedup. Out of scope: sharing, search, previews and billing.

Numbers

A back-of-envelope that decides the shape.

These are planning figures for the exercise, not facts about any company.

  • Users: 10 million, 20% active a day = 2 million. Each changes 5 files a day: 10 million file changes ÷ 86,400 s = about 120 a second, 600 at a 5x peak.
  • Bytes: 10 million × 2 MB = 20 TB a day, which is 20,000,000 MB ÷ 86,400 = about 230 MB a second, or roughly 1.9 Gbit a second; the peak is about 9 Gbit a second. If dedup and sending only changed chunks save half, new storage is 10 TB a day, which is 3.65 PB a year before replication.
  • Chunks: take 2 per change on average (most files fit in one 4 MB chunk, the long tail adds many), which is 20 million references a day; half are new, so 10 million chunk rows at about 100 B: 1 GB a day.
  • Versions: 10 million rows a day × 300 B = 3 GB a day, about 1.1 TB a year.
  • Sync reads: each active client asks for changes 50 times a day: 100 million ÷ 86,400 = 1,160, call it 1,200 a second; 6,000 at peak.
  • Notification connections: 2 million at 10 KB is 20 GB, or 4 to 8 gateway boxes.

Two conclusions. Bytes must flow straight to object storage, never through the API servers. Metadata writes (600 a second at peak) and reads (6,000) fit one relational primary at first, but I shard by user from the start: it is the key every query uses, it is cheap to do early, and metadata grows by over a terabyte a year.

The design

Diagrams first, then components, data and the hard parts.

File sync at a glanceBytes go straight between the client and object storage. The metadata service only commits versions and moves a per-user cursor.
Clientchunks of 4 MB, local dbMetadata servicepresign, commitMetadata DBsharded by user_idObject storecontent-addressed chunksNotification serviceOther deviceVerifierre-hashes a samplemissing? then commitupload by presigncommit, one txnoutbox rowcursor movedchanges after cursorchecksum sample1234567
  1. The client splits the file into 4 MB chunks, hashes each and asks which hashes are missing. Later it calls commit on the same path.
  2. It uploads only the missing chunks, in parallel, with presigned URLs. A failed chunk is retried alone.
  3. Commit carries the file id, the base_version it started from and the chunk list. The service first checks every chunk exists in the object store. One transaction then checks current_version = base_version, raises ref counts, inserts the version, bumps the pointer and appends to the journal.
  4. The same transaction writes an outbox row, which feeds the notification service.
  5. The service pushes 'cursor moved' to the user's other devices.
  6. A device asks the change feed for everything after its cursor, then fetches only the chunks it lacks.
  7. A background verifier re-hashes a sample of stored chunks so corruption is found early.

Open on the whiteboardAll 2 diagrams on one tidy board, with a step-by-step walkthrough.

Components. A desktop and mobile client with a local database; an API and metadata service; a metadata database; an object store for chunks; a notification service; a queue for async work (thumbnails, garbage collection, audit); and a background verifier.

Data model.

  • files(user_id, file_id, path, current_version, deleted), sharded by user_id.
  • versions(file_id, version, base_version, chunk_list, size, device_id, mtime).
  • chunks(user_id, hash, size, storage_key, ref_count), sharded by user_id like the rest, so a commit stays on one shard. Within a user the chunk key is the content hash, so uploads are idempotent by construction.
  • journal(user_id, seq, file_id, version): a per-user append-only change log with an increasing seq. This is what devices sync from: "give me everything after cursor N".

Upload path.

  1. The client splits the file into chunks (4 MB here), hashes each, and asks the metadata service which hashes are missing.
  2. The service returns presigned upload URLs for the missing chunks only. The client uploads them in parallel; a failed chunk is retried alone, which gives resume for free.
  3. The client calls commit with the file id, the base_version it started from, and the chunk list.
  4. The service checks that every listed chunk exists in the object store and has a chunks row. Then, in one transaction: check current_version = base_version, raise the ref counts, insert the new version, bump the pointer, append to the journal, write an outbox row. If the base does not match, the commit is a conflict (below).

Download and sync. The outbox feeds the notification service, which pushes "cursor moved" to the user's other devices. They call the change feed with their last cursor, then fetch only chunks they lack through presigned download URLs. Chunks already on disk are reused, so a small edit costs one chunk.

Hard problem 1: conflicts. Never overwrite silently and never trust timestamps (device clocks drift). With optimistic concurrency on base_version, the second writer's commit is rejected; the client saves its copy as a conflicted copy named with the device, and both versions remain. Users resolve them; auto-merge is only for formats we understand.

Hard problem 2: garbage collection. Dedup means a chunk can be shared. Keep ref_count from committed versions, but delete only after a grace period and never a chunk uploaded in the last day, since its commit may be in flight. A weekly mark-and-sweep reconciles counts, because counters drift.

Fault tolerance. Object storage replicates across zones; the metadata primary has a synchronous standby in a second zone. Ask the object store for a checksum on upload (check the current docs for what it supports); the verifier re-hashes a sample of chunks and flags mismatches, so corruption is caught before it is the only copy.

Trade-offs

What you chose, and when you would flip.

  • Fixed-size vs content-defined chunking. Fixed 4 MB chunks are simple, but inserting a byte near the start shifts every later chunk and kills dedup. Content-defined boundaries keep unchanged regions stable at the cost of more client CPU. Pick fixed for the first version; flip to content-defined when users edit large files in the middle.
  • Global vs per-user dedup. Global saves the most space, but answering "this hash exists" can reveal that someone else holds that file. Pick per-user dedup by default. Flip to global with a proof-of-possession step when storage cost dominates.
  • Conflicted copy vs auto-merge. A copy never loses data and needs no format knowledge. Merging is nicer for text but dangerous when wrong. Pick the copy; flip to merge for formats with a safe merge rule.
  • Push vs poll for sync. Push needs 4 to 8 gateway boxes for 2 million connections but gives seconds of delay. Polling every 30 s from 2 million clients is about 67,000 requests a second: stateless, but not cheaper, and slower. Pick push for desktop; on mobile in the background use the phone's own push service and poll when the app opens.

Follow-ups to expect

Where the interviewer pushes next.

  1. How does a client that was offline for a month catch up without downloading everything?
  2. A user deletes a folder with 100,000 files. How do you make that fast and also undoable?
  3. How would you add shared folders with permissions without breaking per-user sharding?
  4. How would you verify that a stored chunk has not silently rotted?
  5. How would you support end-to-end encryption, and what do you lose when you do?

Where candidates slip

The mistakes this question punishes.

  • Sending file bytes through the API servers. They would carry roughly 1.9 Gbit a second on average and 9 at peak for no benefit.
  • Whole-file uploads, so one dropped connection restarts a 2 GB transfer.
  • Last-write-wins by timestamp, which loses edits when clocks differ.
  • Deleting a chunk the moment its count hits zero, racing with an in-flight upload.
  • Ignoring the change feed: asking every device to rescan the whole tree.
  • Hand-waving dedup without saying what the key is.
  • Putting the file tree in one table with no per-user cursor, so sync means a full comparison every time.
  • Forgetting that deletes, renames and moves are also changes that must reach every device.

Have a go before you read.

Interviews reward the sketch you make cold. Clarify out loud, draw it, then compare with the answer.

Now make it stick

Where this was reported

Public pages only. Companies do not confirm these, and they rotate their questions, so treat each link as one person's account or one list's tag.

Read these chapters first