Design Gmail-scale email
Design distributed email: SMTP ingest with spam filtering, mailboxes sharded per user, attachments deduplicated in object storage, a per-user inverted index for search, labels as a many-to-many overlay (not folders), and IMAP-style incremental sync via a per-mailbox change log.
“Email is solved, it’s just SMTP” — until you have to build it for a billion users and notice every assumption breaking. A newsletter goes out to 50 million subscribers: do you really store 50 million identical copies of the same 2 MB PDF attachment? A user searches “invoice from acme march” across 200,000 messages and wants results in 50 ms — are you going to scan their whole mailbox? Their phone, laptop, and webmail must all show the same read/unread state and the same labels instantly — how do they stay in sync without each re-downloading everything? And 90% of incoming mail is spam you must reject before it ever costs you storage. Email looks like a CRUD app and is actually a fan-out problem, a search problem, a dedup problem, and a sync problem stacked together — and the design that handles a thousand mailboxes collapses at a billion unless you shard per user, deduplicate the bytes, index for search, and sync by change-log rather than by copy.
Requirements
When a system handles a billion mailboxes, every design decision you take for granted at a thousand becomes a scaling crisis. Before reading the requirements, ask yourself: which of these — fan-out delivery, search, attachment storage, or sync — will dominate cost, and which will dominate latency? Functional — receive mail via SMTP and deliver it to the right mailbox; send mail; read/list messages; full-text search across a mailbox; organize with labels (a message can have many); handle attachments; keep multiple clients (web, phone, desktop) in sync including read/unread, starred, label state. Spam must be filtered. IMAP/JMAP-style protocols for clients.
Non-functional — durability (losing mail is unacceptable), per-user scale (a mailbox can hold hundreds of thousands of messages; the system holds a billion mailboxes), search latency (sub-100 ms over a huge mailbox), sync efficiency (clients must not re-fetch everything), and abuse resistance (spam is the majority of inbound volume and must be cheap to reject). Reads (opening, listing, searching) vastly outnumber sends.
The shaping facts: data partitions cleanly per user (your mail is independent of mine), attachments are huge and often duplicated, search needs an index not a scan, and sync must be incremental.
Estimation
Users: ~1B mailboxes
Inbound: ~100s of billions of messages/day; ~90% spam (rejected at ingest)
legit ~1e10/day ÷ ~1e5 s ≈ ~1e5 messages/sec delivered, peak higher
Mailbox: avg tens of thousands of messages; power users 100K+
Message: headers + body ~ tens of KB; attachments KB–tens of MB (the bulk of bytes)
Storage: 1B users × ~15 GB ≈ ~1e19 bytes (~10s of EB) — attachments dominate
Dedup: one viral attachment sent to millions → store ONE physical blob, ref-count it
Search: inverted index per mailbox; sub-100 ms requires index, never a scan
Sync: per-mailbox monotonic change counter → clients fetch only changes since XThe numbers force four decisions immediately: shard by user (the only natural partition key), put attachments in deduplicated object storage (or you store the same newsletter PDF millions of times), build a per-mailbox inverted index (a scan of 200K messages can’t hit 50 ms), and sync by a change log (re-listing a 200K-message mailbox on every device is absurd).
High-level design
Inbound mail hits SMTP servers behind the MX records; a spam/filter stage rejects or quarantines the majority before storage; accepted messages are written to the recipient’s mailbox shard (metadata + body), with attachments split off to deduplicated object storage and the message storing only a reference. An indexer adds the message to the user’s search index. Clients read and sync through an API/IMAP layer that uses a per-mailbox change log to serve only deltas.
The partition key is the user: each mailbox is independent, so you shard the message store, the index, and the change log all by user ID. This is the cleanest possible sharding because there are no cross-user joins in the common path — your inbox never needs mine — which is why email scales horizontally so well once you commit to per-user partitioning.
Deep dive: attachments, dedup, and search
A message is two very different things glued together: a small structured part (headers, recipients, body text, label set) and potentially large attachments. Store them separately — the message body and metadata in the mailbox shard, the attachment bytes in object storage, content-addressed and deduplicated (the same idea as the Drive lesson). A 2 MB PDF newsletter sent to 50 million people is one physical blob with 50 million references, not 50 million copies — without dedup, attachments would dominate and bankrupt storage. The message simply holds a pointer (the content hash) plus a ref-count so the blob is deleted only when the last referencing message is gone.
Search cannot be a scan. A mailbox of 200,000 messages queried for “invoice acme march” must answer in tens of milliseconds, so you build a per-user inverted index: a map from each term to the list of message IDs containing it, so a query intersects a few short posting lists instead of reading every message. The index is sharded by user (like everything else), updated asynchronously by the indexer as mail arrives. Per-user indexing keeps each index small and the query local to one user’s shard — you never search across users, so the index never needs to be global.
▸Why this works
Why model labels as a many-to-many overlay instead of classic folders, and why does it matter at scale? A folder is a location: a message lives in exactly one place, so “move to folder” physically relocates it and a message can’t be in two folders at once. Gmail’s insight is that organization is really tagging: one message can be Work and Receipts and Important simultaneously, and Inbox/Archive are just labels too (archiving removes the Inbox label, it doesn’t move bytes). So you model it as a many-to-many relation — a message has a set of label IDs — rather than a parent-folder pointer. This matters because (1) it avoids ever moving the message’s bytes when you reorganize (you mutate a small label set), (2) listing “everything labeled Work” is an index lookup, not a directory walk, and (3) it composes with search trivially (a label is just another indexed term). The folder model forces single-location semantics that real email organization doesn’t fit; labels are the data model that matches the domain.
Deep dive: incremental sync and spam
Three devices show one mailbox; none may re-download 200,000 messages to refresh. The mechanism is a per-mailbox change log with a monotonic counter (Gmail’s HISTORYID, IMAP’s MODSEQ/CONDSTORE, JMAP’s state string). Every mutation — new message, read/unread flip, label change, delete — increments the mailbox’s counter and is recorded. A client remembers the last counter it saw and asks “give me everything since counter X”; the server returns just that delta. So opening your phone after your laptop marked ten emails read fetches ten changes, not the whole mailbox. This is the same push-then-pull-delta shape as the Drive lesson: the change log makes sync O(changes), not O(mailbox size), which is the only thing that works at hundreds of thousands of messages per user.
Spam is filtered at ingest, before storage, for a blunt economic reason: ~90% of inbound is spam, and storing-then-filtering would mean paying to durably store, index, and replicate ten times more mail than you keep. The SMTP stage applies cheap rejects first (reputation/DNSBL on the sending IP, SPF/DKIM/DMARC authentication failures, rate limits) and then content classification (ML models over headers and body), rejecting outright or routing to a Spam label. Reject early and cheap; the goal is to never spend durable storage on mail you’ll throw away.
▸Common mistake
A scaling mistake that seems harmless at first: implementing the sent-to-many case as a write to every recipient’s mailbox of a full copy including the attachment bytes. For a newsletter to 50M users that’s 50M × 2 MB = 100 TB written for one send — and you’ve stored the same PDF 50 million times. The fix has two parts: deduplicate the attachment (one blob, ref-counted, 50M pointers) so the bytes are stored once, and recognize that fan-out delivery still writes 50M small message rows (each mailbox genuinely receives the message) but those rows are tiny without the embedded bytes. The error is conflating “deliver to 50M inboxes” (unavoidable small writes, the fan-out) with “store 50M copies of the attachment” (avoidable, via dedup). Separate the heavy bytes from the light delivery and the send becomes affordable.
A 2 MB PDF attachment is emailed to 50 million subscribers. How should attachment storage be designed?
A user's phone, laptop, and webmail must show the same mailbox state without each re-downloading 200,000 messages. What's the mechanism?
Because your inbox never needs to join against mine, the message store, the search index, and the change log are all partitioned by the same key. That natural partition key is the _______.
Bottlenecks & tradeoffs
When you are reviewing this design in an interview or code review, the questions to stress-test are: what happens if dedup breaks and ref-counts drift, and what’s the worst-case indexing lag before a new message becomes searchable? The byte cost is dominated by attachments, controlled by dedup (one ref-counted blob, not N copies) — the central storage tradeoff. Search trades index storage and async indexing latency for sub-100 ms queries; the alternative (scanning) doesn’t scale, so the index is non-negotiable. Sync trades a small per-mailbox change log for O(changes) refresh instead of O(mailbox) re-listing. Spam trades some false-positive risk for the economics of rejecting 90% of volume before storage — reject early, reject cheap. The labels-as-overlay model trades the familiar single-location folder mental model for a many-to-many tag set that never moves bytes and composes with search. And the foundational decision — partition everything by user — is what makes all of this scale, because email’s data has no cross-user coupling on the hot path. As with every design in this unit: the heavy bytes (attachments) go to deduplicated object storage, and the structured state (messages, labels, index, change log) lives in per-user sharded stores where its consistency is cheap to maintain.
- 01Why partition email by user, and how are attachments and search handled?
- 02Why are labels a many-to-many overlay instead of folders?
- 03How does incremental sync work and why filter spam at ingest?
“Email is just SMTP” hides four stacked problems that only appear at a billion users. The foundational move is to partition everything by user — message store, search index, and change log all shard by user ID, because mailboxes are independent with no cross-user joins on the hot path, the cleanest sharding in this unit. Inbound mail is screened by a spam filter at SMTP ingest, before storage (~90% of volume rejected with cheap IP/auth checks then content classification), so you never pay durable storage for mail you discard. Accepted messages split into a small structured part (headers, body, label set) stored in the mailbox shard and large attachments stored content-addressed and deduplicated in object storage — a newsletter PDF to 50M users is one ref-counted blob with 50M pointers, not 50M copies; the fan-out writes only tiny message rows. Search is served by a per-user inverted index (term → posting lists), never a scan, so a 200K-message mailbox answers in tens of milliseconds. Labels are modelled as a many-to-many overlay rather than folders, so reorganizing mutates a tiny tag set instead of moving bytes and composes with search. And clients stay consistent via incremental sync over a per-mailbox monotonic change log (HISTORYID/MODSEQ/JMAP state) — fetch only changes since your last counter, O(changes) not O(mailbox). As across the unit, the heavy bytes go to deduplicated object storage while the per-user structured state carries the consistency. Now when you see “email is just CRUD” in a design review, you know which questions to raise: are attachments deduplicated, does search have an index, is sync driven by a change log, and is spam rejected before it ever touches storage?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.