Design problems
Classic building blocksHardSDE-2 35 min

Design file storage and sync (Google Drive / Dropbox)

Asked at
AmazonAppleGoogleMicrosoftZoho

The interviewer asks

“Design a service like Google Drive or Dropbox. Users store files in folders, the files sync automatically across their laptop, phone and the web, and they can share files with others.”

What a strong answer looks like

A strong answer separates file content from file metadata. Content is split into chunks (about 4 MB, content-hashed) stored once in object storage, so uploads are resumable, unchanged chunks are never re-sent and identical chunks are deduplicated. Metadata (the file tree, versions, the list of chunk hashes per version, sharing) lives in a sharded relational store with a per-user or per-namespace change journal. Sync works by each device keeping a cursor into that journal: it pulls changes since its cursor, and a notification channel (long poll or WebSocket) tells it when to pull. Deep dives: chunking and delta sync, the journal/cursor sync protocol, conflict handling (conflicted copies, not silent overwrite), and sharing/permissions. Numbers: 500M users, ~100 PB of data, upload traffic dominated by a small share of heavy users.

Concepts to mention

Separate metadata from contentContent-addressed chunks (SHA-256)Delta sync and resumable uploadDeduplicationChange journal with per-namespace sequence numbersDevice cursorsLong poll / WebSocket notification then pullOptimistic concurrency with parent revisionConflicted copiesShared folders as namespacesPre-signed URLs and CDN downloadsGarbage collection of unreferenced chunks

1 Requirements

Functional

  • Upload, download, rename, move and delete files and folders
  • Automatically sync changes across all of a user's devices (desktop, mobile, web)
  • Keep version history and restore older versions for 30 days
  • Share files and folders with other users as viewer or editor, and via links
  • Work offline on desktop and sync when back online
  • Notify other devices and collaborators of changes within seconds

Non-functional

  • Scale: 500M registered users, 100M daily active, ~100 PB stored
  • Durability: never lose a file that was acknowledged (11 nines on content storage)
  • Sync latency: a change shows on other online devices within ~5-10 s
  • Efficiency: re-upload only changed chunks; deduplicate identical content
  • Availability: 99.95% for metadata APIs; downloads served via CDN
  • Consistency: per-user file tree must be strongly consistent (no lost renames or duplicate files from races)

Out of scope (say it out loud)

  • Real-time co-editing inside documents (Google Docs style)
  • Full-text search over file contents (mention as extension)
  • Virus scanning and DLP details

2 Estimates

Storage~100 PB logical, ~70 PB after dedupMost users store little and a few store a lot: assume 500M × 200 MB average = 100 PB; dedup saves ~20-30%
File writes~12k/s (peak ~40k/s)100M DAU × ~10 file changes/day ÷ 86,400 ≈ 11,600/s; ×3 for daytime peak
Upload bandwidth~12 GB/s average12k changes/s × ~1 MB average changed data (after delta sync) ≈ 12 GB/s ≈ 100 Gbps
Metadata rows~100B file versions500M users × ~100 files × ~2 versions kept; ~500 bytes per row ≈ 50 TB of metadata, sharded
Chunks~25B chunks100 PB ÷ 4 MB average chunk ≈ 25B chunk entries in the chunk index
Open notification connections~150M100M DAU × ~1.5 online devices; each notification server holds ~500k-1M idle connections → ~200-300 servers

3 API

EndpointWhat it does
POST/v1/uploads/checkSend the list of chunk hashes for a new file version; server returns which chunks it does not have yet
PUT/v1/chunks/{sha256}Upload one missing chunk (or get a pre-signed URL to put it directly in object storage)
POST/v1/files/commitCommit a file version: path or file_id, parent_rev, ordered chunk hashes, size; fails with conflict if parent_rev is stale
GET/v1/changes?cursor={cursor}&limit=1000List all changes in the user's namespaces since the cursor; returns new cursor and has_more
GET/v1/changes/longpoll?cursor={cursor}Block up to ~60 s until there are changes after the cursor (or use a WebSocket push)
GET/v1/files/{file_id}/content?rev=Get pre-signed URLs for the chunks of a version (served via CDN)
POST/v1/sharesShare a file or folder with a user or create a link with a role and optional expiry

4 Data model

namespacesPostgres / MySQL (sharded by namespace_id)
namespace_id (PK)owner_idtype (personal|shared_folder)journal_seq

A namespace is a root: a user's own Drive or a shared folder. Sharding by namespace keeps one tree and its journal on one shard so moves and renames are single-shard transactions.

file_nodesPostgres / MySQL (same shard as namespace)
node_id (PK)namespace_idparent_idnameis_folderlatest_revdeletedupdated_at; UNIQUE(namespace_id, parent_id, name)

