Lesson 387 min read

Design Google Drive / Dropbox

April 18, 2026

// TL;DR

Design Drive/Dropbox by splitting files into ~4MB content-addressed chunks: store blocks in object storage (S3), keep metadata (file tree, versions, chunk lists) in a separate database. Upload only changed chunks (delta sync) and store identical chunks once (dedup) for huge bandwidth and storage savings. A notification service pushes changes to other devices; the metadata service is the source of truth; concurrent edits produce conflicted copies rather than silent loss.

Design Google Drive / Dropbox

File sync services like Google Drive and Dropbox let hundreds of millions of users store files in the cloud and keep them identical across every device. The interesting engineering is not “store a file in S3” — it’s doing so efficiently (don’t re-upload a 2 GB file because one byte changed), keeping many devices in sync in near real-time, and resolving conflicts when two devices edit offline. Let’s design it.

Understanding the Problem

Functional Requirements

  1. Upload / download — Store and retrieve files of any size.
  2. Sync — Changes on one device appear on all the user’s other devices automatically.
  3. Sharing — Share files/folders with other users, with permissions.
  4. Versioning — Keep a history of file versions; restore old ones.
  5. Offline edits — Work offline; sync when reconnected.

Non-Functional Requirements

Requirement Target
Durability 99.999999999% (eleven nines) — never lose a file
Sync latency Changes propagate to other devices in seconds
Bandwidth efficiency Transfer only what changed, not whole files
Storage efficiency Don’t store identical data many times
Availability 99.99% for the metadata/sync service
Large files Support multi-GB files reliably over flaky networks

Scale Estimation

  • 1 billion users, 100 million daily active.
  • Average 100 GB stored per user → ~100 EB of raw data (deduplication and tiering matter enormously at this scale).
  • Say 50 file changes/user/day across actives → ~5 billion change events/day ≈ ~60K/sec, bursty.
  • Metadata (file tree, versions, chunk lists) is small per file but queried constantly during sync.

Two early conclusions: the bytes belong in cheap, durable object storage (S3-class), and the metadata belongs in a fast, queryable database — and the two scale very differently, so keep them separate.

Core Entities and APIs

The central idea is to stop thinking about “files” as opaque blobs and start thinking about chunks. A file is an ordered list of fixed-size chunks; a chunk is addressed by the hash of its content.

Data Model

-- Metadata DB (e.g. sharded relational / Spanner-like)

-- A logical file/folder in a user's namespace
CREATE TABLE files (
    id            UUID PRIMARY KEY,
    owner_id      UUID,
    parent_id     UUID,             -- folder tree
    name          VARCHAR(255),
    is_folder     BOOLEAN,
    latest_version BIGINT,
    deleted       BOOLEAN DEFAULT false,
    updated_at    TIMESTAMP
);

-- Each save creates a new immutable version
CREATE TABLE file_versions (
    file_id       UUID,
    version       BIGINT,
    size          BIGINT,
    chunk_hashes  TEXT[],           -- ordered list of chunk content-hashes
    created_at    TIMESTAMP,
    device_id     UUID,
    PRIMARY KEY (file_id, version)
);

-- A chunk is stored once, referenced by its content hash
CREATE TABLE chunks (
    hash          CHAR(64) PRIMARY KEY,   -- SHA-256 of chunk bytes
    size          INT,
    block_url     TEXT,                   -- location in object storage
    ref_count     BIGINT                  -- how many versions reference it
);

The chunk_hashes array is the heart of it: a file version is just an ordered list of content hashes. Two files (or two versions) that share content share chunk hashes, and therefore share stored blocks.

API Design

# Upload is a negotiation: tell the server your chunk hashes first
POST /files/{id}/prepare
  body: { chunk_hashes: ["a1b2...", "c3d4...", ...] }
  response: { missing: ["c3d4..."] }      # server only wants chunks it lacks

PUT  /blocks/{hash}                         # upload only the missing chunks
  body: <chunk bytes>

POST /files/{id}/commit
  body: { version, chunk_hashes: [...], size }
  response: { version }

# Download
GET  /files/{id}                            # metadata + chunk list
GET  /blocks/{hash}                          # fetch a chunk (often from CDN)

# Sync — long-poll / push for changes since a cursor
GET  /changes?cursor={last_seen}
  response: { changes: [...], cursor: {new} }

The prepare → missing → upload handshake is what makes sync cheap: the client announces the hashes it wants to store, and the server replies with only the ones it doesn’t already have.

High-Level Design

File sync architecture: a client watcher sends chunks to a chunker, metadata service records the file tree and versions, a content-addressed block store (S3) holds deduped blocks, and a notification service pushes changes to the user's other devices
  • Client (with a file watcher) detects local changes, chunks files, computes hashes, and talks to the server. It keeps a local database of file→chunk mappings so it knows what changed.
  • Chunker splits files into chunks and hashes them (on the client for upload efficiency; conceptually a shared step).
  • Metadata Service is the source of truth for the namespace: the file/folder tree, versions, and each version’s chunk list. Backed by a sharded, strongly-consistent database.
  • Block Store is object storage (S3 / GCS), content-addressed: the object key is the chunk hash. Immutable, cheap, eleven-nines durable, and fronted by a CDN for downloads.
  • Notification Service tells a user’s other online devices “something changed, pull the delta,” via long-poll or a persistent push connection.

Separating metadata from blocks is the key architectural move. Metadata is small, relational, and needs strong consistency (you must never show a file tree that points at a version that isn’t committed). Blocks are huge, immutable, and belong in object storage. They scale, fail, and cost very differently.

