← System design

Communication

Design Gmail

Email is the oldest messaging system still in heavy use, and it was never designed to be friendly. You are building the part that faces billions of messages a day from servers you do not control, keeps every accepted one safe for years, and still lets a user find a receipt from 2014 in half a second.
Core: durable SMTP intake + per-user mailbox and index1.2B mailboxesWrite once, read rarelySpam defence at the edge

01

What the mail service must do

02

What the mail service must do

Functional
  • Accept mail from any server on the internet for our domains over SMTP, and deliver our users' outgoing mail to any domain.
  • Keep each user's mailbox: read state, stars, labels instead of folders, archive, trash and spam.
  • Group messages into conversations using reply headers, with a fallback when clients drop them.
  • Search everything a user has, including subjects, bodies, sender names and text extracted from attachments, with operators like from:, has:attachment and before:.
  • Run user filters on arrival, sort mail into tabs, and block spam, phishing and malware.
  • Sync quickly to web, mobile and IMAP clients, with push notifications for new mail.
Non-Functional
  • Once our server says 250 OK to a sender, that message is never lost.
  • Inbound mail appears in the inbox within a few seconds of acceptance for the vast majority of messages.
  • Inbox listing and opening a message: p95 under 200 ms. Search: p95 under 500 ms even for mailboxes with 15 years of mail.
  • Receiving stays up at 99.99%; sending can be a little less, because SMTP senders retry for days.
  • Strict isolation: one user's data never appears in another user's results, caches or logs.
Scope it before you start

Email covers a lot of ground. Agree to focus on receiving, storing, searching and syncing, with spam defence and outbound delivery as supporting pieces. Calendar invites, chat, and the compose experience can be mentioned and left out. Ask whether you are designing a consumer service, a business service, or both, because business domains bring retention rules and admin policies that change the storage design.

02

Sizing the mail system

03

Sizing the mail system

Assume 1.2 billion mailboxes with 500 million people active on a given day, 3 billion inbound SMTP attempts a day of which most are junk, and an average mailbox of 9 GB including attachments.

QuantityWorkingResult
Inbound attempts3B / 86,400 sabout 35,000 / s, peaks near 90,000 / s
Rejected before storagebad reputation, failed auth, content blocks: assume 60%about 1.8B a day never stored
Messages delivered to mailboxesremaining 1.2B, including spam folderabout 14,000 / s average
Outbound sendsassumptionabout 400M a day, 4,600 / s
Client read calls500M daily users x 30 list, open and sync calls15B a day, about 174,000 / s
New bytes a day1.2B x 60 KB average, attachments includedabout 72 TB a day before dedup
Total stored mail1.2B x 9 GBabout 10.8 EB logical
Physical footprinterasure coding at about 1.4x, minus attachment deduproughly 13 to 15 EB
Search indexabout 3% of text-bearing bytesa few hundred PB, mostly cold
Metadata rows1.2B mailboxes x about 40,000 messages eachabout 48 trillion message records
What the numbers tell you

Most bytes are never read again. A typical user opens mail from the last few weeks and searches the rest occasionally. That justifies tiering: recent metadata and bodies on fast storage, years of history on cheaper, erasure-coded disks, and a per-user index that can sit cold until the user searches.

Partitioning comes for free

Almost every operation touches one user's mailbox. Sharding by user id keeps reads, writes, search and quotas local to one partition, and the only cross-user operation, delivering one message to many recipients, is a fan-out on the write side.

03

Interfaces

04

Interfaces

There are three faces to this system: SMTP for other mail servers, a JSON API for our own apps, and IMAP for third-party clients. The first two get most of the design attention.

