FullStack Course LogoFullStack Course
Module: System Design
System Design·256·14 MIN READ

256: Indexes, Access Patterns, Query Models, and Read/Write Amplification

TOPICS COVERED: Indexes, Access Patterns, Query Models, and Read/Write Amplification

Learning outcomes

By the end of this lesson, you can:

  • explain and apply access-pattern first in a realistic implementation;
  • explain and apply primary and secondary indexes in a realistic implementation;
  • explain and apply read amplification in a realistic implementation;
  • explain and apply write amplification in a realistic implementation;
  • explain and apply materialized views in a realistic implementation.

Prerequisites and retrieval

This lesson assumes the earlier 01–06 foundation and the preceding lessons in this module. Before you begin, retrieve one concrete example from an earlier project in which the same concern appeared. Perhaps a list endpoint became slow because it scanned too much data, or a write became expensive after several indexes and replicas were added. The point is not to memorize a set of database terms. It is to make a defensible choice inside a large-scale distributed service, where requirements, traffic, failure modes, cost, and operational constraints all need to be explicit.

Terminology

  • Access-pattern first: Write down the exact query shapes, sort orders, filters, and expected cardinalities before choosing partition keys or indexes. The storage layout should answer the reads the product actually needs.
  • Primary and secondary indexes: Primary partition and clustering choices determine where related records live and how they are ordered. Secondary indexes add alternate access paths, but they also add storage and write coordination and may have consistency limitations in distributed stores.
  • Read amplification: One logical read can fan out across shards, tables, indexes, or remote calls. Each individual lookup may be fast while the complete request is still expensive.
  • Write amplification: Replication, indexes, log structures, compaction, and derived views cause one logical update to produce multiple physical writes. The extra work affects latency, storage, and recovery cost.
  • Materialized views: Precompute query-shaped projections when read latency or throughput is more important than keeping every projection immediately current. The design must define how stale or eventual updates work and how the view is rebuilt.
  • Hot partitions: A poor partition key concentrates traffic or storage on a small number of nodes. A large dataset does not help if one key receives most of the requests.

Mental model

Treat Indexes, Access Patterns, Query Models, and Read/Write Amplification as a design problem with observable inputs, outputs, invariants, and failure modes. Begin with the reads and writes that matter, then choose a physical model that serves them. Indexes and materialized read models exchange additional write and storage work for faster, more targeted reads. That exchange is not automatically good or bad; it depends on the workload and on the consistency the caller requires.

A strong implementation makes its assumptions visible, narrows uncertainty at system boundaries, and leaves evidence that the design is safe. That evidence might be tests, types, database constraints, metrics, query plans, or diagrams. For example, “timeline reads are limited to one user partition and one page” is a useful invariant. “The database is fast” is not.

A useful interview and production sequence is:

text
requirement -> constraints -> model -> implementation -> failure analysis -> verification

Do not jump from a requirement straight to a library call or an index definition. First state what must remain true: which queries must be supported, what ordering and freshness they need, and what resource limits are acceptable. Then choose the mechanism that enforces those properties and verify it with representative data and traffic.

Deep dive

1. Access-pattern first

Start by listing the exact query shapes, sort orders, filters, and cardinalities before designing partition keys or indexes. “Fetch posts” is not a sufficient query shape. “Fetch the newest 20 posts for one user,” “fetch posts for a hashtag ordered by creation time,” and “fetch one post by ID” place different demands on the model. A schema can represent entities elegantly and still be incomplete if it cannot answer the required queries without an unbounded scan or a broad fan-out.

This approach also makes non-requirements visible. If the service does not need arbitrary filtering or globally sorted results, do not pay for a general-purpose model that promises both. At scale, a deliberately narrow query model is often easier to operate than a flexible one whose worst-case behavior is unclear.

Decision rule: Use access-pattern first deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Record the expected cardinality and page size as part of the contract so that a later “small” query cannot silently become a full-dataset operation.

2. Primary and secondary indexes

Primary partition and clustering choices determine locality and, in many databases, the order in which records can be read within a partition. A good primary key can make the dominant query a targeted lookup. A secondary index supplies another route to the same data, which can be convenient, but it is not free: it consumes storage, adds work to updates, and in distributed systems may not provide the consistency or scale characteristics you expect.

This is where people usually get confused: an index can make a read selective without making the underlying write cheap. Updating one record may update the base row and several index entries, and replicas may need those changes as well. Check the database’s execution plan and consistency model rather than assuming that “indexed” means “constant-time” or “globally available.”

Decision rule: Use primary and secondary indexes deliberately when they make the contract or invariant easier to prove. If an index only hides an access-pattern assumption or permits an unbounded query, prefer the more explicit design. Consider a purpose-built table or materialized projection when the alternate query is important enough to deserve its own locality and failure semantics.

3. Read amplification

