Skip to main content

Design a Weather App

Premium

You're asked to design the backend for a weather service. A user opens the app and sees the current conditions and a forecast for wherever they are; they can check another city; and they get a push notification when a severe storm is heading toward them. Behind that, forecast data arrives from a handful of numerical weather models that publish a fresh global run every few hours.

The serving path is a simple read-heavy system. The pipeline is harder because every few hours a model publishes hundreds of gigabytes describing a new global forecast. Let's work through how to ingest and publish that dataset without exposing a partial update.

Clarify the requirements

  • What does a query look like? A latitude and longitude, not a city. Cities are a lookup on top; the underlying data is a grid.
  • How current does the forecast need to be? Forecasts update on the model's schedule, every one to six hours. Current conditions from nearby stations update more often.
  • Are severe weather alerts in scope? Confirm this early because alerts have a hard latency requirement and need a separate path.
  • How far out does the forecast go? Hourly for a couple of days and daily for a week or two is standard, and it sets the data volume.
  • Where does the data come from? Government models such as GFS and ECMWF, plus station observations and radar. You're consuming these, not producing them.

Assume: coordinate-based queries, forecasts refreshed on the model cycle, severe alerts in scope, and a two-week horizon.

Back-of-envelope numbers

  • Users: 100M daily actives×3 checks=300M requests/day≈3.5k/sec100\text{M daily actives} \times 3 \text{ checks} = 300\text{M requests/day} \approx 3.5\text{k/sec}, peaking near 20k/sec20\text{k/sec} in the morning
  • Grid size: a 0.25°0.25° global grid is roughly 1,440×720≈1M points1{,}440 \times 720 \approx 1\text{M points}
  • Forecast volume per run: 1M points×120 hourly steps×20 variables×4 B≈10 GB1\text{M points} \times 120 \text{ hourly steps} \times 20 \text{ variables} \times 4\text{ B} \approx 10\text{ GB} before compression, and considerably more at higher resolution
  • Ingest cadence: a full run every 6 hours6\text{ hours}, arriving over roughly 30 minutes30\text{ minutes}
  • Writes to reads: 4 bulk loads/day4 \text{ bulk loads/day} against 300M queries/day300\text{M queries/day}

Four bulk loads per day serve 300 million daily queries. Precompute the query layout after each load and serve the immutable result through caches.

High-level architecture

Model runs + observations

① Ingestion job

② Raw grid files

③ Processing job

④ Forecast store
versioned

⑤ Forecast API

⑥ Cache + CDN

⑦ Alert stream

⑧ Push service

Components
  1. Ingestion job. Pulls each model run as it publishes, in parallel across variables and forecast hours.
  2. Raw grid files. The source data as delivered, kept so processing can be re-run.
  3. Processing job. Decodes, interpolates, derives fields, and writes the query-optimized layout.
  4. Forecast store. The served dataset, written as a new immutable version per run.
  5. Forecast API. Resolves a coordinate to grid cells and assembles the response.
  6. Cache and CDN. Where nearly every request is actually answered.
  7. Alert stream. Severe weather warnings, on a separate low-latency path.
  8. Push service. Fans alerts out to affected users by location.
Each model run is processed into a new immutable dataset version and published atomically. Queries read the current version through a cache and never touch the ingestion path.

Deep dive 1: publishing a new model run without a gap

A model run replaces the entire forecast dataset. Writing those updates in place would mean serving a mixture of old and new data for the half hour the load takes, which produces visibly inconsistent forecasts, with the same location showing Tuesday's rain from the new run and Wednesday's sun from the old one.

Write each run as a new immutable version and swap a pointer. Processing writes to a version identified by the run, such as gfs/2026-07-28T12Z, and only when the whole version is complete and validated does a pointer flip to make it current. Readers resolve the pointer once per request, so the switch is atomic from their perspective.

That structure gives you three things beyond consistency:

  • Rollback is a pointer flip. A bad run is reverted in seconds by pointing back at the previous version, which is a far better position than repairing partially overwritten data.
  • Runs can be processed in parallel with serving. The next run is built while the current one is being read, with no contention.
  • Validation has somewhere to happen. Between "complete" and "current" there's a gate where you check the run before anyone sees it.