SMTPmx1.mail.example:25Inbound from the internet
// connection opened: IP reputation decides accept, slow down or 421 EHLO relay.shop.example MAIL FROM:<orders@shop.example> // SPF evaluated for this domain and IP RCPT TO:<priya@mail.example> // unknown users rejected here, not after DATA DATA ...headers and body... . // DKIM and DMARC checked, content scanned, then durably queued in two zones 250 2.0.0 OK id=7Qm2xA // only now is the message our responsibility
GET/v1/threads?label=INBOX&page_size=50&page_token=CgQ3MjEList conversations
{ "threads": [ { "thread_id": "t_18c7f", "subject": "Flight change for 14 Oct", "snippet": "Your new departure time is 07:40...", "unread": true, "count": 3 } ], "next_page_token": "CgQ3NzA", "history_id": 88120457 }
GET/v1/search?q=from:airline has:attachment after:2025/01/01Search the user's mail
// parsed into structured filters plus free text, run against this user's index only { "messages": [{ "id": "m_9be2", "thread_id": "t_18c7f" }], "estimated_total": 41 }
GET/v1/history?since=88120457Incremental sync
{ "changes": [ { "type": "message_added", "id": "m_a01c", "labels": ["INBOX", "UNREAD"] }, { "type": "labels_removed", "id": "m_9be2", "labels": ["UNREAD"] } ], "history_id": 88120463 } // 410 if the cursor is too old, telling the client to do a full resync
POST/v1/messages:sendSend a message
// Idempotency-Key from the client draft id, so a flaky network never sends twice { "raw": "base64url MIME...", "thread_id": "t_18c7f" } // 202: stored in Sent at once, handed to the outbound queue for delivery

04

Architecture overview

05

Architecture overview

Inbound, storage, search and syncDiagram
flowchart LR
	EXT["Internet mail servers"] --> MX["SMTP edge fleet"]
	MX --> REP["Reputation and auth checks"]
	MX --> SPAM["Content classifier"]
	MX --> IQ[["Durable inbound queue"]]
	IQ --> DLV["Delivery workers"]
	DLV --> MB[("Mailbox store, sharded by user")]
	DLV --> BLOB[("Blob store for bodies and attachments")]
	DLV -. "change event" .-> IDX["Per-user indexer"]
	IDX --> SI[("Search index segments")]
	APP["Web and mobile apps"] --> API["Mail API"]
	API --> MB
	API --> SI
	API --> OQ[["Outbound queue"]]
	OQ --> OUT["Outbound SMTP fleet"]

Mail enters through a fleet of SMTP servers that face the open internet. They are designed to be suspicious and cheap: most of the work they do is saying no. Messages that survive the checks are written to a durable inbound queue replicated across zones before the server answers 250, so a crash after that point cannot lose the message.

Delivery workers drain the queue. For each recipient they run the user's filters, choose labels and a tab, find or create the conversation, store the body, and append a row to the user's mailbox. Each change also bumps the user's history id and emits an event that the indexer and the push notification service consume. The Mail API serves clients from the mailbox store and search index, and hands outgoing mail to an outbound queue that a separate SMTP fleet delivers with retries.

SMTP edge

Terminates TLS, enforces connection limits per IP, looks up reputation, checks SPF, DKIM and DMARC, and runs a fast content model. It rejects during the SMTP conversation wherever possible, so bounce responsibility stays with the sender.

Mailbox store

A wide-column store in the style of Bigtable, with rows keyed by user and then message id. It holds headers, labels, flags, thread ids and a pointer to the body. Each user's rows sit together, so listing a label is a range scan.

Blob store

Bodies and attachments live here, addressed by content hash, so a 4 MB PDF mailed to 3,000 employees is stored once and referenced many times.

Search and sync

A per-user inverted index built from change events, plus a history log per mailbox that lets every client ask "what changed since id N" instead of re-downloading the inbox.

05

Deep dive: from 250 OK to searchable inbox

06

Deep dive: from 250 OK to searchable inbox

Saying no early

Most inbound connections are abusive, so the edge works in order of cost. First the IP: a reputation lookup against our own history and shared blocklists can refuse a connection or answer with a temporary 421, which legitimate servers retry and many spam tools do not. Next the envelope: RCPT TO for an unknown address is rejected immediately, which stops directory harvesting. Then the domain: SPF says whether this IP may send for the envelope domain, DKIM proves the domain signed the content, and DMARC tells us what the domain owner wants done when those fail.

Only after all that does the edge spend real CPU on content. A fast classifier looks at headers, URLs, text and sender history and returns a verdict in milliseconds: reject, quarantine, deliver to spam, or deliver. Rejecting during the SMTP session matters. If we accepted and later bounced, our bounce would go to a forged sender address and we would become a source of junk ourselves.

The durability line