A logical read that fans out across shards, tables, indexes, or remote calls can become expensive even when every individual lookup is quick. A request for a user timeline, for instance, may need to read many followed-user partitions, merge their results, and fetch additional author data. The latency includes the slowest branch and the coordination overhead, not just the average lookup time.

Bound the fan-out where possible. Pagination, batching, caching, and a query model with the right locality can reduce the work, but each introduces trade-offs. A cache can return stale data; batching can create larger individual requests; and a materialized feed may move the work to writes. Measure the number of backend operations and bytes read, not only the endpoint’s final latency.

Decision rule: Use read amplification deliberately when the resulting behavior fits the read contract and its upper bound is known. If a logical read can contact an unbounded number of partitions or services, make that risk explicit before optimizing the individual lookups. A design that is acceptable at one hundred followers may fail at a million.

4. Write amplification

Replication, indexes, log structures, compaction, and derived views multiply physical writes per logical update. A single post creation might write the canonical post, several replicas, an author-history projection, and an index used for hashtag queries. Compaction can later rewrite storage again. These costs are easy to miss if capacity planning counts only application-level writes.

Heavy write workloads must budget this hidden work. More indexes can improve read latency while reducing write throughput and increasing storage; synchronous derived updates can provide fresher reads while lengthening the write path; asynchronous updates can protect write latency while exposing temporary gaps. The right choice depends on which side of that trade-off the product can tolerate.

Decision rule: Use write amplification deliberately when the extra physical work buys a required read or consistency property. If it only makes a convenient query faster without a measured need, prefer fewer indexes or an asynchronous projection. Track logical writes separately from physical writes so that the amplification factor is visible during load tests and in production.

5. Materialized views

When read latency or throughput dominates and stale or eventual updates are acceptable, precompute a query-shaped projection. A materialized view is not merely a cache entry: it is a maintained representation of data for a particular access pattern. For a social service, an author-history table or a hashtag-posts table might be populated from post events and read directly by the corresponding endpoint.

The design needs explicit consistency semantics. Decide whether a missing projection is retried, whether the source of truth can repair it, how duplicate events are handled, and what the caller sees while the view catches up. Also define rebuild semantics. If the view is deleted or its schema changes, can it be reconstructed from durable source data, and can the rebuild run without overwhelming the primary store?

Decision rule: Use materialized views deliberately when their read benefit justifies the additional write path and the system can tolerate their freshness and rebuild behavior. If the caller requires a transactionally current answer and the view cannot be updated in that transaction, query the authoritative model or change the consistency contract instead.

6. Hot partitions

Poor partition keys concentrate traffic or storage on a small set of nodes. A key such as a globally popular hashtag, a single tenant ID, or a constant “latest posts” bucket can become a hotspot even when the average traffic across the cluster looks healthy. High-cardinality distribution and write spread matter as much as average dataset size.

Mitigations include bucketing by time, adding a controlled shard suffix, or choosing a key that distributes writes more evenly. These choices make reads more complicated: a time range may span buckets, and a sharded key may require fan-out and merge logic. Do not distribute writes blindly; preserve the query’s ordering and define how many buckets a normal read may touch.

Decision rule: Treat hot partitions as a failure mode to detect and avoid, not as an access pattern to adopt. Choose a partition strategy whose traffic and storage bounds are credible, then verify the distribution with production-shaped keys rather than with uniform test data.

Worked example

Consider a large-scale distributed social post service. Its requirements, traffic, failure modes, cost, and operational constraints need to be written down before choosing a table or index. Start with one sentence such as “Users can create posts and retrieve an individual post, an author’s history, and hashtag results with bounded page reads.” Then list the input and output contracts and identify which concept owns each failure mode.

The important move is separation. Parsing and validation belong at the boundary. Domain rules belong in the domain or service layer. Persistence rules belong in the database or repository. Presentation rules belong in the client. Mixing these concerns can make a happy-path demo look shorter, but it makes retries, malformed values, consistency gaps, and edge cases much harder to reason about.

For the stated reads, a first-pass model might use the post ID for direct lookup, an author-and-time access path for author history, and a hashtag-and-time access path for hashtag results. Before accepting that model, ask how each path partitions data, how many partitions one read can touch, what happens for a very popular hashtag, and whether each projection is authoritative or eventually consistent.

text
Client
  |
DNS -> CDN / Edge
  |
Load Balancer -> API instances -> Cache
                         |          |
                         +------> Primary datastore
                         |
                         +------> Queue / Stream -> Workers

The diagram is a responsibility and request-flow model, not a guarantee that every request uses every component. A cache may satisfy a read before the primary datastore is contacted. A post write may commit to the primary datastore and publish an event for workers to update derived views. That event path introduces a freshness boundary, so the API needs a defined behavior when a view has not caught up.

