The Pre-Computing Pattern: Compute on Write, Not on Read

Pre-computing explained: move expensive, repeated queries to write time with materialized views, denormalization, indexes and fan-out; trade-offs and hybrids.

Intermediate 3 min read · Updated

A product manager clicks "View dashboard" and watches a spinner while the database aggregates millions of rows, joins user data and computes percentiles, and 200 colleagues are waiting on the same computation. The insight behind pre-computing: for predictable, repeated queries, compute the result when the data changes, not when someone asks.

The problem: repeated expensive queries

SELECT DATE(created_at), COUNT(DISTINCT user_id)
FROM events
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY DATE(created_at)
ORDER BY 1;

Over 50 million events this scans gigabytes, and 200 product managers run it all day. If the data changes once a minute but is queried 100 times a minute, you do roughly 100× more computation than necessary.

Trade write cost for read speed

Compute on readCompute on write10 usersHeavy query × 10scan, join, aggregate each timeRaw dataData changesCompute onceincremental updatePre-computed result10 users: 10 lookups
Compute-on-read pays the full cost on every request; compute-on-write pays once (often incrementally) when data changes, and every read is a lookup.

With compute-on-read, every query pays the full cost and response time grows with data volume. With compute-on-write, the cost is paid when data is written (often incrementally), and reads become simple lookups. The pattern goes by many names:

  • [Materialized views](/learn/materialized-views) persist query results, often aggregations.
  • [Denormalization](/learn/normalization-denormalization) pre-joins related tables.
  • [Search indexes](/learn/search-and-inverted-index) pre-process text for fast retrieval.
  • [Fan-out on write](/learn/fan-out) pre-distributes content to followers' feeds.
  • [Write-through caches](/learn/cache-strategies) populate cache entries as data is written.

Worked example: a social feed

Compute-on-read joins your follow list with all posts on every request:

SELECT posts.* FROM posts
JOIN follows ON posts.author_id = follows.following_id
WHERE follows.follower_id = ?
ORDER BY posts.created_at DESC
LIMIT 20;

Follow 500 accounts with 1,000 posts each and the database may consider up to 500,000 candidate posts, depending on indexes and the plan. Pre-computing reverses the work:

  1. Author publishes: the post is saved to the posts table.
  2. Fan-out: a background worker reads the author's followers and inserts the post reference into each follower's feed (e.g. a Redis list or a feed table).
  3. User opens the feed: read the pre-built list; some filtering and hydration remain, but the expensive follows-to-posts join is gone and reads typically take low milliseconds.
import redis
r = redis.Redis()
FEED_LEN = 800

def on_post_created(post_id: int, author_id: int, followers: list[int]):   # write path (async worker)
    pipe = r.pipeline()
    for f in followers:
        pipe.lpush(f"feed:{f}", post_id)
        pipe.ltrim(f"feed:{f}", 0, FEED_LEN - 1)     # keep the newest 800 only
    pipe.execute()

def read_feed(user_id: int, page: int = 0, size: int = 20) -> list[int]:  # read path
    start = page * size
    return [int(x) for x in r.lrange(f"feed:{user_id}", start, start + size - 1)]

The trade-offs

  • More storage: denormalized copies and materialized results take space; storing post references for millions of followers multiplies it.
  • More write-side work: every write triggers computation, synchronously or in background workers. When an account with 10 million followers posts, fan-out takes a while, so async processing trades latency for eventual consistency.
  • Staleness: results reflect the moment they were computed; decide how stale is acceptable.
  • Less flexibility: it speeds up known queries only; a new aggregation or filter may not benefit.
  • More complexity: source data and derived views must both be maintained; schema changes touch both, and debugging spans several data paths.

When to pre-compute, and when not to

Pre-compute whenCompute on read when
Read-heavy: reads far outnumber writes (a dashboard viewed 1,000 times a day, updated hourly)Write-heavy: you'd pay on every write but rarely serve reads
Predictable queries: fixed dashboard metrics, known feedsUnpredictable queries: exploration, debugging, ad-hoc analysis
Staleness is acceptable: "updated every 5 minutes" is fineStrict consistency: balances, inventory counts (or maintain views synchronously)
Expensive computation: big aggregations, multi-table joins, scans of millions of rowsCheap queries that already run fast, or a tight storage budget

Implementation strategies

StrategyFits
Materialized views (database or streaming)Dashboards and reports; refreshed automatically, manually or on a schedule
Application-level cache (Redis, Memcached with TTLs)Computed API responses; see cache-aside
DenormalizationData that's always joined; accept keeping copies in sync
Fan-out on writeContent distribution: push at creation instead of pulling at read; see push vs pull
Search indexesText queries; Elasticsearch pre-processes documents for full-text search
CQRS read modelsSeparate read-optimized models fed from writes; see CQRS

Hybrid approaches

Production systems mix strategies. Twitter historically fanned out on write for most users but computed on read for accounts with millions of followers, because fan-out for them would be prohibitive. The same idea generalizes: pre-compute common cases and compute edge cases on demand; pre-compute hot data and let cold data compute on read; pre-compute where staleness is fine and compute fresh on critical paths. Match the strategy to the access pattern rather than applying one everywhere.

Where it’s used

News feeds

Fan-out on write into per-user timelines, with on-read merging for celebrity accounts.

Dashboards

Rollups and materialized views refreshed every few minutes instead of live scans.

E-commerce listings

Denormalized product documents with price, stock and rating ready to render.

Leaderboards

Redis sorted sets updated on each score change, read in O(log n).

In an interview

  • When a read is expensive and repeated, move the work to write time and say how stale the result may be.
  • Name the concrete form: materialized view, denormalized table, cache, index or fan-out.
  • Quantify the write amplification (e.g. followers per post) and add a hybrid path for outliers.