SMTP has a clear contract: before 250, the sender keeps the message and will retry; after 250, it is ours. So the edge writes the full message to a queue that is synchronously replicated to a second zone before replying. Everything downstream, from filters to indexing, can then fail and retry without risk, because the source of truth is already safe. If the queue write fails, the edge answers 451 and the sending server simply tries again later.

One message, many recipientsDiagram
flowchart TD
	Q["Message in durable queue"] --> H["Hash body and attachments"]
	H --> B["Write blobs if hash is new"]
	B --> F["Split by recipient"]
	F --> R1["Recipient A: filters, labels, tab"]
	F --> R2["Recipient B: filters, labels, tab"]
	R1 --> T1["Find or create thread"]
	R2 --> T2["Find or create thread"]
	T1 --> W1["Append mailbox row, bump history id"]
	T2 --> W2["Append mailbox row, bump history id"]
	W1 --> E["Change events to indexer and push"]
	W2 --> E

Labels are an index, not a location

Folders put each message in exactly one place. Labels let a message be in Inbox, Work and Travel at once, and "archive" simply removes the Inbox label. Store the message row once, with its label set as a field, and maintain a secondary row per label: (user, label, reverse_time, message_id). Listing Inbox newest-first is then a short range scan of the Inbox rows for that user. Changing a label rewrites a handful of small rows rather than moving any data, which is why archiving thousands of messages is cheap.

Unread counts per label are kept as counters next to the label rows and corrected periodically by a recount, because counters that drift by one or two are a common source of user complaints.

Threading without trusting clients

The standard way to link replies is the Message-ID of each message plus the In-Reply-To and References headers of replies. When those are present, the delivery worker looks up the referenced ids in a small per-user map and attaches the new message to the same thread. Plenty of clients and mailing list servers mangle those headers, so fall back to a normalised subject (strip "Re:", "Fwd:" and list tags) within a time window and among overlapping participants. Keep the fallback conservative. Merging two unrelated "Invoice" emails into one thread is more annoying than leaving a reply on its own.

Search per user

A single global index over trillions of messages would be enormous and would need a security filter on every query. Mail search is always scoped to one user, so build one small index per mailbox, stored as immutable segments in the same shard as the mailbox. New mail is added to a small in-memory segment and flushed periodically; background merges combine small segments into large ones, the way an LSM tree compacts. Structured operators such as from:, label: and after: become filters on indexed fields, and free text runs against the tokenised subject, body and attachment text.

Most users rarely search, so their segments live on cold storage and are loaded on first query, which costs a second or so once and is then cached. Heavy searchers keep theirs warm. Because the index is derived data, a corrupted or outdated one can always be rebuilt from the mailbox store.

Sync with a history log

Every mutation to a mailbox (new message, label change, deletion) appends an entry to that user's history log and increments its history id. A client keeps the last id it saw and asks for changes since then, receiving a short list instead of the whole inbox. Push notifications only need to say "something changed, fetch history", which keeps them tiny and safe to drop. The log is trimmed after a few weeks; a client that has been offline longer gets a 410 and does a full resync of recent mail.

06

Decisions and their costs

07

Decisions and their costs

Chosen
Labels with per-label index rows

A message can carry many labels, archive is a label removal, and nothing ever moves. It costs extra index rows per label and careful counter maintenance, and IMAP clients that expect folders need a translation layer.

Flexible and cheap to change
Alternative
Classic folders

Maps directly to IMAP and is simple to store, since each message has one parent. Users who want a message in two places must copy it, and moving large folders becomes real data movement.

Simpler, less useful
Chosen
One search index per mailbox

Privacy is structural because a query cannot touch another user's data, indexes stay small, and cold ones cost nothing until used. The cost is billions of small indexes to manage and slower first searches for inactive users.

Fits a per-user workload
Rejected
Global index filtered by user

Easier to operate as one cluster, but every query needs a user filter that must never be forgotten, and posting lists for common words span billions of users you will never return.

Wrong shape for mail
Chosen
Reject during SMTP when possible

The sender's server is told immediately and owns the bounce, so we never send backscatter to forged addresses. It requires classifiers fast enough to run inside the session, which limits how heavy the content model can be.

Protects our sender reputation
Alternative
Accept everything, filter later

Lets heavier models run with no time pressure. But every rejection becomes a bounce we must send, often to an innocent forged address, and storage absorbs all the junk first.

For the second-pass scan only
Chosen
Content-addressed attachments