Walk the example through at least four cases:

  • Normal path: a valid post is stored, its event is processed, and each supported query reads the intended access path.
  • Empty or missing value: an unknown post ID or a hashtag with no matching posts produces the documented empty or not-found result, rather than an accidental full scan.
  • Duplicate, retry, or concurrent path: a retried create or a repeated event does not create unintended duplicate state; concurrent updates follow the declared ordering and idempotency rules.
  • Dependency failure: a cache, primary datastore, queue, or worker is unavailable. State which layer detects the problem, whether the write or read can be retried, and what the caller observes.

For each case, state which layer owns the decision and what evidence would confirm it: an API response, a database constraint, a query plan, a metric, or a worker log. This is the level of explanation expected in a senior code review or technical interview.

Production perspective

Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failure, stale clients, concurrent requests, malformed data, schema changes, and high cardinality. A query that is safe for a local dataset may become a shard-wide scan in production. A view that is fresh during a quiet test may lag under a burst of writes.

Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after you can identify the bottleneck or risk with evidence. Query plans, partition-size distributions, p95 and p99 latency, cache hit rate, queue lag, replica behavior, and logical-to-physical write ratios are more useful than intuition alone.

When the topic involves an external dependency, define a timeout and cancellation strategy. When it involves persistence, define transaction and consistency expectations. When it involves user-visible state, define loading, empty, error, stale, and success states. When it involves security, assume the client can be modified and all network input is untrusted. Never let a client-side filter stand in for authorization or a client-visible cache stand in for a server-side data guarantee.

Guided lab

Design query, index, and partition access patterns for a social post service with these reads: post by ID, user timeline, posts by hashtag, and author history. Identify where the design fans out, where each logical write is amplified, and which keys could become hotspots. For every proposed path, write down its expected cardinality, ordering, page limit, freshness requirement, and recovery behavior.

Complete the lab with this discipline:

  1. Write the requirement and two non-requirements.
  2. List input, output, and error contracts before implementation.
  3. Implement the smallest correct vertical slice.
  4. Add at least one invalid-input test and one edge-case test.
  5. Instrument or inspect the behavior instead of guessing. In a database-backed implementation, inspect the execution plan and the number of partitions or rows touched.
  6. Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
  7. Explain one alternative design and why you did not choose it.
  8. Record a short “what would break at 10× scale?” note. Include at least one read-amplification, write-amplification, or hotspot concern.

Edge cases and failure modes

  • Access-pattern first: test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Confirm that unsupported query shapes fail clearly instead of degrading into an unbounded scan.
  • Primary and secondary indexes: test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Verify that index updates and consistency behavior match the database’s actual guarantees.
  • Read amplification: test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Measure fan-out, bytes read, and the effect of a slow or failed branch.
  • Write amplification: test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Observe the effect of replicas, indexes, compaction, and derived writes on throughput and latency.
  • Materialized views: test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Include delayed, duplicated, out-of-order, and replayed updates, as well as a rebuild from the source of truth.

Common mistakes and debugging

  • Solving the example instead of the requirement: a copied partition or index pattern can be syntactically valid but architecturally wrong for the actual query shapes.
  • Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary” any values. These can turn a malformed key or consistency failure into misleading data.
  • Testing only the happy path and therefore discovering contracts only after integration.
  • Optimizing before measuring, or selecting a scalable mechanism without a scale requirement.
  • Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees.

For debugging, reproduce the smallest failing case and inspect the actual value, partition key, execution plan, or request trace. Trace the boundary where the invariant first becomes false: source or build, browser or DOM, Network or HTTP, server or route, database or query, or deployment or configuration. Then fix the owning layer rather than adding a downstream patch. A slow endpoint may be caused by a broad query shape, a hot partition, queue lag, or cache misses; looking only at application code can hide the real boundary.

Interview questions

  1. What problem does Access-pattern first solve, and what trade-off or failure mode would make you choose a different approach?
  2. What problem do Primary and secondary indexes solve, and what trade-off or failure mode would make you choose a different approach?
  3. What problem does Read amplification describe, and what trade-off or failure mode would make you choose a different approach?
  4. What problem does Write amplification describe, and what trade-off or failure mode would make you choose a different approach?
  5. What problem do Materialized views solve, and what trade-off or failure mode would make you choose a different approach?

Checkpoint

Without notes, explain Indexes, Access Patterns, Query Models, and Read/Write Amplification to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code. Be able to explain which query it serves, how its data is partitioned, what work one read and one write cause, and what the caller sees when a dependency or derived view is unavailable.

Mastery checklist

  • I can define the core terms precisely.
  • I can choose a design from requirements instead of from habit.
  • I can implement and test the normal path and edge cases.
  • I can explain the runtime, storage, or complexity cost.
  • I can identify which layer owns validation, errors, and recovery.
  • I can compare at least two reasonable alternatives.
  • I can explain how the design changes at larger scale or stricter reliability.

References

Reader page: /system-design/lesson/256/indexes-access-patterns-query-models-and-read-write-amplification