Deep Dive: Chunking and Content-Addressed Storage

Why chunk at all?

Storing whole files is wasteful. If you edit page 300 of a 500-page document, re-uploading the entire file wastes bandwidth, and storing the new copy wholesale wastes storage. Chunking fixes both.

Split each file into chunks (Dropbox famously uses ~4 MB blocks). Hash each chunk with SHA-256. The hash becomes the chunk’s identity and its storage key:

def chunk_file(path, chunk_size=4 * 1024 * 1024):
    chunks = []
    with open(path, "rb") as f:
        while block := f.read(chunk_size):
            h = sha256(block).hexdigest()
            chunks.append((h, block))
    return chunks   # ordered list of (hash, bytes)

Because the key is the content hash, storage is automatically deduplicated: writing a chunk that already exists is a no-op. Identical chunks — across versions, across files, even across different users (subject to privacy/encryption policy) — are stored exactly once. At Drive scale, dedup is the difference between 100 EB and a small multiple of that.

Fixed vs content-defined chunking

Fixed 4 MB boundaries are simple but suffer from the boundary-shift problem: insert one byte at the start of a file and every subsequent chunk boundary shifts, so every chunk’s hash changes and dedup fails. Production systems often use content-defined chunking (a rolling hash like Rabin fingerprinting sets boundaries based on content), so an insertion only changes the one chunk around it. Mention both in an interview; fixed-size is fine for a first pass, content-defined is the refinement.

Deep Dive: Delta Sync and Deduplication

Delta sync: a new file version shares unchanged chunks with the old one; only the changed chunk is uploaded, and identical chunks are stored once to save bandwidth and storage

This is where the design pays off. When a client saves a change:

  1. The client re-chunks the file and computes the new list of hashes.
  2. It compares against the previous version’s hashes — most chunks are identical.
  3. It calls prepare with the full hash list; the server replies with only the missing hashes (the genuinely new chunks).
  4. The client uploads just those few chunks, then commits the new version (which is just a new ordered hash list).

Editing one paragraph of a large document uploads a single 4 MB chunk, not the whole file. This delta sync cuts upload bandwidth by orders of magnitude for typical edits, and the download path is symmetric — a syncing device fetches only the chunks it’s missing.

def upload_new_version(file_id, local_chunks):
    hashes = [h for h, _ in local_chunks]
    missing = api.prepare(file_id, hashes)          # server: which do you lack?
    for h, block in local_chunks:
        if h in missing:
            api.put_block(h, block)                  # upload only new chunks
    return api.commit(file_id, chunk_hashes=hashes)  # metadata points at the list

Dedup via ref_count also makes deletion safe: a chunk’s block is only garbage-collected when no version references it anymore.

Deep Dive: Multi-Device Sync and Conflict Handling

Pushing changes to other devices

When a device commits a new version, the user’s other online devices must learn about it quickly. Each device holds a long-poll or persistent connection to the Notification Service and tracks a cursor (its last-seen change). On any change, the service nudges the device, which then calls /changes?cursor=... to pull the deltas and download the missing chunks. This keeps sync near real-time without devices polling constantly.

Because the metadata service is the source of truth, the protocol is: upload blocks first (they’re immutable and idempotent — safe to retry), then commit the metadata version last. If an upload fails midway, no half-written file is ever visible; the commit either happens with all chunks present or not at all.

Conflicts

Two devices edit the same file offline, then both reconnect. You cannot silently pick one and discard the other — that’s data loss. Options:

  • Last-writer-wins is simple but loses an edit; unacceptable for documents.
  • Conflicted copy (Dropbox’s approach): both versions are kept. One becomes the canonical version; the other is saved as report (Alice's conflicted copy).docx. No data is lost, and the user resolves it.
  • Operational transforms / CRDTs are used by real-time collaborative editors (Google Docs), where edits merge at the character level — a different, heavier problem than file sync.

For a Drive/Dropbox design, conflicted copies are the expected answer: detect divergence by comparing the parent version each device edited from, and if they differ, keep both.

Large files and flaky networks

Chunking also makes large uploads robust: each 4 MB block uploads independently and is resumable, so a dropped connection only costs the in-flight chunk, not the whole file. Blocks are immutable, so retries are naturally idempotent.

Security

Chunks are compressed and encrypted at rest; many designs encrypt per-chunk. Note the tension with cross-user dedup: global dedup of encrypted data requires convergent encryption (the key derived from content), which has its own privacy trade-offs — worth a sentence in a senior interview.

Key Takeaways

Decision Choice Why
File representation Ordered list of content-addressed chunks Enables dedup and delta sync
Metadata vs blocks Separate DB vs object storage They scale, cost, and need consistency differently
Block storage S3-class, key = content hash Immutable, eleven-nines durable, auto-deduplicated
Upload efficiency prepare → upload only missing chunks Delta sync cuts bandwidth by orders of magnitude
Storage efficiency Dedup via content hash + ref counts Identical chunks stored once; safe GC
Multi-device sync Notification service + per-device cursor Near real-time pull of only the deltas
Consistency Metadata service is source of truth; blocks first, commit last No half-written files ever visible
Conflicts Keep both as conflicted copies Never lose a user’s edit

Drive/Dropbox is a masterclass in doing less work: chunk the file, address chunks by content, transfer and store only what’s new. Interviewers push hardest on why you chunk and content-address, how delta sync falls out of it, and how you handle two offline edits without losing data. Have the chunk→hash→block model and the conflicted-copy answer ready.