The same file sent to a whole company is stored once. Deletion becomes reference counting, which must be done carefully so that one user's delete never removes a blob another user still points to.

Large savings on business mail
Alternative
Copy per recipient

Each mailbox is fully self-contained, and deletion is trivial. Storage grows with recipient count, which is painful for newsletters and large internal announcements.

Simpler, costlier

07

Failure modes

08

Failure modes

📭
An important message lands in spam

A job offer from a small company's misconfigured mail server fails DKIM and looks unfamiliar. The candidate never sees it.

Personalise verdicts with the user's history: people they have written to, contacts and past "not spam" clicks outweigh weak global signals. Feed spam and not-spam reports back into training, and show a clear "why is this in spam" note so users learn to check.
🎣
A phishing link passes the filter

A freshly registered domain hosts a fake login page that looks harmless at delivery time and turns malicious an hour later.

Check links again when they are clicked, not only on arrival, by routing them through a safety lookup. Warn on lookalike sender domains and on first contact from outside an organisation, and make account protection (second factor, suspicious sign-in alerts) the backstop.
🌊
A flood aimed at one mailbox

An attacker signs a victim up to thousands of newsletters to bury a fraud alert, or a misbehaving system mails one address 50,000 times an hour.

Rate limit per recipient at the edge with temporary failures, so legitimate senders retry later. Collapse repeated near-identical messages, and detect subscription bombs by the burst of confirmation emails.
🚫
Outbound IPs land on a blocklist

A batch of compromised accounts sends spam through us. Major receivers start rejecting mail from those IPs, and ordinary users' mail bounces too.

Score outgoing mail like incoming mail, cap new and suspicious accounts, and send from separate IP pools by trust level so bad traffic cannot damage good. Watch feedback loops from large receivers and act within minutes.
🔍
Indexer falls behind

A bug in attachment text extraction stalls the indexer on certain PDFs. New mail arrives in the inbox but does not show up in search.

Isolate slow extraction on separate workers with timeouts, index headers and body first and attachments after, and alert on per-shard index lag. Search can also scan the newest unindexed messages directly as a fallback.
💾
A storage zone goes offline

A power event takes out one zone holding replicas for millions of mailboxes.

Mailbox data is replicated across zones and blobs are erasure coded across them, so reads continue from survivors. Inbound queues already hold a second copy, so delivery resumes where it stopped and nothing accepted is lost.

08

How to run the interview

09

How to run the interview

1

Draw the 250 OK line. Explaining exactly when responsibility passes from sender to you shows you understand SMTP rather than treating email like any other API.

2

Shard by user and say why. Reads, search, quotas and privacy all follow from it. The one cross-user operation is fan-out at delivery.

3

Layer spam defence by cost. IP, envelope, authentication, then content. Name SPF, DKIM and DMARC and what each one proves.

4

Model labels as index rows. It answers "how does archive work" and "how is the inbox listed quickly" in one sentence.

5

Argue for per-user search indexes. Interviewers often push toward one big cluster; explain why mail is different.

6

Explain sync with a change log. A history id per mailbox makes mobile clients cheap and push notifications trivial.

7

Remember the outbound side. Sending reputation is a shared resource, and protecting it from compromised accounts is part of the design.

09

How the design grows

11

How the design grows

Stage 1: one organisation
An off-the-shelf mail server

Postfix for SMTP, Dovecot for IMAP, folders as directories on disk, and a basic spam filter. Fine for thousands of mailboxes.

Stage 2: millions of users
Split edge, storage and clients

A stateless SMTP fleet, mailboxes in a distributed store sharded by user, bodies in object storage, and a web app on top of a JSON API.

Stage 3: users stop filing mail
Search first, labels and threads

Per-user indexes make archive and search a workable habit. Labels replace folders and conversations replace flat lists.

Stage 4: spam becomes most of the traffic
Authentication and learned filters

SPF, DKIM and DMARC enforcement, sender reputation, and models trained on user reports, with rejection pushed into the SMTP session.

Stage 5: mail on every device
History-based sync, tabs and assistance

Incremental sync for mobile, automatic tabs for promotions and updates, and suggested replies and summaries layered on top of the same store.

Try next

Free to read · better with landed

Whiteboard this with landed

landed runs it as a live system design round on the whiteboard and grades your trade-offs.