The 100 problems, solved / Collaboration and productivity / 056 · Design job alerts for Google Careers

Design job alerts for Google Careers

Match new postings to saved searches for millions of candidates and notify them once.

Concepts this design leans on

01The design, in the six-step framework

1. Requirements

Google Careers lets a candidate save a search (keywords, location, level, team) and asks to be told when a matching job is posted. The design question is a reverse search: instead of matching a query against many documents, match one new document against millions of stored queries.

Functional

  • Candidates save alerts: free-text keywords plus structured filters, with a frequency (instant, daily digest, weekly).
  • When a job is posted or materially edited, every matching alert fires, by email or push.
  • A candidate is notified about a given job at most once per alert, and a digest groups the day's matches.
  • Candidates can pause, edit or delete alerts; unsubscribing works from every email.
  • Out of scope: application tracking, recommendations beyond saved alerts.

Non-functional

  • Scale: 20 million saved alerts, 5,000 new or edited postings a day, bursts of 500 postings in a minute after a hiring freeze lifts.
  • Latency: instant alerts within 5 minutes of posting; digests at the local morning.
  • Correctness: no duplicate notifications; no missed matches for postings visible on the site.
  • Availability: the careers site is independent of the alert pipeline; alert delays never block postings.
Back of the envelope

5,000 postings/day × 20 × 106 alerts = 1011 naive comparisons/day, about 1.2 million a second: too many for brute force

A typical posting matches about 0.5 percent of alerts, so 100,000 notifications per posting and 5 × 108 notifications a day in the worst case; digests cut delivered messages to a few million emails a day.

Alert storage: 20 × 106 × 1 KB = 20 GB, which fits in memory on a handful of machines

2. Core entities

  • Alert: alert id, user id, query terms, filters (locations, levels, teams, employment type), frequency, channel, status, created at.
  • Posting: job id, title, description, structured fields, version, posted at, visibility.
  • Match: alert id, job id, posting version, matched at, delivery status.
  • Digest: user id, period, list of matches, sent at.

3. API

  • POST /alerts with query, filters, frequency, channel; returns the alert id. Called from the careers site.
  • GET /alerts, PATCH /alerts/{id}, DELETE /alerts/{id} for management; GET /unsubscribe?token= for one-click unsubscribe.
  • Internal: POST /postings/{jobId}/published is the event the careers site emits on publish or material edit, with the version.

4. High-level design

ClientsEdgeServicesDatajob publishedload indexinsert matchesCareers siteCandidateweb / mobileAPI gatewayAlert serviceMatcher workers(reverse index)Delivery + digestworkersAlerts DBPosting eventsMatches table(alert, job) uniqueEmail / push
  1. The careers site publishes a posting event with the job id and version onto the queue. Posting is never blocked on alerts.
  2. A matcher worker fetches the posting and runs it through the reverse index: structured filters first (location, level, team) to shrink the candidate alert set, then keyword matching.
  3. Each match is inserted into the matches table with a unique key of (alert id, job id); a duplicate from a retried event is rejected by the key, which is how the pipeline becomes idempotent.
  4. Delivery workers send instant matches at once and fold the rest into digests sent at the user's local morning, through the email and push providers.

5. Deep dives

How do we match one posting against 20 million alerts in seconds?

Build a reverse index over alerts, the mirror of an inverted index: for every keyword, the list of alert ids that contain it; for every structured value (city, level), a posting list of alert ids. A posting's structured fields select a small intersection (alerts wanting "Hyderabad" and "L5" might be 50,000), and the posting's title and description terms then score those alerts. An alert with three required keywords matches when all three posting lists contain the posting's terms; that is a few list intersections per alert, done in memory. The index is 20 GB, so it is sharded by alert id range across, say, 8 matcher machines, each holding its slice in memory and rebuilt from the alerts database every few minutes with a tail of recent alert edits applied live. One posting costs under a second per shard, so a burst of 500 postings in a minute is 500 queue messages processed in parallel.

How do we avoid notifying twice when a posting is edited or an event is retried?

The matches table's unique key on (alert id, job id) makes repeated matching harmless. Edits are the harder case: a fixed typo should not re-alert everyone, but a change of location might create new matches. Treat edits as a new version: the matcher runs the new version, inserts matches for alerts that did not match before (the unique key drops the rest), and only those get a notification. If the edit removes a match, nothing is sent; the earlier notification stands. Delivery itself is at-least-once from the queue, so each send carries an idempotency key of (alert id, job id, channel) checked at the provider call.

How do digests work across time zones and frequencies?

Instant alerts are delivered as matches land. Daily and weekly alerts are not delivered; their matches accumulate in the matches table with status pending. A digest scheduler runs every hour and selects users whose local time is 8 am (user time zone stored on the alert) and who have pending matches for that period, builds one email per user grouped by alert, and marks the matches sent. This is batch on top of a stream: matching is streaming for freshness, delivery is batched for sanity. Rate limits per user (at most one instant email per 10 minutes, overflow folded into the next digest) prevent a burst of 500 postings from sending 500 emails to one candidate.

What about postings that become visible later, or alerts created after the posting?

A posting scheduled for Monday emits its published event on Monday, so the pipeline sees it then. A newly created alert should show existing matches immediately: on creation, run a forward search against the postings index (the normal search engine) for the last 30 days and show the results in the UI, but do not notify for them; mark them as seen so future digests skip them. This avoids the common complaint of an alert that emails the entire backlog on day one.

6. What a strong answer sounds like

  • Name the shape early: reverse search, one document against millions of queries, so the index is over the alerts, not the jobs.
  • Do the arithmetic that rules out brute force, then show the structured-filter-first intersection that makes it cheap.
  • Make dedup a database constraint, not a hope: unique (alert, job) plus idempotent sends.
  • Separate matching (streaming) from delivery (instant or digest), with per-user rate limits.
  • Follow-ups to expect: fuzzy and synonym matching (expand terms at index-build time), ranking matches within a digest, and compliance (one-click unsubscribe, deletion of alerts when an account is deleted).

02Where this question was reported

  • Blind · Job alert function on Google career site; went reasonably but not exceptionally, triggering an extra design round