# Design Google Drive | System Design Question

"Design Google Drive" is really two problems bolted together. One is a **storage** problem: millions of people upload files up to 10 GB each, you can never lose a single byte, and you have to serve those files back fast and cheap. The other is a **sync** problem: I edit a file on my laptop, and two seconds later the same file on my phone and my coworker's desktop shows the change, without re-uploading the whole thing. Nail one and forget the other and you have not designed Google Drive. Most people spend the whole hour on "where do the files live?" and never get to "how does the edit reach the other devices?"

We will build it end to end on a 60-minute budget using the [4-step system design framework](/60-mins-system-design-interview-framework): about 10 minutes to scope, 15 for the high-level design, 25 for the deep dive, and 5 to 10 to wrap up. Every term gets defined the first time it shows up, so you can follow this even if "block storage" and "delta sync" are new words today.

## Step 1: Scope it before you draw anything

"Design Google Drive" could mean a hundred different products. Pin down the features and the scale first. Ask, and be ready for messy answers - a real interviewer will not always hand you a clean number.

> **You:** What are the core features - upload, download, and sync files across devices?
> **Interviewer:** Yes. Upload and download files, sync across devices, and send a notification when a file changes.
> **You:** Web, mobile, or both?
> **Interviewer:** Both.
> **You:** Any restriction on file types or size?
> **Interviewer:** Any file type. Cap the size at 10 GB.
> **You:** Do files need to be encrypted at rest?
> **Interviewer:** Yes, stored files must be encrypted.
> **You:** How many users are we designing for - daily active users?
> **Interviewer:** You tell me.

That last one is a **punt** - the interviewer bounces the question back. Do not freeze. Propose a number out loud and write it down as an assumption: "I will assume 10 million daily active users - people who open the app on a given day - and 50 million signed-up total. Stop me if you had a different order of magnitude in mind." They nod, and now you own a concrete number to design against.

> **You:** Do we need real-time collaborative editing, like two people typing in the same doc at once?
> **Interviewer:** Actually, yes - throw that in too.

There is the **curveball** - a weak interviewer bolting on scope that quietly triples the problem. Real-time collaborative editing (think Google Docs, where every keystroke merges live) is its own hard system built on conflict-resolution algorithms like Operational Transformation or CRDTs. Name its cost and defer it: "Live co-editing needs a real-time merge engine - Operational Transformation or CRDTs - which is a separate design on top of storage and sync. I will design file-level sync so it slots in later, and only detail co-editing if we have time." Then keep moving.

The rule underneath all three answers: **use their number when they give one; when they punt, assume and state it; when they bolt on scope, name its cost and defer.** Now write the scope down precisely, one requirement per line.

**Functional requirements**

- Add files (drag and drop, upload from any device).
- Download files.
- Sync a file across all of a user's devices automatically when it changes on one.
- See a file's revision history (older versions).
- Share files with other people.
- Send a notification when a file is added, edited, deleted, or shared.

**Non-functional requirements** (the "how well," not the "what")

- **Reliability.** A storage system must never lose data. This is the hard requirement.
- **Fast sync.** If a change takes minutes to reach the other device, users quit.
- **Low bandwidth.** Do not re-send data you do not have to - people are on metered mobile plans.
- **Strong consistency.** Two devices must never show two different "current" versions of the same file at the same time.
- **High availability.** The system keeps working when some servers are down.
- **Scalability.** It handles growing traffic without a redesign.

### Back-of-the-envelope estimate

A quick sizing tells us which parts are hard. (If rough sizing math is new to you, here is a [short guide to back-of-the-envelope estimation](/easier-back-of-the-envelope-estimation).)

- **Users:** 50 million signed up, 10 million daily active (DAU).
- **Free space per user:** 10 GB.
- **Total storage allocated:** 50 million x 10 GB = **500 Petabytes.** (1 PB = 1 million GB.) That is not a database - that is a warehouse problem.
- **Uploads:** assume each active user uploads 2 files/day, average file 500 KB.
- **Upload QPS** (queries per second): 10 million x 2 / 24 hours / 3600 seconds = **~240 uploads/sec.**
- **Peak QPS:** roughly 2x the average = **~480/sec.**
- Assume a **1:1 read-to-write ratio** - people download about as often as they upload.

Two numbers set the whole design. **500 PB** says file bytes cannot live in a normal database or on one machine's disk - we need purpose-built object storage. **~480 peak QPS** is genuinely modest - the request rate is easy; the *data volume* and the *sync* are the hard parts. Good, now we know where to spend the hour.