Tree by parent pointers. The unique constraint stops two devices creating the same name at once.

file_versionsPostgres / MySQL
node_idrev (PK together)sizechunk_hashes[] (or blocklist id)author_devicecreated_at

Each version is just a list of chunk hashes; restore = point latest_rev at an older version.

journalPostgres / MySQL (append-only per namespace)
namespace_idseq (PK together)node_idop (create|update|move|delete)revts

Ordered change log per namespace. A device's cursor is a map of namespace → last seq it has applied.

chunksObject storage (S3 / GCS) + chunk index in a KV store
sha256 (PK)sizestorage_locationref_count

Content-addressed: the key is the hash, so identical chunks are stored once. Ref counts (or a periodic mark-and-sweep) drive garbage collection.

aclPostgres + cache (Redis)
namespace_id or node_idprincipal (user/group/link)roleexpires_at

Sharing a folder = mounting its namespace in the other user's tree; permission checks cached with short TTL.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAmetadata APIcommit, list changescheck / put chunksstore chunkstree + journal txnACL cachejournal appendednamespace changedpush: pull changesthumbnails, GCdownload chunkson missMobile / WebDesktop Sync ClientAPI Gateway / LBCDN (downloads)Metadata ServiceNotification ServiceBlock (Chunk) ServiceKafka (journal events)Preview / Index / GCMetadata DB (sharded)Redis (ACL, presence)Chunk Store (S3)
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Edit a 200 MB file on a laptop

  1. 1Desktop client's file watcher sees the change; it waits a few seconds for writes to settle
  2. 2Client splits the file into ~4 MB chunks (50 chunks) and computes SHA-256 for each; only 2 chunks differ from the last synced version
  3. 3Client calls /uploads/check with all 50 hashes; server replies that 2 are missing (the rest exist, possibly even from other users)
  4. 4Client uploads the 2 chunks (8 MB instead of 200 MB), in parallel and resumable per chunk; block service writes them to S3
  5. 5Client calls /files/commit with parent_rev = the version it edited and the 50 hashes
  6. 6Metadata service, in one transaction on the namespace shard: checks parent_rev == latest_rev, inserts the new version, updates the node, appends a journal entry with the next seq
  7. 7After commit, an event goes to Kafka; notification service tells the user's other online devices (and sharers) that the namespace changed
  8. 8Phone receives the push, calls /changes?cursor=…, sees the new rev, and downloads only the chunks it lacks (or just metadata, if it syncs on demand)

Device comes back online after a week

  1. 1Client sends its stored cursor (namespace → last seq) to /changes
  2. 2Server returns journal entries after each seq in pages of 1,000; client applies them to its local database
  3. 3For local edits made offline, client commits each with its old parent_rev; stale ones get a conflict response
  4. 4On conflict the client keeps both: server version under the original name, local version as 'report (Asha's laptop conflicted copy).docx'
  5. 5If the cursor is too old (journal compacted), server returns 'reset' and the client does a full listing and reconcile

7 Deep dives

Chunking, delta sync and deduplication

Files are split into chunks and each chunk is named by its SHA-256 hash. This gives three wins. Resumable upload: a failed upload restarts from the last chunk, not from zero, which matters on Indian mobile networks. Delta sync: on edit only chunks with new hashes are sent. Deduplication: if the server already has a hash (from this user or anyone), the chunk is not uploaded again. Fixed-size chunks (Dropbox uses 4 MB) are simple but an insert at the start of a file shifts every boundary and changes all hashes. Content-defined chunking (a rolling hash such as Rabin or FastCDC cuts where hash mod N == 0) keeps boundaries stable around inserts, so only nearby chunks change; it is used by backup tools. For very large files that change in small places (VM images, databases) an rsync-style rolling checksum can send even less. Cross-user dedup has a privacy trap: if 'already have this hash' is visible to clients, an attacker can test whether someone stored a known file. Mitigations: dedup only within a user or team, or always require upload of small files. Chunks are encrypted at rest with per-chunk keys wrapped by a master key.

The sync protocol: journal and cursors

Clients should never diff their whole tree against the server. Instead, every change in a namespace appends a row to an ordered journal with a sequence number that increases by one inside that namespace. A device keeps a cursor: for each namespace it can see, the last seq it applied. Sync becomes 'give me everything after my cursor', which is cheap, ordered and resumable. The cursor is opaque to the client, so the server can change its format. Notifications carry no data, only 'namespace X has new changes'; the client then pulls. This makes push best-effort: if a notification is lost, the next long poll or a periodic check (every few minutes) still catches up. Sharding by namespace means each journal lives on one shard, so appending a journal row in the same transaction as the tree change is atomic, and there is no outbox problem for correctness. The Kafka event is only a hint for notifications and workers. Old journal entries are compacted after, say, 90 days; a device with a cursor older than that gets a 'reset' and re-lists.