What to validate: completeness first, meaning every forecast hour and variable is present, since a truncated run is the common failure. Then plausibility, comparing the new run's distribution against the previous one. A model producing continental temperatures twenty degrees off is a real occurrence, and a distribution check is what stops it from reaching users.

Deep dive 2: laying out the data for the query you actually serve

The source data is organized the way models produce it: one file per variable per forecast hour, covering the whole globe. The query is the transpose of that: one location, every variable, every hour. Answering it from the source layout would mean opening a hundred and twenty files and reading four bytes from each.

Reorganize on ingest so a query is one read. Group the grid into tiles, and within each tile store the full time series for every point. A request for one coordinate then fetches a single tile and slices out the point, which is one lookup instead of hundreds.

Two details make that work well in practice:

  • Tile size is a tuning decision. Too large and every query drags in data it doesn't need; too small and you lose the benefit of one read. A tile covering a few degrees is the usual landing spot.
  • Interpolate between grid points. A user isn't standing on a grid vertex. Bilinear interpolation from the four surrounding points at query time is cheap and avoids the discontinuity of snapping to the nearest cell, which users notice as the forecast changing when they cross an invisible line.

Connect the layout to the query: store derived data in the shape readers need and pay the reorganization cost once per model run instead of on 300 million daily reads.

Deep dive 3: serving 300 million requests from four writes a day

With the dataset immutable between runs, essentially everything is cacheable, and the design becomes an exercise in making that explicit.

  • Cache by coordinate, rounded. Full precision would give every user a unique key and a zero hit rate. Rounding to two decimal places, roughly a kilometer, collapses everyone in a neighborhood onto one entry and is well within the accuracy the data actually has.
  • Set the TTL from the run schedule, not from a guess. The data genuinely doesn't change until the next run publishes, so expiry should align with that rather than an arbitrary five minutes. Include the version in the cache key and stale entries fall out naturally at the swap.
  • Put popular locations on a CDN. Requests concentrate heavily in cities, so a small number of keys serve a large share of traffic, and those belong at the edge close to users.
  • Warm the cache after a swap. A cold cache immediately after publication means a thundering herd of requests reaching the store at once. Pre-populating the busiest locations as part of the publication step turns that spike into a controlled batch.

The result is that the origin sees a small fraction of user traffic, which is what makes the read side of this system almost boring, appropriately so, since all of the difficulty was moved upstream into the pipeline.

Deep dive 4: severe weather alerts

Alerts are the one part of this system that can't wait for the next model run, and treating them as an extension of the forecast path is the mistake to avoid.

They ride a separate low-latency path. Warnings are issued by meteorological agencies as discrete events, and they should flow through a stream to push delivery directly, without waiting on any batch processing. A tornado warning that arrives on the six-hour cycle is not a product feature.

The fan-out is geographic. A warning covers a polygon, and the system must find every user inside it. This is the geospatial indexing problem from Design Yelp: index users by geohash or S2 cell, find cells that intersect the polygon, then refine the matches. A large metro warning may require millions of notifications in a short window, so size the push path for that burst.

Deduplicate before sending. Agencies reissue and extend warnings, and a user who gets six notifications for one storm turns off notifications permanently. Key delivery on the warning's identity per user, so an extension updates rather than re-alerts.

Common pitfalls

  • Updating the forecast dataset in place. Users see a mixture of two model runs for the duration of the load.
  • Querying the source layout directly. One coordinate lookup becomes hundreds of file reads.
  • A cold cache after each publication. The swap creates a thundering herd against the origin.
  • Alerts on the forecast path. A severe weather warning that waits for the next batch is useless.
  • No deduplication on alerts. Reissued warnings train users to disable notifications.

Leveling signals

Mid-levelIngests model output on a schedule, stores forecasts keyed by location, and serves reads through a cache.
SeniorPublishes each run as an immutable version with an atomic pointer swap, reorganizes the data into tiles so a coordinate query is one read, interpolates between grid points, and sets cache keys and TTLs from the run schedule.
Staff+Validates completeness and distribution before publication and treats rollback as a pointer flip. Warms the cache at swap time to avoid a herd, and runs severe alerts on a separate path with polygon-based geospatial fan-out and per-user deduplication.
Design YelpMedium

Search local businesses by proximity, with filters and aggregate ratings.

Design a Metrics and Logging ServiceHard

Collect, index, and retain metrics and logs from thousands of services.