## Step 2: High-level design - build it up from one server

Instead of drawing the final architecture cold, we will start with the simplest thing that works - one server - and let each scaling problem force the next component into existence. This is how you should reason in the interview: every box earns its place from a number.

### The one-server starting point

Spin up a single machine with three things on it:

- A **web server** to handle upload and download requests.
- A **database** (say MySQL) for **metadata** - the *data about the files*: who owns them, filenames, paths, sizes, versions. Not the file bytes themselves.
- A **directory on local disk** to hold the actual file bytes.

Under a root `drive/` directory, each user gets a **namespace** - their own root folder, named by their user id, holding all their files. Any file is uniquely identified by joining the namespace and the relative path (`/user_123/recipes/soup.txt`).

![Single-server Google Drive: user talks to a web server, which reads and writes metadata in MySQL and stores file bytes in a local drive directory](https://mermaid.ink/img/Zmxvd2NoYXJ0IExSCiAgVVsiVXNlcjxici8+d2ViIG9yIG1vYmlsZSBhcHAiXSAtLT4gV1NbIldlYiBzZXJ2ZXI8YnIvPkFwYWNoZSJdCiAgV1MgLS0+IERCWygiTXlTUUw8YnIvPm1ldGFkYXRhIildCiAgV1MgLS0+IEZTWyIvZHJpdmUgZGlyZWN0b3J5PGJyLz5vbiBsb2NhbCBkaXNrLyJdCg==?type=png&bgColor=FFFFFF)

This works for the first few thousand users. It also fails in three obvious ways we will fix one at a time: the disk fills up, the machine is a single point of failure, and one machine cannot serve global traffic.

### The APIs

Three endpoints carry the core features. All of them require authentication and run over HTTPS (encrypted transport), so file data is protected in transit.

**1. Upload a file.** Two flavors:

- **Simple upload** for small files - one request, done.
- **Resumable upload** for large files on flaky networks. You do not want a 9 GB upload to restart from zero because the wifi blinked. A resumable upload works in three steps: ask the server for a resumable session URL, upload the bytes while tracking how far you got, and on interruption resume from the last confirmed byte. Google's Drive API does exactly this - see their [resumable upload guide](https://developers.google.com/drive/api/guides/manage-uploads).

```
POST https://api.example.com/files/upload?uploadType=resumable
Params:
  uploadType = resumable
  data       = the file bytes
```

**2. Download a file.**

```
GET https://api.example.com/files/download
Params:
  path = "/recipes/soup/best_soup.txt"
```

**3. Get revisions.**

```
GET https://api.example.com/files/list_revisions
Params:
  path  = "/recipes/soup/best_soup.txt"
  limit = 20
```

### Problem 1: the disk fills up - move file bytes off the box

At 500 PB, no single disk works. The first instinct is to **shard** the storage - split files across many storage servers by user id (users A-M on server 1, N-Z on server 2, and so on). **Sharding** means partitioning data across machines so each holds a slice. That buys space, but now *you* are running a fleet of storage servers, worrying about a dead drive losing someone's only copy forever - and reliability was our hard requirement.

The better answer is **object storage**. Object storage is a service built to hold enormous numbers of files ("objects") durably and cheaply, addressed by a key, replicated across machines and data centers for you - Amazon [S3](https://aws.amazon.com/s3/) is the canonical one. It gives us same-region and cross-region **replication** (keeping copies in multiple physical locations so one failure loses nothing) out of the box, so a whole data center can burn down and the files survive. We do not re-explain object storage here because it is the same decision as in the [photo-sharing app design](/design-a-photo-sharing-app-system-design-question) - files go in object storage, never in the database.

Managed vs self-managed trade-off: running our own replicated storage fleet is months of engineering and on-call pain to match durability that S3 already sells by the gigabyte. Buy it. We store the metadata ourselves (it is small, it is queried constantly, and we want tight control over it) and rent the bulk storage.

### Problem 2 and 3: kill the single points of failure

With bytes in S3, decouple the rest so no single machine can take the system down:

- **Load balancer.** A **load balancer** sits in front of the web servers and spreads incoming requests across them; if one server dies, it stops routing there. (More on this in [how to scale backends](/how-to-scale-backends).)
- **Stateless web/API servers.** Keep no user session on the server itself, so any server can handle any request and you can add or remove servers freely with traffic.
- **Replicated + sharded metadata DB.** Move the database off the box, replicate it (copies for availability), and shard it (split for scale). See [how to scale databases](/how-to-scale-databases) for replication and sharding in depth, and [consistent hashing](/what-is-consistent-hashing) for the standard way to pick which shard a user lands on without reshuffling everything when you add a shard.
- **S3 for files,** replicated across two regions.

![Scaled Google Drive: users hit a load balancer that spreads traffic across several stateless web servers, which read and write a metadata cache, a sharded and replicated metadata database, and Amazon S3 replicated across regions](https://mermaid.ink/img/Zmxvd2NoYXJ0IFRCCiAgVVsiVXNlcnM8YnIvPndlYiBhbmQgbW9iaWxlIl0gLS0+IExCWyJMb2FkIGJhbGFuY2VyIl0KICBMQiAtLT4gV1MxWyJXZWIgc2VydmVyIl0KICBMQiAtLT4gV1MyWyJXZWIgc2VydmVyIl0KICBMQiAtLT4gV1MzWyJXZWIgc2VydmVyIl0KICBXUzEgLS0+IE1DWygiTWV0YWRhdGEgY2FjaGUiKV0KICBXUzIgLS0+IE1DCiAgV1MxIC0tPiBNREJbKCJNZXRhZGF0YSBEQjxici8+c2hhcmRlZCArIHJlcGxpY2F0ZWQiKV0KICBXUzIgLS0+IE1EQgogIFdTMyAtLT4gTURCCiAgV1MxIC0tPiBTM1soIkFtYXpvbiBTMzxici8+cmVwbGljYXRlZCBhY3Jvc3MgcmVnaW9ucyIpXQogIFdTMiAtLT4gUzMKICBXUzMgLS0+IFMzCg==?type=png&bgColor=FFFFFF)

### Sync conflicts - decide the rule now

Two people (or two of your devices) edit the same file at the same moment. Whose version wins? Our rule: **the first write our system fully processes wins; the later one gets flagged as a conflict.** The loser is not silently overwritten - we keep both copies and hand the user a choice: keep the server's latest version, keep their local copy, or merge the two. This is deliberately simple and it is enough for file-level sync. (Live keystroke-level co-editing needs the merge engine we deferred in scoping.)

### The full high-level architecture

Now the components have all earned their place. A few need naming before the final picture:

- **Block servers.** Instead of shipping whole files to S3, these split a file into **blocks** (chunks, max 4 MB each - the size [Dropbox uses](https://www.dropbox.com/)), then compress and encrypt each block. They are the workhorses of upload; we build them in the deep dive.
- **Metadata cache.** A fast in-memory copy of hot metadata so we do not hit the database for every lookup.
- **Notification service.** A publish/subscribe system that tells a client "a file you care about changed elsewhere - come pull it."
- **Offline backup queue.** If a client is offline when a change happens, the update waits here and is delivered when the client reconnects.
- **Cold storage.** Cheap, slow storage (like S3 Glacier) for files nobody has touched in months.

Wire them together and this is the high-level design:

![Full Google Drive high-level architecture: client talks to a load balancer into stateless API servers, and separately to block servers; API servers use a metadata cache, sharded metadata DB, notification service, and offline backup queue; block servers write to multi-region S3 which tiers cold data to Glacier](https://mermaid.ink/img/Zmxvd2NoYXJ0IFRCCiAgVVsiV2ViIC8gbW9iaWxlIGNsaWVudCJdCiAgVSAtLT4gTEJbIkxvYWQgYmFsYW5jZXIiXQogIFUgLS0+IEJTWyJCbG9jayBzZXJ2ZXJzPGJyLz5jaHVuaywgY29tcHJlc3MsIGVuY3J5cHQiXQogIExCIC0tPiBBUElbIkFQSSBzZXJ2ZXJzPGJyLz5zdGF0ZWxlc3MiXQogIEFQSSAtLT4gTUNbKCJNZXRhZGF0YSBjYWNoZSIpXQogIEFQSSAtLT4gTURCWygiTWV0YWRhdGEgREI8YnIvPnNoYXJkZWQgKyByZXBsaWNhdGVkIildCiAgQVBJIC0tPiBOU1siTm90aWZpY2F0aW9uIHNlcnZpY2U8YnIvPmxvbmcgcG9sbGluZyJdCiAgQVBJIC0tPiBPQlFbIk9mZmxpbmUgYmFja3VwIHF1ZXVlIl0KICBOUyAtLT4gVQogIE9CUSAtLT4gVQogIEJTIC0tPiBDU1soIkNsb3VkIHN0b3JhZ2UgUzM8YnIvPm11bHRpLXJlZ2lvbiIpXQogIENTIC0tPiBDT0xEWygiQ29sZCBzdG9yYWdlPGJyLz5TMyBHbGFjaWVyIildCg==?type=png&bgColor=FFFFFF)

Notice the split: **file bytes** flow client -> block servers -> S3, while **metadata and coordination** flow client -> load balancer -> API servers -> DB / notifications. That separation is the backbone of the whole design.

## Step 3: Design deep dive

Five things deserve a close look: the block servers, strong consistency, the metadata schema, the upload/download flows, and the notification service. Then storage savings and failure handling.

### Block servers: why we chunk files

Here is the single most important idea in the design. Say you have a 1 GB video in your Drive and you change the title metadata in the first second of it. If we treat the file as one blob, syncing that tiny change re-uploads a full gigabyte to every device. That is unacceptable on the bandwidth requirement.

The fix is **block-level storage**: split every file into fixed-size blocks (4 MB max), and treat each block as an independent object with its own **hash** - a short fingerprint computed from the block's bytes, where identical bytes always produce the same fingerprint and any change produces a different one. The file becomes an *ordered list of block hashes*. To rebuild the file, fetch its blocks and concatenate them in order.

![Block server pipeline: an original file is split into 4MB blocks, each block is compressed, then encrypted, then uploaded to S3](https://mermaid.ink/img/Zmxvd2NoYXJ0IExSCiAgRlsiT3JpZ2luYWwgZmlsZSJdIC0tPiBTWyJTcGxpdCBpbnRvIGJsb2Nrczxici8+NE1CIG1heCBlYWNoIl0KICBTIC0tPiBDWyJDb21wcmVzczxici8+ZWFjaCBibG9jayJdCiAgQyAtLT4gRVsiRW5jcnlwdDxici8+ZWFjaCBibG9jayJdCiAgRSAtLT4gVVsoIlVwbG9hZCBibG9ja3MgdG88YnIvPmNsb3VkIHN0b3JhZ2UgUzMiKV0K?type=png&bgColor=FFFFFF)

Splitting a file into hashed blocks is a few lines. Using Node's built-in `crypto` for SHA-256 (a standard hashing function):

```typescript
import { createHash } from "crypto";

const BLOCK_SIZE = 4 * 1024 * 1024; // 4 MB

interface Block {
  order: number;   // position of this block in the file
  hash: string;    // sha-256 fingerprint of the block bytes
  data: Buffer;    // the raw block bytes (before compress/encrypt)
}

function splitIntoBlocks(file: Buffer): Block[] {
  const blocks: Block[] = [];
  for (let offset = 0, order = 0; offset < file.length; offset += BLOCK_SIZE, order++) {
    const data = file.subarray(offset, offset + BLOCK_SIZE);
    const hash = createHash("sha256").update(data).digest("hex");
    blocks.push({ order, hash, data });
  }
  return blocks;
}
```

Each block then gets **compressed** (shrunk with an algorithm like [gzip](https://www.gnu.org/software/gzip/) for text; images and video need their own codecs) and **encrypted** before it leaves for S3, so nothing is stored in the clear.

A key design choice: we do this on the **block servers**, not on the client. Why centralize it? Two reasons. First, if clients chunked, compressed, and encrypted, we would have to implement that identical logic correctly on iOS, Android, and web - three chances to get encryption subtly wrong. Second, a client can be hacked or tampered with, so trusting it to encrypt correctly is risky. One centralized implementation is safer and simpler. (The trade-off: the file crosses the network twice - client to block server, block server to S3. We will revisit that in the wrap-up.)

### Delta sync: only ship what changed

Because a file is a list of block hashes, syncing an edit is trivial: compare the new block hashes against the ones already stored, and upload only the blocks whose hashes differ. This is **delta sync** - "delta" meaning "the difference." Edit block 2 and block 5 of a 5-block file and only 2 blocks move, not the whole file. (The classic reference for computing differences over a network is the [rsync algorithm](https://rsync.samba.org/tech_report/).)

![Delta sync: original blocks 1 to 5 sit in cloud storage; after an edit the local file has blocks 2 and 5 changed; comparing hashes shows only blocks 2 and 5 need to upload](https://mermaid.ink/img/Zmxvd2NoYXJ0IExSCiAgc3ViZ3JhcGggT0xEWyJPcmlnaW5hbCBibG9ja3MgaW4gY2xvdWQgc3RvcmFnZSJdCiAgICBkaXJlY3Rpb24gTFIKICAgIEExWyJCbG9jayAxIl0gLS0+IEEyWyJCbG9jayAyIl0gLS0+IEEzWyJCbG9jayAzIl0gLS0+IEE0WyJCbG9jayA0Il0gLS0+IEE1WyJCbG9jayA1Il0KICBlbmQKICBzdWJncmFwaCBORVdbIkxvY2FsIGZpbGUgYWZ0ZXIgZWRpdCJdCiAgICBkaXJlY3Rpb24gTFIKICAgIEQxWyJCbG9jayAxPGJyLz5zYW1lIl0gLS0+IEQyWyJCbG9jayAyPGJyLz5DSEFOR0VEIl0gLS0+IEQzWyJCbG9jayAzPGJyLz5zYW1lIl0gLS0+IEQ0WyJCbG9jayA0PGJyLz5zYW1lIl0gLS0+IEQ1WyJCbG9jayA1PGJyLz5DSEFOR0VEIl0KICBlbmQKICBPTEQgLS5jb21wYXJlIGhhc2hlcy4tPiBORVcKICBEMiAtLT4gVVBbIlVwbG9hZCBvbmx5PGJyLz5ibG9ja3MgMiBhbmQgNSJdCiAgRDUgLS0+IFVQCg==?type=png&bgColor=FFFFFF)

Computing the delta is just a set difference over hashes:

```typescript
// Given the block hashes S3 already has (in order) and the new file's
// blocks, return only the blocks whose bytes actually changed.
function blocksToUpload(existingHashes: string[], newBlocks: Block[]): Block[] {
  const have = new Set(existingHashes);
  return newBlocks.filter((block) => !have.has(block.hash));
}
```

### Strong consistency: the cache is the tricky part

We promised **strong consistency**: no two clients ever see two different "current" versions of a file. The file bytes in S3 are easy - a block is immutable once written (a new version writes new blocks). The danger is the **metadata cache**.

Caches default to **eventual consistency** - after a write, different cache replicas may briefly hold different values until they catch up. That is fine for a like counter; it is not fine for "which version of this file is current." To keep strong consistency we do two things:

- Keep the cache replicas and the database in agreement.
- **Invalidate the cache on every database write** - the moment metadata changes in the DB, evict the stale cached copy so the next read reloads the fresh value.

This is the write-through / invalidate-on-write pattern; if caching strategies are new, our [caching strategies post](/caching-strategies-you-should-know) walks through them. The database choice follows from the same requirement: a **relational database** (MySQL, Postgres) gives us **ACID** natively - Atomicity, Consistency, Isolation, Durability, the set of guarantees that make a transaction all-or-nothing and never leave the data half-updated. NoSQL stores can be made consistent, but you write that logic yourself. For metadata we want ACID for free, so we pick relational.

### Metadata schema

The metadata is small but relational - users own devices and namespaces, namespaces hold files, files have versions, versions are made of blocks. A simplified schema:

![Metadata database schema: USER has many DEVICE and NAMESPACE; NAMESPACE contains FILE; FILE has many FILE_VERSION; FILE_VERSION is made of many BLOCK, each with an order, hash, and size](https://mermaid.ink/img/ZXJEaWFncmFtCiAgVVNFUiB8fC0tb3sgREVWSUNFIDogaGFzCiAgVVNFUiB8fC0tb3sgTkFNRVNQQUNFIDogb3ducwogIE5BTUVTUEFDRSB8fC0tb3sgRklMRSA6IGNvbnRhaW5zCiAgRklMRSB8fC0tb3sgRklMRV9WRVJTSU9OIDogaGFzCiAgRklMRV9WRVJTSU9OIHx8LS1veyBCTE9DSyA6ICJtYWRlIG9mIgogIFVTRVIgewogICAgYmlnaW50IGlkIFBLCiAgICBzdHJpbmcgdXNlcm5hbWUKICAgIHN0cmluZyBlbWFpbAogIH0KICBERVZJQ0UgewogICAgYmlnaW50IGlkIFBLCiAgICBiaWdpbnQgdXNlcl9pZCBGSwogICAgc3RyaW5nIHB1c2hfaWQKICB9CiAgTkFNRVNQQUNFIHsKICAgIGJpZ2ludCBpZCBQSwogICAgYmlnaW50IHVzZXJfaWQgRksKICB9CiAgRklMRSB7CiAgICBiaWdpbnQgaWQgUEsKICAgIGJpZ2ludCBuYW1lc3BhY2VfaWQgRksKICAgIHN0cmluZyBuYW1lCiAgICBzdHJpbmcgc3RhdHVzCiAgICBpbnQgbGF0ZXN0X3ZlcnNpb24KICB9CiAgRklMRV9WRVJTSU9OIHsKICAgIGJpZ2ludCBpZCBQSwogICAgYmlnaW50IGZpbGVfaWQgRksKICAgIGludCB2ZXJzaW9uCiAgICBkYXRldGltZSBjcmVhdGVkX2F0CiAgfQogIEJMT0NLIHsKICAgIGJpZ2ludCBpZCBQSwogICAgYmlnaW50IGZpbGVfdmVyc2lvbl9pZCBGSwogICAgaW50IGJsb2NrX29yZGVyCiAgICBzdHJpbmcgaGFzaAogICAgaW50IHNpemUKICB9Cg==?type=png&bgColor=FFFFFF)

- **User** - basic account info.
- **Device** - a user's devices; `push_id` is the address for sending that device a push notification. One user, many devices.
- **Namespace** - the user's root directory.
- **File** - everything about the *current* file: name, `status` (`pending` or `uploaded`), and which version is latest.
- **File_version** - the revision history. These rows are **read-only once written** so history cannot be corrupted.
- **Block** - one row per block: its order in the file, its hash, and its size. Join a version's blocks in `block_order` and you have reconstructed that exact version of the file.

In TypeScript, the file record and its rebuild logic look like this:

```typescript
interface FileVersion {
  version: number;
  blockHashes: string[]; // ordered list of block fingerprints
}

// Rebuild a specific version by fetching its blocks (from cache/S3),
// decrypting and decompressing each, and concatenating in order.
async function reconstruct(
  v: FileVersion,
  fetchBlock: (hash: string) => Promise<Buffer>,
): Promise<Buffer> {
  const parts = await Promise.all(v.blockHashes.map(fetchBlock));
  return Buffer.concat(parts);
}
```

### Upload flow

When a client uploads, two things happen in parallel: register the metadata, and ship the bytes. Both start from client 1.

![Upload sequence: client 1 adds metadata via API servers which store it as pending and notify the notification service and client 2; in parallel client 1 uploads content to block servers which chunk, compress, encrypt, and push blocks to cloud storage, which fires a completion callback that flips status to uploaded and notifies clients](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgcGFydGljaXBhbnQgQzEgYXMgQ2xpZW50IDEKICBwYXJ0aWNpcGFudCBBUEkgYXMgQVBJIHNlcnZlcnMKICBwYXJ0aWNpcGFudCBNREIgYXMgTWV0YWRhdGEgREIKICBwYXJ0aWNpcGFudCBOUyBhcyBOb3RpZmljYXRpb24gc2VydmljZQogIHBhcnRpY2lwYW50IEMyIGFzIENsaWVudCAyCiAgcGFydGljaXBhbnQgQlMgYXMgQmxvY2sgc2VydmVycwogIHBhcnRpY2lwYW50IENTIGFzIENsb3VkIHN0b3JhZ2UKICBDMS0+PkFQSTogQWRkIGZpbGUgbWV0YWRhdGEKICBBUEktPj5NREI6IFN0b3JlIG1ldGFkYXRhLCBzdGF0dXMgPSBwZW5kaW5nCiAgQVBJLT4+TlM6IE5ldyBmaWxlIGJlaW5nIGFkZGVkCiAgTlMtPj5DMjogQSBmaWxlIGlzIGJlaW5nIHVwbG9hZGVkCiAgQzEtPj5CUzogVXBsb2FkIGZpbGUgY29udGVudAogIEJTLT4+QlM6IENodW5rLCBjb21wcmVzcywgZW5jcnlwdAogIEJTLT4+Q1M6IFVwbG9hZCBibG9ja3MKICBDUy0tPj5BUEk6IFVwbG9hZCBjb21wbGV0ZSBjYWxsYmFjawogIEFQSS0+Pk1EQjogc3RhdHVzID0gdXBsb2FkZWQKICBBUEktPj5OUzogRmlsZSBzdGF0dXMgdXBsb2FkZWQKICBOUy0+PkMyOiBGaWxlIGZ1bGx5IHVwbG9hZGVkCg==?type=png&bgColor=FFFFFF)

- **Add metadata (left path):** client 1 tells the API servers about the new file; they write it to the DB with status `pending` and tell the notification service, which lets client 2 know something is coming.
- **Upload bytes (right path):** client 1 streams the file to the block servers; they chunk, compress, encrypt, and push blocks to S3. When S3 confirms, it fires an **upload-completion callback** to the API servers, which flip the status to `uploaded` and notify the clients that the file is fully there.

The `pending` -> `uploaded` status is what keeps the two parallel paths honest: other devices do not treat the file as ready until the bytes actually landed. Editing an existing file is the same flow, except delta sync means only changed blocks travel.

### Download flow

Download is triggered when a file changed *somewhere else* and this device needs to catch up. How does a device even learn a change happened?

- If it is **online**, the notification service pushes it a "something changed, come look" signal.
- If it is **offline**, the change waits in the offline backup queue and is delivered when the device reconnects.

Once it knows, the device pulls metadata first, then the blocks:

![Download sequence: notification service tells client 2 a file changed; client 2 fetches metadata through API servers and the metadata DB, then requests blocks from block servers, which fetch them from cloud storage and return them for the client to reconstruct the file](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgcGFydGljaXBhbnQgTlMgYXMgTm90aWZpY2F0aW9uIHNlcnZpY2UKICBwYXJ0aWNpcGFudCBDMiBhcyBDbGllbnQgMgogIHBhcnRpY2lwYW50IEFQSSBhcyBBUEkgc2VydmVycwogIHBhcnRpY2lwYW50IE1EQiBhcyBNZXRhZGF0YSBEQgogIHBhcnRpY2lwYW50IEJTIGFzIEJsb2NrIHNlcnZlcnMKICBwYXJ0aWNpcGFudCBDUyBhcyBDbG91ZCBzdG9yYWdlCiAgTlMtPj5DMjogQSBmaWxlIGNoYW5nZWQgZWxzZXdoZXJlCiAgQzItPj5BUEk6IEZldGNoIGxhdGVzdCBtZXRhZGF0YQogIEFQSS0+Pk1EQjogUXVlcnkgY2hhbmdlZCBtZXRhZGF0YQogIE1EQi0tPj5BUEk6IE1ldGFkYXRhCiAgQVBJLS0+PkMyOiBSZXR1cm4gbWV0YWRhdGEKICBDMi0+PkJTOiBEb3dubG9hZCBibG9ja3MKICBCUy0+PkNTOiBGZXRjaCBibG9ja3MKICBDUy0tPj5CUzogQmxvY2tzCiAgQlMtLT4+QzI6IFJlY29uc3RydWN0IHRoZSBmaWxlCg==?type=png&bgColor=FFFFFF)

Because of delta sync, client 2 only downloads the blocks it does not already have - the same hash comparison, in reverse.

### Notification service: long polling, not WebSocket

The notification service tells clients when to sync. Two standard ways to push server-to-client updates:

- **WebSocket** - a persistent, two-way connection; the client and server can both send anytime. Great for chat, where messages fly both directions. (We used exactly this in the [WhatsApp design](/design-whatsapp-system-design-question).) See [MDN on WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API).
- **Long polling** - the client makes an HTTP request that the server *holds open* until it has something to say (or a timeout hits); the client then immediately opens another. It is one-directional server-to-client, riding plain HTTP.

For Drive, the traffic is **one-directional** (the server tells clients about changes; clients do not stream anything back over this channel) and **infrequent** (file changes trickle in - there is no burst). WebSocket's full-duplex power is wasted here, and long polling is simpler and cheaper to operate. Dropbox famously uses long polling for exactly this. So we pick **long polling**.

The client side of long polling is a simple loop:

```typescript
async function listenForChanges(userId: string): Promise<void> {
  while (true) {
    try {
      // Server holds this request open until a change, or a timeout.
      const res = await fetch(`/notifications/poll?userId=${userId}`);
      if (res.status === 200) {
        const change = await res.json();
        await pullLatest(change.fileId); // go fetch metadata + blocks
      }
      // status 204 = timed out with no change; just loop and reconnect.
    } catch {
      await sleep(1000); // network blip - back off, then reconnect
    }
  }
}
```

### Saving storage space

Storing every version of every file across multiple regions gets expensive fast. Three techniques cut the bill:

**1. De-duplicate identical blocks.** Since a block is identified by its hash, two blocks with the same hash *are* the same bytes - store them once and point both references at the single copy. If you and I upload the same PDF, its blocks are stored a single time.

```typescript
// Store a block only if its content hash is not already present.
class BlockStore {
  private readonly stored = new Set<string>();

  async put(block: Block): Promise<void> {
    if (this.stored.has(block.hash)) return; // dedup: identical bytes already stored
    await uploadToS3(block.hash, block.data);
    this.stored.add(block.hash);
  }
}
```

**2. Limit versions intelligently.** A file edited 1,000 times in an hour does not need 1,000 saved versions. Cap the number kept and weight recent versions more heavily - drop the oldest or least useful when you hit the cap.

**3. Tier cold data to cold storage.** Files untouched for months move to **cold storage** like [S3 Glacier](https://aws.amazon.com/s3/storage-classes/glacier/) - far cheaper per gigabyte, slower to retrieve, which is fine for data nobody is actively opening.

### Failure handling

Interviewers love "what happens when X dies?" Have an answer per component:

- **Load balancer:** run a standby. Load balancers watch each other with a **heartbeat** - a periodic "I am alive" signal; miss enough beats and the standby takes over the traffic.
- **Block server:** stateless work - another block server picks up the pending or unfinished jobs.
- **Cloud storage:** S3 is replicated across regions; if one region is unreachable, read from another.
- **API server:** stateless, so the load balancer just routes around the dead one.
- **Metadata cache:** replicated across nodes; lose one and the others still serve, while a replacement spins up.
- **Metadata DB:** if a replica (a read copy) dies, read from another replica. If the primary (the write copy) dies, promote a replica to primary and bring up a new replica to replace it.
- **Notification service:** each server holds huge numbers of long-poll connections (Dropbox reported over a million per machine). If one dies, every client on it must reconnect to another server - and reconnecting a million clients at once is slow, so this is a real thundering-herd risk to plan capacity for.
- **Offline backup queue:** replicate the queue; if one copy fails, consumers re-subscribe to a backup copy.

## Follow-up questions to expect

**"The block servers make every file cross the network twice. Can you skip them?"** Yes - clients could chunk, compress, encrypt, and upload straight to S3, so bytes travel once and upload is faster. The cost: you reimplement that exact logic on iOS, Android, and web (three chances to ship a bug), and you trust a client - which can be tampered with - to encrypt correctly. It is a real trade-off: faster uploads vs centralized, trustworthy, single-implementation processing. State the trade and let the interviewer pick.

**"A single file is being hammered - a hot file. What breaks?"** The metadata row and its cache entry become a hotspot. Cache the metadata aggressively, and since versions are append-only, reads scale well; serialize the writes with the "first write wins" rule so concurrent edits resolve to conflicts rather than corruption.

**"How do you shard the metadata DB, and what breaks at 10x?"** Shard by user id so all of one user's files sit on one shard (their queries never cross shards). Use [consistent hashing](/what-is-consistent-hashing) so adding shards moves minimal data. The strain at 10x is the notification fleet (10x the open long-poll connections) and cross-region replication lag, not the metadata queries.

**"Where does encryption happen and what about key management?"** Blocks are encrypted on the block servers before hitting S3, so storage is encrypted at rest and transport is HTTPS. In a full design you would add a key-management service holding per-user or per-file keys, and rotate them - worth naming even if you do not design it.

**"How do you keep the metadata cache and DB from disagreeing?"** Invalidate on write: every DB write evicts the stale cache entry so the next read reloads fresh. That is what buys the strong consistency we promised.

## Wrap up

We built Google Drive by letting each number force a component: 500 PB of files pushed bytes into replicated object storage (S3), the bandwidth requirement pushed us to block-level storage with delta sync so only changed 4 MB blocks ever move, and the sync requirement gave us a notification service on long polling plus an offline queue. Metadata lives in a sharded, replicated relational database (ACID for strong consistency) fronted by an invalidate-on-write cache, and the whole thing separates two flows: **file bytes** through block servers to S3, **metadata and coordination** through API servers.

The interesting tension in this design is holding **strong consistency, low bandwidth, and fast sync** at the same time - block-level delta sync is what makes all three possible at once. The likely next bottleneck as you scale is the notification fleet and its millions of open connections, which is why pulling the online/offline logic into a separate **presence service** is the natural next evolution. As with every system design, there is no single right answer - know your trade-offs, and say them out loud.