Conflicts and concurrent edits

Two devices can edit the same file offline. Every commit carries parent_rev, the version the device started from. The metadata service does an optimistic concurrency check: if parent_rev is not the current latest_rev, the commit is rejected. The client then keeps both versions and creates a 'conflicted copy' file, which is what Dropbox and Drive do, because the service cannot merge arbitrary binary files and silently losing someone's work is the worst outcome. Folder operations need more care. A move is a single update of parent_id in one transaction, so it is atomic. Cycles (moving folder A into its own child) are checked by walking up the parent chain before commit. Name collisions use the unique (parent_id, name) constraint; the loser gets a suffix like '(1)'. Deletes go to trash, and an edit to a file deleted on another device restores it rather than losing the edit. Clients must also handle case-insensitive file systems (Windows, macOS) where 'Report.pdf' and 'report.pdf' collide locally even if the server allows both.

Sharing and permissions

Sharing a single file can be an ACL row on the file. Sharing a folder is harder: the folder may hold 100k files, and permissions must apply to all of them and to future children. The clean model is to make a shared folder its own namespace. The owner's tree and every member's tree contain a mount point that points to that namespace. Members' devices add the namespace to their cursor map and sync its journal like their own. Access checks happen at the namespace level (one lookup, cached in Redis), not per file. Removing a member deletes their mount and their cursor entry; their devices then remove local copies. Link sharing creates a token mapped to (node, role, expiry); downloads via link use short-lived pre-signed CDN URLs. Moving files between namespaces (from personal to shared) is a cross-shard operation; implement it as copy-metadata-then-delete with an idempotent workflow, because chunk data is content-addressed and does not need copying.

8 Trade-offs

Fixed-size vs content-defined chunking

Fixed 4 MB chunks are simple and fast to compute and work well for most office files and photos, which are usually rewritten whole. Content-defined chunking saves more bandwidth for large files with inserts, at the cost of CPU and variable chunk sizes. Many products use fixed chunks plus special handling for large files.

Push vs pull for sync

Pure polling is simple but wastes requests and adds latency. Pure push needs reliable delivery and replay. The hybrid (push a small 'something changed' signal, then pull from the journal with a cursor) is robust: lost pushes only cost latency, never correctness.

Sync everything vs on-demand files

Syncing full copies to every device makes offline work easy but fills small laptop and phone disks. On-demand (placeholder) files download content only when opened. Mobile apps are almost always on-demand; desktops let users choose per folder.

9 Bottlenecks

Huge shared folders

A company-wide folder with 1M files and 10k members means one namespace shard handles every change and fans out notifications to 10k users. Batch notifications (coalesce per namespace for ~1 s), and paginate change lists. Split very large team spaces into several namespaces.

Metadata shard hot spots

A few power users or automated tools create millions of small files. Rate-limit per user, use bulk APIs, and move very heavy namespaces to dedicated shards.

10 Failure handling

Upload interrupted halfway

Chunks already uploaded stay in storage. The client retries only missing chunks and then commits. Chunks never referenced by any commit are removed by garbage collection after a grace period (for example 7 days).

Notification service down

Devices fall back to periodic long-poll or polling of /changes. Sync slows from seconds to a minute or two but nothing is lost, because the journal is the source of truth.

Client bug deletes files

A buggy or ransomware-infected client may sync mass deletes or encrypted versions. Version history and trash allow restore; detect abnormal patterns (thousands of deletes or rewrites in minutes) and offer a 'rewind folder to time T' feature.

11 What impresses

  • Explaining the cursor + journal sync model and why notifications carry no data
  • Calling out the cross-user dedup privacy leak
  • Content-defined chunking for insert-heavy files
  • Sharding by namespace so tree change and journal append are one transaction
  • Handling case-insensitive file systems and moves into own subfolder
  • Ransomware/mass-delete detection and folder rewind

12 Mistakes

  • Uploading the whole file on every change
  • Storing file content in the database
  • Syncing by comparing full file lists between client and server
  • Last-write-wins on conflicts, silently losing edits
  • No resumable upload for large files on weak networks
  • Ignoring sharing semantics for folders

13 Follow-ups

Q1How would you add end-to-end encryption, and what does it break (dedup, previews, search)?
Q2How do you support a 50 GB file?
Q3How would you implement search over file names and contents?
Q4How do you keep data of Indian enterprise customers inside India?
Q5How would you garbage-collect chunks safely when versions expire?
Q6How do you make the mobile app save battery while staying in sync?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free