Email Service (Gmail-like)
Problem statement
Design a web email service: receive mail via SMTP from the internet, store it durably, let users read/search/organise mail in a web/mobile client, send outbound mail, and filter spam.
Requirements
Functional
Receive inbound mail (SMTP); deliver to the right mailbox; handle attachments.
Send outbound mail with retries and bounce handling.
Read, list (threaded), label/folder, star, delete; sync across devices.
Full-text search over a user's mail.
Spam/phishing filtering.
Non-functional
Durability: mail never lost once accepted (SMTP 250 OK = committed).
Inbox list < 300 ms; search < 1 s.
1B users, 100B emails/day incl. spam; 15 GB quota each.
Privacy and per-user isolation; encryption at rest.
Back-of-the-envelope estimates
- 100B emails/day inbound (≈ 80% spam rejected early); 20B stored × 50 KB avg = 1 PB/day → object storage with dedup for attachments/bodies.
- Metadata per email ~1 KB → 20 TB/day in a sharded DB (by user).
- Search index: ~30% of body size → 300 TB/day; per-user indexes sharded.
- SMTP ingress: 100B/86,400 ≈ 1.2M connections/s at peak — many stateless MTA nodes behind DNS/MX load balancing.
API
Protocols: SMTP (25/587) inbound & outbound; IMAP/POP optional; JMAP/REST for clients.
GET /v1/mailboxes/{id}/threads?label=INBOX&cursor=
GET /v1/threads/{id} → messages with bodies (lazy attachments)
POST /v1/messages/send { to[], cc[], subject, body, attachmentIds[] }
PUT /v1/messages/{id}/labels { add[], remove[] }
GET /v1/search?q=from:alice has:attachment "invoice"
GET /v1/attachments/{id} → presigned URL
Data model
users (user_id, email, quota, ...)
messages (user_id, message_id, thread_id, from, to[], subject, snippet, labels[], flags, size, received_at, body_ref) -- sharded by user_id
threads (user_id, thread_id, subject_norm, last_msg_at, participants[])
attachments (attachment_id, content_hash, size, storage_key, mime) -- content-addressed, deduped
Object storage: bodies and attachments by hash
Search: per-user inverted index (Lucene-style shard per user group)
High-level design
- Inbound MTA cluster (MX records → many nodes): accepts SMTP, checks SPF/DKIM/DMARC, runs a fast spam pre-filter (reputation, rate limits), and writes the raw message to a durable queue/journal before replying
250 OK. - Delivery workers parse the message, run the full spam/phishing classifier, extract attachments (store content-addressed), write metadata to the user's shard, assign to a thread (by
In-Reply-To/References+ subject), update labels, emitmail.receivedfor search indexing and push notifications. - Storage: metadata in a sharded relational/wide-column DB by user; bodies/attachments in object storage encrypted per user; quota enforced at delivery.
- Client API serves threaded lists from the metadata shard with pagination; bodies fetched lazily.
- Search: an indexer consumes
mail.receivedand updates the user's index; query service supports operators (from:, has:, dates). - Outbound: send API writes to
Sent, enqueues; outbound MTAs deliver via SMTP with retries/backoff, DKIM signing, bounce processing, and IP-reputation-aware sending pools. - Sync: change sequence per mailbox so clients fetch deltas; push via notification service.
Architecture diagram
Deep dives
Accept-then-process: the SMTP 250 OK is a durability promise, so journal first; everything else (spam, indexing) can be retried.
Threading: use Message-ID/In-Reply-To/References headers; fall back to normalised subject + participants within a time window.
Dedup: attachments and bodies are content-addressed; a 10 MB attachment sent to 1,000 people is stored once.
Spam: layered — connection-level reputation (cheap) → header/auth checks → ML classifier on content; user feedback loops retrain.
Deliverability: warm IP pools, DKIM/DMARC, bounce handling, feedback loops with big providers.
Bottlenecks & mitigations
Spam floods on the MTA tier → reject at connection time by reputation; autoscale MTAs; never let spam reach delivery workers unfiltered.
Hot users (mailing lists receiving 1M/day) → separate shard or rate-limit per mailbox.
Search index writes → batch per user; near-real-time (≤ 1 min) is enough.
Attachment egress → CDN-style presigned URLs, range requests.
Failure modes
- Delivery worker crash: message remains in the journal; redelivered; dedupe by
Message-ID+ user. - Metadata shard outage: inbound mail still journaled (250 OK given); delivery to that shard's users delayed; other users unaffected.
- Outbound recipient server down: retry with backoff for up to 2–3 days, then bounce to sender.
- Index corruption: rebuild a user's index from metadata + bodies.
- Quota exceeded: reject with a temporary SMTP error (4xx) so senders retry, or bounce (5xx) per policy.
Follow-up questions interviewers ask
How would you implement 'undo send'?
How would you build server-side filters/rules efficiently?
How would you support end-to-end encrypted mail?