System design / Databases

Databases for System Design Interviews

How to choose, model and scale the database in a high-level design interview: database families, storage engines, indexes, ACID, isolation levels, consistency, replication, sharding, caching, sagas and outbox, CDC, OLAP, disaster recovery and a decision framework, with diagrams for every core idea.

On this page
  1. A database is a contract, not a folder for bytes
  2. Database fundamentals interviewers expect
  3. Data modeling: start with correctness, then shape for reads
  4. The database families, compared without ten mini-essays
  5. Indexes: the read speed you pay for on every write
  6. Transactions and ACID: where the safe boundary ends
  7. Concurrency control: locks, versions and the rows in between
  8. Isolation levels and anomalies: the table interviewers expect
  9. Consistency models: what a read is allowed to see
  10. Replication: copies, lag and failover
  11. Partitioning and sharding: splitting data without splitting your brain
  12. Caching and databases: a useful lie with an expiry date
  13. Database scaling: climb the cheap rungs before sharding (members only)
  14. Microservices and database ownership: split data only when you can own the consequences (members only)
  15. Distributed transactions and sagas: choosing between atomic commit and recoverable workflow (members only)
  16. Event-driven data systems: logs, outboxes and replay (members only)
  17. Search, analytics and OLAP: move scans off the request database (members only)
  18. Schema design for HLD: entities, access paths and failure behavior (members only)
  19. Reliability and disaster recovery: RPO, RTO and the restore drill (members only)
  20. Common HLD database patterns: holds, idempotency and leases (members only)
  21. Worked database examples: three designs I would defend (members only)
  22. Interview decision framework: the database flow I use (members only)
  23. Pitfalls and cheat sheets: the fast review before your interview (members only)
  24. Interview questions (members only)

Databases are the decision interviewers probe hardest because every shortcut eventually shows up there: duplicate orders, stale balances, overloaded hot partitions, a search index that lies, or an analytics query that slows checkout. The database choice is not a brand-name exercise. It is where you state the contract your system needs: which data is authoritative, which reads may be stale, which writes must survive crashes, which queries must stay under a few milliseconds, and which failures you are willing to recover from later. In this guide we build that judgment from first principles, then keep layering on indexes, transactions, isolation, replication, sharding, caching, distributed workflows, analytics, recovery and the interview decision framework. The goal is not to memorize ten database families. The goal is to say, calmly, "this is the source of truth, this is the read model, and here is the trade-off I am choosing."

After reading this article you will be able to

  • Explain what a database promises beyond storage: durability, isolation, constraints, query access and failure recovery.
  • Choose between relational, key-value, document, wide-column, graph, time-series, search, object, vector and distributed SQL stores using access patterns instead of labels.
  • Design normalized write models and denormalized read models, and know when duplicated data is a feature rather than a smell.
  • Reason about indexes, storage engines, transactions, ACID, concurrency control, isolation levels and consistency models without hiding behind buzzwords.
  • Plan replication, sharding, caching and scaling paths while naming the cost: lag, hotspots, fan-out, resharding and operational complexity.
  • Handle microservices data boundaries, sagas, outbox tables, CDC, event-driven projections, search indexes and OLAP pipelines without losing the source of truth.
  • Prepare schemas, disaster-recovery plans, high-level design patterns and worked examples that survive realistic interview follow-ups.
  • Use a compact interview framework, common mistakes list and cheat sheets to make a defensible database decision in minutes.

01A database is a contract, not a folder for bytes

Database Definition

A system that stores data behind a contract: what can be written, what can be read, what stays true when many clients act at once, and what can be recovered after a crash.

The easiest analogy is a bank locker plus a clerk. The locker gives you storage, but the clerk gives you rules: only the account owner can withdraw, the ledger balances, two withdrawals cannot spend the same rupee, and a power cut does not erase yesterday's deposit. The rules are the database.

That distinction matters in interviews. If you say "I'll use Postgres" or "I'll use Cassandra" too early, you have skipped the contract. Start by naming what the data needs from the system, then pick the store that gives you those guarantees with the least drama.

Where we start

We will get the mental model, core database fundamentals, data modeling choices and database-family comparison right first. Later sections go deep on indexes, ACID, isolation, consistency, replication, sharding, caching, distributed transactions, CDC, OLAP, disaster recovery and worked design examples.

A good database decision answers five questions. I use these as my checklist when a candidate proposes a store.

QuestionWhat it forces you to decideInterview signal
What data do we store?Entities, schema, ownership, lifecycleYou know what is authoritative.
How do we query it?Indexes, partition keys, sort keys, joins, searchYou design for access patterns, not fashion.
How correct must it be?Transactions, constraints, isolation, consistencyYou can separate money from metrics.
How fast must it be?Cache, denormalization, read replicas, shardingYou can trade freshness for latency intentionally.
What happens when it fails?WAL, replication, backups, replay, idempotencyYou are thinking past the happy path.

The source-of-truth hierarchy

Most real systems have more than one place that looks like a database. The checkout service may write to Postgres, publish events through Kafka, hydrate Redis, feed Elasticsearch and land facts in a warehouse. Only one of those is usually the source of truth for an order.

Here is the rule I want you to say out loud: when derived stores disagree, the source of truth wins. A cache can be invalidated. A search index can be rebuilt. A warehouse can re-run yesterday's load. The source-of-truth database is where the system must not invent or lose business facts.

How to say it in the interview

"For order creation, I need a strongly consistent source-of-truth store because payment, inventory and order state must not diverge. For order history reads and search, I can build derived stores asynchronously from events or CDC, and I will tolerate a few seconds of lag there."

Many candidates make the answer too binary: SQL or NoSQL, consistent or scalable, normalized or denormalized. Strong designs combine stores with clear ownership. The write path can be conservative and transactional while the read path is denormalized and fast.

02Database fundamentals interviewers expect

Before comparing database families, get three foundations straight: OLTP vs OLAP, the write path, and the storage-engine shape underneath your indexes. These are small ideas, but they explain many production incidents.

OLTP vs OLAP

OLTP is product-facing transaction processing: user signup, checkout, message send, booking a ticket, changing a password. Operations are small, targeted and latency-sensitive, usually measured in single-digit or low tens of milliseconds inside the data tier.

OLAP is analytical processing: revenue dashboards, fraud exploration, cohort analysis, product usage reports. Queries scan many rows, aggregate by time or dimension, and can take seconds or minutes because they are not sitting in the user's click path.

DimensionOLTPOLAP
Typical operationPoint lookup, insert, update, deleteScan, join, group, aggregate
Data touchedRows or small rangesMillions to billions of rows
Latency budgetMilliseconds to low hundreds of millisecondsSeconds to minutes can be acceptable
ConcurrencyMany users at onceFewer analysts or batch jobs
Schema styleNormalized or semi-normalizedColumnar, star schema, denormalized
ExamplesPostgreSQL, MySQL, DynamoDB, CassandraBigQuery, Snowflake, Redshift, ClickHouse
Many candidates run analytics on the primary database

A heavy GROUP BY over a live orders table can burn IO, evict useful pages from cache and compete with checkout queries. A better answer is: keep the OLTP database as source of truth, then feed a warehouse through CDC, events or ETL. Minutes of lag are usually fine for dashboards.

What happens on a write

You do not need to implement a storage engine in an HLD round. You do need to know why a database can acknowledge a write before the final data page is neatly rewritten on disk. The trick is the log.

Here is the write path you should picture before we talk about durability, transactions or replication.

Fig. 1

A durable write reaches the log before data pages are flushed

A durable write reaches the log before data pages are flushedA client sends an update. The SQL layer parses and plans it, the transaction manager assigns a transaction, the storage engine appends the change to the write-ahead log and updates dirty pages in the buffer pool. The client is acknowledged after the durable log record; background flush later writes data pages to disk.1 update page in memory2 append + fsync WAL3 ack after WAL is durable4 checkpoint laterClientUPDATE orderSQL layerparse, planTransactionmanagertx idlocks, snapshotStorage engineprepare changesBuffer poolhot page updatedand marked dirtyWrite-ahead logappend record, then fsyncClient gets OKcommit is recoverableData files on diskdirty pages flushed laterCrash recovery replays committed log records and discards incomplete ones
The write-ahead log makes crash recovery practical. The database appends the change to a durable log before dirty pages are flushed later. If the process dies after acknowledging a committed write, recovery replays the log and reconstructs the missing page changes.

The write-ahead log is the database's flight recorder. PostgreSQL calls it WAL, InnoDB has redo logs, Cassandra has a commit log, and Kafka is essentially an append-only log exposed as a product. The names differ, but the idea repeats: append first because sequential writes are cheaper and easier to recover than random page rewrites.

The buffer pool is the other half of the picture. Databases keep hot pages in memory, modify them there, and flush dirty pages in the background. That is why a database can be both durable and fast: the durable record is the log, while the data files catch up later.

Default explanation

When asked how a database survives crashes, say: "A committed write is recorded in the WAL before acknowledgement. Data pages may flush later. On restart, the database replays committed log records and rolls back incomplete transactions." That answer is short, accurate and opens the door to ACID later.

B-tree and LSM storage engines

Most OLTP stores lean toward one of two storage-engine shapes. B-tree style engines keep sorted pages and update them in place, which is excellent for point reads, range scans and stable indexes. PostgreSQL and MySQL are the familiar examples, with implementation differences we will cover in the indexing section.

LSM-tree style engines turn writes into appends. A write lands in a memory table and log, then flushes to immutable sorted files. Later, compaction merges those files. Cassandra, RocksDB, LevelDB and many high-write systems use this pattern.

The trade-off is straightforward: B-trees pay more work at write time to keep reads tidy; LSMs make writes cheap and pay read amplification plus compaction later. If you are designing a feed or metrics system with very high ingest, LSM-shaped storage is attractive. If you need flexible reads, joins and transactions, a relational B-tree-backed database is often the calmer default.

Fig. 2

B-tree and LSM engines optimize different parts of the workload

B-tree and LSM engines optimize different parts of the workloadLeft side shows B-tree pages updated in place and kept sorted for point reads and range scans. Right side shows LSM writes landing in a memtable and log, flushing to immutable SSTables, then compacting in the background.B-tree styleLSM-tree styleflushcompactionUpdate sorted pagesrow changes land in existing tree pagesroot pagebranch pagesleaf pages with keys / row pointersMemtablein memoryCommit logdurable appendSSTable level 0immutable sorted fileSSTable level 1SSTable level 2Great for point reads, range scans and flexible indexesGreat for high write throughput, with read amplification and compaction cost
B-tree engines organize pages for reads; LSM engines organize writes for append speed. Neither is universally better. The storage shape explains why some databases prefer range queries and transactions while others prefer high write throughput and predictable partition-key reads.

03Data modeling: start with correctness, then shape for reads

Data modeling is where candidates often reveal whether they are designing a system or reciting products. The safe default is to model the write side for correctness, then build read models for the hot paths. That sounds slower at first, but it prevents a lot of expensive cleanup later.

Normalized write models

Normalization stores each fact in one place. In an e-commerce system, users, products, categories, orders, order items and payments should not all be one giant row. Splitting them lets the database enforce foreign keys, uniqueness, not-null constraints and transactional updates.

users(id, name, email)
products(id, name, current_price, category_id)
categories(id, name)
orders(id, user_id, status, created_at)
order_items(id, order_id, product_id, quantity, price_snapshot)
payments(id, order_id, amount, status, charged_at)

Notice price_snapshot. It is denormalized on purpose inside an otherwise normalized model, because an order must remember the price at purchase time, not today's catalog price. Good modeling is not dogma. It is deciding which facts should change together and which facts are historical snapshots.

Denormalized read models

Denormalization duplicates data to make a read cheap. An order-history card might store the user name, product names, thumbnail URLs and final price in one document or wide-column row so the mobile app does not join six tables for every screen.

{
  "order_id": "o123",
  "user_id": "u1",
  "user_name": "Asha Rao",
  "items": [
    { "product_id": "p7", "name": "Mechanical keyboard", "price_at_purchase": 4500 }
  ],
  "status": "DELIVERED",
  "created_at": "2026-01-01T10:00:00Z"
}

The cost is staleness and repair. If the user changes her display name, should old orders change? Sometimes no, because invoices need snapshot semantics. If the product thumbnail changes, maybe yes, but a few seconds of lag is acceptable. Name the business rule and the sync mechanism.

This is the modeling split I usually sketch on the board.

Fig. 3

Write model protects invariants; read models serve access patterns

Write model protects invariants; read models serve access patternsNormalized source-of-truth tables for users, orders, order items and payments emit events or CDC. Derived stores include a Redis cache, an order history document, an Elasticsearch index and an OLAP warehouse. Derived stores can lag and be rebuilt.Source of truthordersid, user_id, status, created_atorder_itemsorder_id, product_id, price_snapshotpaymentsorder_id, amount, statususers / productsconstraints and foreign keysEvents / CDCcommitted changesRedis cachehot product cardsOrder history docone screen readSearch indexfull-text + facetsWarehouseanalytics scansTransactional write pathDerived stores may lag; rebuild them from the source
Normalize the write path for correctness; denormalize read paths for speed. The source-of-truth tables protect invariants. Events or CDC hydrate read models, search indexes, caches and warehouses that are allowed to lag and can be rebuilt.
Interview move

Say: "I would keep the order creation path normalized and transactional. For order history, search and analytics, I would project separate read models from events or CDC." That single sentence tells the interviewer you understand source of truth, denormalization and operational recovery.

Query-driven modeling for NoSQL

Relational modeling starts with entities. NoSQL modeling starts with queries. If the hot query is "get the latest 50 messages in conversation C", a table keyed by conversation_id and sorted by created_at makes sense. If the query is "search messages by text", that same table is the wrong tool and a search index belongs in the design.

-- Cassandra-style table for one hot query
CREATE TABLE messages_by_conversation (
  conversation_id UUID,
  created_at      TIMESTAMP,
  message_id      UUID,
  sender_id       UUID,
  body            TEXT,
  PRIMARY KEY ((conversation_id), created_at, message_id)
) WITH CLUSTERING ORDER BY (created_at DESC);

The trap is treating Cassandra or DynamoDB like a relational database without joins. Filtering on arbitrary non-key columns turns into scans, secondary-index pain or a pile of global indexes you did not budget for. With NoSQL, first list the access patterns. Then create the key shapes.

Relationship patterns and unbounded growth

One-to-many is usually a foreign key or a partition key: orders by user, messages by conversation, payments by order. Many-to-many usually needs a join table in relational systems or two query-specific tables in NoSQL systems. The pattern is not hard. The growth boundary is where candidates slip.

Do not put an unbounded array inside one record because it looks convenient on day one.

// Bad: this can grow to millions of entries
{
  "user_id": "celebrity",
  "followers": ["u1", "u2", "u3"]
}
-- Better: each relationship is addressable and pageable
followers(follower_id, followee_id, created_at)
INDEX (followee_id, created_at)
INDEX (follower_id, created_at)
Many candidates hide scale inside a document

Embedding addresses inside a user profile is fine because the array is bounded and read with the parent. Embedding 50,000 comments inside a post is not fine: the document becomes huge, pagination is awkward, writes contend on one item and some stores have hard size limits, such as MongoDB's 16 MB document limit.

04The database families, compared without ten mini-essays

You will rarely get points for listing database categories. You get points for connecting the access pattern to the family, and for naming when that family is a bad fit. Use this section as a decision map, not a catalogue.

Here is the fast path I would use in an interview before diving into the individual families.

Fig. 4

Choosing a database family from the dominant access pattern

Choosing a database family from the dominant access patternA decision map. Start with the need for source-of-truth transactions and flexible queries. Choose relational by default, distributed SQL for global SQL, key-value for key lookups, document for aggregate JSON, wide-column for high write scale, graph for traversals, time-series for time-window ingest, search for full text, object storage for blobs, and vector for similarity.yesglobal?no or derived readkey lookupJSON aggregateNeed source-of-truthtransactions and flexible SQL?Relationaldefault source of truthDistributed SQLSQL + consensusIs the hot path mostlyone key or one aggregate?Key-valuecache, session, counterDocumentbounded aggregateWhich specializedaccess pattern dominates?Wide-columnpartition + timeGraphmulti-hop edgesTime-seriestime windowsSearchinverted indexObject storagelarge blobsVectornearest neighborIf two answers fit, use one as source of truth and the other as a derived read model
Pick the family by the dominant access pattern and correctness need. Start with relational when you need transactions and flexible queries. Move away only when a specific pressure dominates: key lookup latency, document locality, high write scale, graph traversal, time-window ingest, full-text search, blob storage, vector similarity or global SQL.

Relational / SQL databases

Relational databases store typed rows in tables and let you declare queries in SQL. The optimizer chooses access paths through indexes, joins and scans. PostgreSQL, MySQL, SQL Server and Oracle belong here.

Pick relational when correctness and query flexibility matter: payments, orders, bookings, inventory, accounts, admin workflows and anything with multi-row invariants. You get constraints, transactions, joins, mature indexes, migrations and a deep ecosystem.

Do not pick it as your only answer for very high write ingest, globally distributed low-latency writes, full-text relevance, blob storage or graph traversals with many hops. You can stretch relational databases far, especially with read replicas and partitioning, but name the point where a specialized read model or store enters the design.

Key-value stores

A key-value store is a dictionary: GET key, SET key value, DEL key. Redis, Memcached, Aerospike and DynamoDB in simple key-value usage fit this mental model, although several of them offer richer structures.

Pick key-value for sessions, cache-aside reads, rate-limit counters, feature flags, idempotency records, leaderboards and predictable primary-key lookups. Redis is often memory-first and excellent for sub-millisecond cache patterns. DynamoDB is managed, partitioned and durable, with partition key plus optional sort key access.

Do not pick it when you need ad hoc queries, joins or cross-item transactions as the main path. If your design needs "find all orders where status is pending and amount greater than 5,000", you either need planned indexes and access patterns or a different store.

A note on DynamoDB and single-table design

DynamoDB deserves a separate mention because it often appears in interviews as both a key-value and document-like store. The physical idea is still access-pattern driven: every item has a partition key, and many designs add a sort key so related items can live together and be scanned in order.

PK = USER#u1,  SK = ORDER#2026-05-01#o1
PK = USER#u1,  SK = ORDER#2026-05-02#o2
PK = ORDER#o1, SK = ITEM#p7
PK = ORDER#o1, SK = ITEM#p9

This is the single-table design idea: different entity types share one table, and the key prefixes encode the queries you already know. "Get a user's orders" reads PK = USER#u1 with a sort-key prefix. "Get order items" reads PK = ORDER#o1. A global secondary index handles a different access pattern, such as orders by status and date.

The trade-off is planning. DynamoDB is excellent when your access patterns are stable, traffic is spiky, and you want managed partitioning. It is painful when the product team keeps asking new ad hoc questions. In that case, pair it with a stream-fed search index or warehouse instead of pretending the base table can answer everything.

Document databases

Document databases store JSON-like documents, often BSON or JSON, grouped into collections. MongoDB, Couchbase, Firestore and DocumentDB are common examples. The modeling choice is embed versus reference.

Pick document when a record is naturally aggregate-shaped and usually read as one unit: user profiles with bounded addresses, product catalog entries, CMS pages, device configurations or event metadata. Documents are pleasant when the schema evolves and not every record has the same fields.

Do not pick it for unbounded children, heavy cross-document transactions or highly relational reporting. MongoDB supports multi-document ACID transactions since 4.0, but if every write path needs them, you may be fighting the model. Keep most operations single-document when possible.

Wide-column / column-family databases

Wide-column stores such as Cassandra, ScyllaDB, HBase and Bigtable are best understood as distributed sorted maps. A partition key chooses the nodes. Clustering columns sort rows inside that partition. This is not the same thing as columnar OLAP storage.

Pick wide-column for high write throughput and query patterns that are known in advance: messages by conversation, events by device and time, activity feeds, IoT data, and large append-heavy datasets spread across many partitions.

Do not pick it when you need joins, arbitrary filters, strong multi-row transactions or global secondary-query flexibility. Hot partitions are the classic failure. A low-cardinality key like status or one celebrity user's feed can concentrate traffic on a small set of nodes.

The partition and clustering-key layout is the most important picture for this family.

Fig. 5

Wide-column layout: partition key chooses placement, clustering key orders rows

Wide-column layout: partition key chooses placement, clustering key orders rowsA messages_by_conversation table uses conversation_id as the partition key and created_at plus message_id as clustering columns. All rows for conversation c123 are grouped together and sorted newest first. A query by sender_id is not efficient without another table or index.One partition on the owning replicashash partition keyToken ringreplicas own rangesReplica setRF = 3 examplepartition keyconversation_id = c1232026-05-01 10:05 | m9sender u1 | Hey2026-05-01 10:04 | m8sender u2 | Hi2026-05-01 10:03 | m7sender u1 | YoWHERE sender_id = u1needs another table or scanTable: messages_by_conversationPRIMARY KEY ((conversation_id), created_at, message_id)CLUSTERING ORDER BY (created_at DESC)Query: WHERE conversation_id = ? LIMIT 50Efficient: latest messages in c123 are contiguous and already sorted
A wide-column table is designed around one query shape. The partition key groups related rows on the owning replicas. Clustering columns keep the rows ordered inside the partition, so "latest 50 messages for conversation C" is efficient while "messages by sender" needs another table or index.

Graph databases

Graph databases store nodes and edges as first-class data. Neo4j, Neptune, JanusGraph and TigerGraph are typical names. Their strength is relationship traversal, not replacing every relational model.

Pick graph for variable-depth relationships: fraud rings, social recommendations, identity graphs, network topology, permission hierarchies and knowledge graphs. A query like "friends of friends who bought this item" can be a natural traversal instead of a growing chain of self-joins.

Do not pick it for ordinary CRUD with a few joins. Graph stores add operational and modeling complexity. If your relationship depth is one or two and the query set is predictable, relational tables with indexes are often enough.

Time-series databases

Time-series databases make timestamped data cheap to ingest, compress, retain and aggregate. InfluxDB, TimescaleDB, Prometheus, QuestDB and Timestream live in this area.

Pick time-series for metrics, traces, sensor readings, monitoring, financial ticks and any workload that writes mostly append-only points and queries by time window plus labels. Features like chunking, compression, downsampling and retention policies are the product.

Do not pick it as the system of record for mutable business entities. A metric point is not an order. Also be careful with high-cardinality labels, such as user id as a metric label for every event, because index cardinality can explode.

Search databases

Search databases build inverted indexes: terms map to documents, then queries intersect terms and rank results. Elasticsearch, OpenSearch, Solr, Typesense and Meilisearch are common choices.

Pick search for full-text search, relevance ranking, autocomplete, faceted catalog filters, log exploration and geospatial-style search features. Elasticsearch also stores doc values for sorting and aggregations, which is why it shows up in logging stacks.

Do not pick it as the authoritative store for orders, payments or inventory. It is usually a derived store fed from a primary database through events or CDC. Elasticsearch has near-real-time refresh behavior, often around one second by default, so newly written documents may not be searchable immediately.

Object storage

Object storage is a flat namespace of blobs addressed by bucket and key. Amazon S3, Google Cloud Storage, Azure Blob Storage and MinIO are examples. It is not a relational database, but it is a database-adjacent building block in most large designs.

Pick object storage for images, videos, PDFs, backups, logs, data-lake files and static assets. S3 is designed for extreme durability, commonly described as 11 nines for object durability, and storage cost is far lower than SSD-backed OLTP databases.

Do not pick it when you need transactions, fine-grained updates or low-latency structured queries. Make blobs immutable where you can, store metadata in a database, and serve public objects through a CDN.

Vector databases

Vector databases store embeddings and answer nearest-neighbor queries: "which items are close to this vector?" Pinecone, Weaviate, Milvus, Qdrant, pgvector and FAISS-based systems live here.

Pick vector search for semantic search, recommendations, image similarity, anomaly detection and RAG retrieval. The key idea is approximate nearest-neighbor search, often with indexes such as HNSW, trading a little recall for much lower latency.

Do not pick it as your primary business database. Vectors answer similarity, not truth. You still need metadata filters, authorization, deletion semantics and a source-of-truth record to join back to.

Distributed SQL / NewSQL

Distributed SQL systems try to keep SQL and ACID transactions while distributing data across nodes or regions. Google Spanner, CockroachDB, YugabyteDB and TiDB are examples, each with different operational profiles.

Pick distributed SQL when you need relational semantics at horizontal scale, especially multi-region strong consistency or PostgreSQL-like querying after a single-node relational database is no longer enough. It is attractive for financial, inventory and global SaaS workloads that cannot accept eventually consistent writes.

Do not pick it to avoid thinking about latency. Cross-region consensus is bounded by physics. If a write must be agreed across continents, budget tens to hundreds of milliseconds depending on placement. For many systems, a regional relational primary plus replicas is simpler and faster.

FamilyBest atWeak atDefault interview use
RelationalTransactions, constraints, joins, flexible queriesMassive write ingest, global low-latency writesSource of truth for core business data
Key-valueKey lookups, caching, counters, sessionsAd hoc queries and relationshipsCache, rate limiter, idempotency store
DocumentAggregate-shaped JSON recordsUnbounded children, relational reportingProfiles, catalog docs, CMS content
Wide-columnHigh write scale by partition keyJoins, arbitrary filters, hot partitionsMessages/events by key and time
GraphMulti-hop relationship traversalOrdinary CRUD and simple joinsFraud, recommendations, permissions graph
Time-seriesTimestamped ingest, rollups, retentionMutable business entitiesMetrics, monitoring, IoT readings
SearchFull-text, relevance, facets, logsAuthoritative transactionsDerived search index from source DB
Object storageCheap durable blobs and data lake filesStructured queries, updates, transactionsMedia, backups, raw analytics files
VectorSimilarity over embeddingsTruth, authorization, transactional stateSemantic retrieval plus metadata DB
Distributed SQLSQL + ACID across nodes or regionsOperational complexity, consensus latencyGlobal relational workloads after simpler options fail
Recommended default

For most interview systems, start with a relational database as the source of truth. Add Redis for caching when latency or load requires it, add search for full-text discovery, add object storage for blobs, and add a warehouse for analytics. Move the source of truth to a NoSQL or distributed SQL store only when a named access pattern or scale constraint demands it.

That default is not boring. It is disciplined. It lets you spend your interview time on the actual hard parts: which invariants need transactions, which reads may be stale, how replicas lag, how shards are chosen, and how derived data is rebuilt when it drifts.

05Indexes: the read speed you pay for on every write

Index Definition

A separate lookup structure the database maintains so it can find rows without scanning the whole table. Think of it like the index at the back of a textbook: extra pages to maintain, much faster when you know the term you want.

An index is not magic storage dust. It is a space and write-cost trade-off for read speed. Without an index on orders.user_id, the database may read every table page to answer WHERE user_id = ?. With a useful index, it walks the B-tree we met earlier, lands near the first matching key, then follows leaf pages for the matching range.

The part candidates often miss is the second half of the lookup. The index usually stores keys plus row pointers, so after finding matching keys the database still fetches table rows unless the index contains every column the query needs. That extra heap lookup is why a query can "use an index" and still be slower than expected.

Here is the picture I keep in my head for a normal B-tree index lookup.

Fig. 6

B-tree index lookup and the table fetch

B-tree index lookup and the table fetchA query on orders by user id walks from root page to internal page to linked leaf pages. If the selected columns are covered by the index it returns immediately; otherwise row pointers fetch table rows from the heap or clustered table.B-tree index on (user_id, created_at)Table rowsrange scan leavesrow pointersroot pageu50 | u100internalu1..u49internalu50..u99internalu100+u1, Janrow 8u1, Febrow 17u1, Marrow 24u2, Janrow 31orders row 1status, total, address...orders row 2status, total, address...orders row 3status, total, address...Covering indexIf SELECT columns are in the index, skip table fetches.Write costEvery insert or update must maintain this structure too.tree pointers
An index lookup is a tree walk plus optional table fetches. The database walks internal pages, scans linked leaf pages for the range, then either returns from a covering index or follows row pointers back to the table.

Composite indexes: leftmost prefix is the rule to say out loud

A composite index sorts by the first column, then by the second inside each first-column value, then by the third. It behaves like a phone book sorted by surname, first name and city. You can quickly find all Patels, then Patels named Asha, but you cannot jump directly to everyone in Pune without scanning across every surname.

IndexQuery shapeHow well it helps
(user_id, status, created_at)WHERE user_id = ?Good: uses the leftmost prefix.
(user_id, status, created_at)WHERE user_id = ? AND status = ?Better: uses two leading columns.
(user_id, status, created_at)WHERE user_id = ? AND status = ? AND created_at > ?Best: seek to the user and status, then range-scan time.
(user_id, status, created_at)WHERE status = ?Poor: skips the leading column, so the order is not useful.
(user_id, status, created_at)WHERE user_id = ? ORDER BY created_atPartial: status is between them, so ordering may not be usable unless status is fixed.

The interview move is to start from query patterns, not from columns. For an orders page, I might pick (user_id, created_at DESC, id DESC) because the hot query is "show this user's latest orders" and it also supports keyset pagination. If the hot query is "pending orders for this user", then (user_id, status, created_at DESC) is a better fit.

Covering, partial and expression indexes

A covering index contains all the columns needed by a query, so the engine can answer from the index without visiting the table. In PostgreSQL you may use INCLUDE columns for this; in MySQL InnoDB, secondary indexes already include the primary key, but other selected columns still need to be part of the index if you want a covering read.

-- Latest orders page: filter, sort and return from the index when possible.
CREATE INDEX idx_orders_user_feed
ON orders (user_id, created_at DESC, id DESC)
INCLUDE (status, total_amount);

A partial index stores only rows that match a predicate. This is excellent when the hot path is a small subset, like pending orders, active sessions or unprocessed jobs. A functional index stores an expression, for example LOWER(email), so a case-insensitive lookup does not scan every user.

CREATE INDEX idx_pending_orders
ON orders (user_id, created_at DESC)
WHERE status = 'PENDING';

CREATE INDEX idx_users_email_lower
ON users (LOWER(email));
Many candidates index every column

That sounds safe until the table becomes write-heavy. Every insert, update or delete now updates the table plus each index, and each index consumes cache space. My default is to index the top query patterns, usually 3 to 5 per hot table, then use EXPLAIN and production query stats before adding more.

Selectivity matters. An index on a boolean or a three-value status column often loses to a table scan if it matches 30% or 40% of rows, because the engine would bounce through many random row fetches. That same low-cardinality column can be useful as the second or third column after a selective prefix like user_id or tenant_id.

Use EXPLAIN, but read it like a detective

When you add an index, do not stop at "the query got faster on my laptop". Look at the plan. You are checking whether the engine estimates the right number of rows, whether it uses an index seek or a scan, and whether it sorts in memory because your index order does not match the ORDER BY.

Plan clueWhat it usually meansWhat I would try next
Estimated rows far from actual rowsStatistics are stale or the predicate is correlated in a way the planner missed.Refresh stats, add extended stats where supported, or rewrite the predicate.
Index scan plus many table fetchesThe index finds keys, but each row lookup is still expensive.Consider a covering index, but only for a hot query.
Explicit sort after an index scanThe index order does not satisfy the requested order.Move equality columns first, then range and sort columns in the needed direction.
Sequential scan on a small tableThe planner may be right. A scan can beat random index reads.Do nothing unless the table is growing or the scan is on the hot path.

This is also why cardinality estimates matter more than index count. If the planner thinks a predicate returns 10 rows but it returns 1 million, it may pick a nested-loop plan that explodes. In a design interview you do not need to recite planner internals, but saying "I would verify with EXPLAIN ANALYZE against realistic data" signals production judgment.

OFFSET pagination and N+1 queries are index problems too

OFFSET 100000 LIMIT 20 still asks the database to walk and discard 100,000 ordered entries before returning 20. Keyset pagination uses the last seen key as a cursor, so the engine seeks into the index and reads the next page. The index must match the ordering, for example (created_at DESC, id DESC).

The N+1 query problem is the other common performance leak. One query loads 100 orders, then the code runs 100 more queries to load users or payments. Batch with WHERE id IN (...), join where appropriate, or use a DataLoader-style batcher in GraphQL. An index helps each lookup, but avoiding 100 round trips helps more.

How I say indexing in an interview

"I would design indexes from the read paths: equality columns first, then range or ordering columns, and I would check selectivity. For the orders feed, I want a composite index on (user_id, created_at DESC, id DESC). I would avoid indexing every low-cardinality field because every extra index slows writes and competes for memory."

06Transactions and ACID: where the safe boundary ends

Transaction Definition

A group of database operations treated as one logical unit. Either the whole unit commits, or the database behaves as if none of it happened.

A money transfer is the classic example because it is small and unforgiving. Debit account A, credit account B, then commit. If the process crashes halfway through, a transaction keeps money from disappearing or doubling. The database gives you a safe box around those changes.

BEGIN;
  UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
  UPDATE accounts SET balance = balance + 100 WHERE id = 'B';
COMMIT;

ACID is the set of promises around that box. Atomicity means all or nothing. Consistency means constraints and valid states are preserved, although business rules still need application logic. Isolation means concurrent transactions should not step on each other in ways your chosen isolation level forbids. Durability means committed data survives crashes.

PropertyWhat it protectsWhat to mention in design rounds
AtomicityHalf-finished writesA reservation row and inventory decrement commit together or roll back together.
ConsistencyInvalid statesConstraints handle foreign keys and checks; app code handles workflow rules like paid before shipped.
IsolationConcurrent interferenceThis is a dial, not one setting. We choose it based on anomalies we can tolerate.
DurabilityCrash loss after commitWAL plus fsync is the usual local mechanism; synchronous replicas can raise the bar.

Durability is where a practical detail matters. Databases write changes to a write-ahead log before acknowledging commit, then flush that log to durable storage depending on configuration. If you disable or relax fsync for throughput, you may acknowledge commits that vanish on a crash. That can be fine for clickstream ingestion. It is not fine for wallet balances.

The ACID boundary does not cross service databases by itself

In a monolith with one database, creating an order, reserving inventory and recording payment may fit in one transaction. In a microservice design with three databases, you have three local transactions. Now you need a saga, outbox, idempotency and compensation, not a single magic distributed transaction on the hot path.

The recommended default is boring: keep strongly coupled invariants inside one database transaction for as long as your architecture allows it. Split databases when ownership, scaling or compliance forces the split. Then be honest that you moved from ACID to workflow coordination.

07Concurrency control: locks, versions and the rows in between

Concurrency control Definition

The set of techniques a database uses so many clients can read and write the same data without corrupting it. It is traffic control for rows: sometimes you wait at a red light, sometimes you drive on a separate lane, sometimes you retry after a near miss.

Here is the problem in one line: two users read available = 1, both decide they can buy, and both write back a valid-looking result. The database needs help deciding whether the second write should wait, fail, retry or be allowed.

Pessimistic locking: block first, avoid retry later

Pessimistic locking assumes conflict is likely. You lock the row before making the decision, usually with SELECT ... FOR UPDATE. Other transactions that want the same row wait until you commit or roll back.

BEGIN;
SELECT available
FROM inventory
WHERE product_id = 42
FOR UPDATE;

UPDATE inventory
SET available = available - 1
WHERE product_id = 42 AND available > 0;
COMMIT;

This is a good fit for high-contention rows like seats, inventory and account balances, where a failed retry is expensive or confusing. It is a bad fit for long user think-time flows, because locks held for seconds can pile up blocked transactions behind them.

Deadlocks are normal; unhandled deadlocks are bugs

Locks introduce another failure mode: deadlock. Transaction A locks row 1 then waits for row 2, while transaction B locks row 2 then waits for row 1. Databases detect this cycle and abort one transaction so the other can continue.

The fix is partly design and partly discipline. Touch rows in a consistent order, keep transactions short, avoid waiting on network calls while holding locks, and retry transactions that fail with a deadlock or serialization error. A retry loop is not a bandage here; it is part of the contract when you choose locking or serializable isolation.

Optimistic concurrency: proceed, then detect conflict

Optimistic control assumes conflicts are rare. You read a version, do the work, and update only if the version is unchanged. If the update affects zero rows, somebody else got there first and you retry or show a conflict.

UPDATE inventory
SET available = available - 1,
    version = version + 1
WHERE product_id = 42
  AND version = 7
  AND available > 0;

This is the right default for profile edits, drafts and low-contention business objects. It keeps latency low when conflicts are rare, but under high contention it can turn into a retry storm. In interviews, say the trade-off: locks cost waiting, optimistic control costs retries.

MVCC: readers get old versions instead of blocking writers

Modern relational databases usually use MVCC, multi-version concurrency control. Instead of overwriting a row in place from every reader's perspective, the database keeps row versions with visibility metadata. A transaction reads the version visible to its snapshot, while a writer can create a newer version.

This is why ordinary reads in PostgreSQL and InnoDB do not block ordinary writes, and ordinary writes do not block ordinary snapshot reads. The price is cleanup. Old versions must stay around while transactions can still see them, so long-running transactions can cause bloat and slow vacuum or purge work.

Fig. 7

MVCC row versions and snapshot visibility

MVCC row versions and snapshot visibilityA transaction T1 starts before an update and keeps reading the old row version. T2 updates the row and commits a new version. New transactions see the new version. Cleanup waits until old snapshots finish.Version chain for users.id = 1T1 sees oldT3 sees newT1 startssnapshot time = 10UPDATE users SET name="Abhi"creates a new committed version at t=12T3 startssnapshot time = 13name="Abhishek"created t=5, expired t=12name="Abhi"created t=12, currentTimet=10t=12t=13laterCleanup waitsOld versions stay while activesnapshots can still see them.
MVCC lets readers and writers pass each other. Older transactions keep seeing the older row version, while new transactions see the committed replacement. Cleanup can remove old versions only after no active snapshot needs them.
Long transactions are a production smell

A report that leaves a transaction open for an hour can pin old row versions for that whole hour. In PostgreSQL that means vacuum cannot remove them yet; in InnoDB the undo history grows. Monitor transaction age, not only query latency.

08Isolation levels and anomalies: the table interviewers expect

Isolation level Definition

A setting that chooses which concurrency anomalies the database must prevent for a transaction. It is like choosing how strict the traffic rules are: stricter rules prevent more crashes, but they add waiting, aborts or coordination.

Start with the anomalies. A dirty read reads data that later rolls back. A non-repeatable read reads the same row twice and sees different committed values. A phantom reruns a predicate and sees new matching rows. A lost update overwrites another committed update. Write skew lets two transactions update different rows after reading the same predicate, breaking a cross-row invariant.

The clean version of the table is below. Treat it as the SQL-standard mental model, then add the engine notes under it because PostgreSQL and MySQL InnoDB do not line up perfectly with the simple labels.

AnomalyREAD UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
Dirty readPossiblePreventedPreventedPrevented
Non-repeatable readPossiblePossiblePreventedPrevented
Phantom readPossiblePossiblePossible in the SQL model; engine-specific in practicePrevented
Lost updatePossiblePossible for read-compute-write unless you use atomic updates, locks or versionsOften detected or blocked for the same row, but do not rely on the label alonePrevented by aborting, blocking or serial ordering
Write skewPossiblePossiblePossible under snapshot isolationPrevented

Now the database-specific part. PostgreSQL defaults to READ COMMITTED. Its REPEATABLE READ is snapshot isolation, so a transaction gets a stable snapshot and PostgreSQL prevents dirty reads, non-repeatable reads and phantoms for ordinary reads, but write skew can still happen. PostgreSQL SERIALIZABLE uses SSI, serializable snapshot isolation, and may abort one transaction with a serialization failure. Your application must retry.

MySQL InnoDB defaults to REPEATABLE READ. InnoDB uses consistent snapshots for plain reads, and next-key locks for many locking reads and writes, which prevents many phantom patterns in practice. It is still not the same as SERIALIZABLE for every invariant. If you are protecting "at least one doctor on call" or "total reserved seats must not exceed capacity", name the invariant and choose explicit locks, constraints or SERIALIZABLE.

Write skew is the anomaly that separates memorising from understanding, so here is the timeline interviewers like.

Fig. 8

Write skew under snapshot isolation

Write skew under snapshot isolationTwo doctors each read that another doctor is on call. They update different rows to go off call, so no direct write conflict occurs. Both commit and the invariant that at least one doctor is on call is broken.1SELECT on_call doctors2sees Dr B on call3SELECT on_call doctors4sees Dr A on call5UPDATE Dr A: off call6UPDATE Dr B: off call7COMMIT8COMMITTx ADr ADatabaseTx BDr BInvariant broken0 doctors on call, no single row conflict
Write skew breaks an invariant without two transactions updating the same row. Each doctor sees another doctor on call, each safely updates a different row, and the final state violates the rule that at least one doctor must remain on call.
How I choose isolation in a design round

"I would use the database default for ordinary product reads and writes, usually READ COMMITTED in PostgreSQL or REPEATABLE READ in InnoDB. For a hot row like inventory, I would use atomic updates or FOR UPDATE. For cross-row invariants where write skew matters, I would use SERIALIZABLE or redesign the invariant into a locked aggregate row or constraint."

09Consistency models: what a read is allowed to see

Consistency model Definition

A contract for what clients may observe after writes in a distributed system. If isolation is about transactions inside a database, consistency is about reads and writes across replicas, regions and clients.

Strong consistency means that once a write succeeds, later reads see it, no matter which replica serves the read. The cost is coordination: writes may wait for a leader, quorum or consensus, and partitions may force the system to reject work rather than lie.

Eventual consistency means replicas may disagree for a while, then converge if no new writes arrive. This is acceptable for like counts, feed rankings, recommendations and search indexes. It is not acceptable for "did my payment succeed?" unless the UI clearly models pending state.

Between those poles are user-facing guarantees that are often enough.

GuaranteeWhat it meansCommon implementation
Read-your-writesAfter I update my profile, I see my new profile on refresh.Route my reads to the leader for a short window, or carry a version token to a caught-up replica.
Monotonic readsOnce I have seen version 8, I do not later see version 6 in the same session.Session stickiness or minimum-version routing.
Causal consistencyIf reply B depends on post A, nobody sees B before A.Dependency metadata, session vectors or routing related writes through the same partition.

CAP is useful only if you say it carefully. During a network partition, a distributed system chooses between returning available but possibly stale answers, or preserving consistency by rejecting or delaying some operations. Partition tolerance is not a feature you opt into; networks fail.

PACELC is the more useful interview framing

CAP talks about partitions. PACELC adds the normal case: else, when there is no partition, do you choose lower latency or stronger consistency? For payments I pay the coordination cost. For feed counters I usually choose lower latency and convergence.

10Replication: copies, lag and failover

Replication Definition

Keeping copies of the same data on multiple nodes. It buys availability, durability and read scale, but it introduces lag and failover decisions.

In leader-follower replication, one leader accepts writes and followers replay its log. PostgreSQL streams WAL; MySQL streams the binlog or uses group replication, depending on setup. Reads may go to followers, but writes normally go to the leader.

The first trade-off is synchronous versus asynchronous replication. Synchronous replication waits for at least one replica acknowledgement before reporting success, which reduces data loss risk but adds latency and can reduce availability if the replica is slow. Asynchronous replication acknowledges after the leader commits locally, then ships changes in the background. It is lower latency, but a leader crash can lose acknowledged writes that had not reached a replica.

Fig. 9

Leader-follower replication and read-your-writes

Leader-follower replication and read-your-writesThe client writes a new email to the leader. The leader acknowledges before the asynchronous replica has replayed the change. A read routed to that replica sees the old email until the replication stream catches up.write email=new200 OKsync or caught upasync stream delayednext read routed hereClientupdates emailLeaderaccepts writesemail = newFollower Acaught upFollower Blagging: old emailRead-your-writes fixleader reads for 30sor session version tokenor avoid lagging replicasAsync trade-offlower write latencypossible data loss on failoverstale follower readsSync trade-offstronger durabilityhigher latencyreplica health affects writes
Replication lag is where users notice your topology. A write succeeds on the leader, but an immediate read from a lagging follower can return the old value unless you route around the lag or use a version token.

Read-your-writes is the practical problem. After a user changes their email, their next page load must not show the old email because it hit a lagging follower. Good options are: route that user's reads to the leader for 30 seconds after a write, attach a version token to the session and choose a replica that has caught up, or fall back to the leader when lag crosses a threshold.

Failover is the second problem. If the leader dies, a follower must be promoted. Manual failover can take minutes and is common for planned maintenance. Automated failover can take tens of seconds in managed systems, but it must prevent split brain, where two nodes both think they are leader and accept divergent writes.

Split brain is worse than downtime

If two leaders accept writes during a partition, you may have no correct automatic merge. Use quorum-based leader election, fencing tokens or cloud-managed failover that ensures the old leader cannot keep writing. Availability is valuable, but accepting conflicting money or inventory writes is usually worse.

Multi-primary replication: low local latency, hard conflicts

Multi-primary, sometimes called active-active, lets more than one region accept writes. The upside is local write latency and regional availability. The catch is conflict resolution: if Mumbai and Singapore both update the same account, the system needs a rule, and "last write wins" can silently lose valid work.

I only like multi-primary for data with natural merge rules, such as shopping cart additions, user preferences with field-level merges, or append-only events. For balances, inventory and uniqueness constraints, a single write leader or consensus-backed path is usually the safer default. If the interviewer asks for multi-region writes, name the invariants that cannot tolerate conflict.

Quorums: N, R and W in one minute

Some replicated databases let you tune how many replicas must participate. With N replicas, W write acknowledgements and R read responses, the rule of thumb is R + W > N if you want reads and writes to overlap on at least one up-to-date replica. For N = 3, W = 2 and R = 2 is a common quorum shape.

Fig. 10

Quorum reads and writes with N=3, W=2, R=2

Quorum reads and writes with N=3, W=2, R=2A write is acknowledged by two of three replicas. A later read asks two replicas. Because R plus W is greater than N, the read set overlaps the write set on at least one replica.N = 3 replicaswrite ackwrite acknot reached yetread quorum can include A+BCoordinatorroutes requestReplica Ahas v9Replica Bhas v9Replica Cstill v8Costmore replicas per operationhigher tail latencybetter stale-read oddsW=2 and R=2 overlap because R + W > N
A quorum read overlaps a quorum write. With three replicas, writing to two and reading from two means at least one read replica has the latest acknowledged write, assuming conflict resolution uses versions or timestamps correctly.

Quorums are not a free lunch. Higher W increases write latency and reduces write availability. Higher R increases read latency and fan-out. The default I would say in an interview is: use leader-follower for relational systems unless the problem calls for a Dynamo-style store, then tune quorum per data type.

11Partitioning and sharding: splitting data without splitting your brain

Sharding Definition

Splitting rows across multiple database nodes so no single node owns all data or traffic. It is like assigning delivery zones to restaurants: the zone boundary makes routing easier, but orders crossing zones become harder.

First separate vertical partitioning from horizontal sharding. Vertical partitioning splits by concern: identity fields in one table, profile fields in another, audit logs elsewhere. It can reduce hot row width and clarify ownership. Horizontal sharding splits rows of the same logical table across nodes, usually because one machine no longer handles the data size or traffic.

The shard key is the decision you should slow down for. A good key has high cardinality, spreads traffic uniformly, keeps common queries local to one shard and avoids hotspots. For a user-centric product, user_id is often a good starting point. For a multi-tenant SaaS, tenant_id gives locality, but large tenants may need dedicated handling.

Shard keyGood forWatch out for
user_idProfiles, carts, user-scoped feeds, notification settings.Celebrity users or cross-user queries.
tenant_idB2B SaaS where most queries stay inside one tenant.One enterprise tenant can dwarf all others.
conversation_idChat messages where a thread is read together.User inbox queries may scatter across conversations.
created_atRange retention and archive jobs.Hot latest partition: all new writes hit the same shard.

Modulo sharding, hash(key) % N, is easy until you resize. Add a fifth shard to a four-shard cluster and many keys move because the modulo changed. Consistent hashing places nodes and keys on a ring, so adding a node moves only part of the keyspace, especially when you use virtual nodes.

Fig. 11

Hash sharding, range sharding and hotspots

Hash sharding, range sharding and hotspotsHash sharding sends users through a hash function to spread load. Range sharding keeps ordered ranges together but can create a hot latest range. Consistent hashing uses a ring so adding a node moves only a slice of keys.Hash shardingRange shardingConsistent hashing ringhash()new writes pile upuser_idu_917Shard 0spread keysShard 1spread keysShard 2spread keysJan-MarcoolApr-JuncoolTodayhotAE newBCDAdd node Ekeys in (A, E] move B to Eevery other key stays putclockwiseGood shard keyhigh cardinalityuniform trafficmost queries hit one shardHot shard fixesbucket a hot keydedicate large tenantsprecompute global views
Hash sharding spreads load, range sharding preserves order, and both can create hotspots. Consistent hashing reduces data movement during resize, while range shards need active splitting when one range gets hot.

Hot partitions are the failure mode to name. If you shard by created_at, today's shard gets nearly all writes. If one product goes viral, product_id can overload one shard. Common fixes are salting or bucketing the hot key, moving large tenants to dedicated shards, or changing the write path so hot counters are aggregated asynchronously.

Global secondary indexes are the hidden sharding tax

Once you shard by user_id, a lookup by email is no longer local unless the email index is partitioned the same way. You either scatter the lookup to every shard, keep a separate lookup table from email to user id, or use a search/indexing system fed asynchronously. Each option changes consistency.

This is why I ask for access patterns before choosing a shard key. If login is by email, we may keep a strongly consistent identity table keyed by email while sharding high-volume user activity by user id. The goal is not to make every query single-shard. The goal is to keep the hot, user-facing queries single-shard and move the rest to explicit supporting systems.

Cross-shard operations are where the bill arrives. A query like "total revenue across all users" becomes scatter-gather: ask every shard, wait for the slowest one, then merge. Cross-shard transactions need two-phase commit or a saga, both of which add failure modes. Global ordering requires a distributed sort or an external index.

The sharding answer interviewers like

"I would avoid sharding until a single primary with replicas, good indexes and archival no longer works. If we must shard, I will choose a key so 95% or more of product queries hit one shard. Analytics and global search move to separate systems fed by CDC, not cross-shard queries on the request path."

12Caching and databases: a useful lie with an expiry date

Cache Definition

A faster store that keeps a copy of data from somewhere slower, usually memory in front of a database. Think of it like keeping the menu specials on a small card at the counter: quick to read, but somebody must update the card when the kitchen changes the price.

A cache is a performance tool, not a truth machine. The database remains the source of truth, and the cache gives you lower latency by accepting that a value may be stale for a small window. The design question is not "can we cache this?" It is how wrong can this answer be, and for how long?

Use that question to separate safe caches from dangerous ones. A product description can be stale for a few minutes. A restaurant availability flag can be stale for a few seconds. A payment status shown as final should not be stale unless your product explicitly has a "pending" state.

Cache-aside is the default pattern

Cache-aside, also called lazy loading, means the application controls the cache. On a read, you check Redis or Memcached first. On a miss, you read the database, store the result with a TTL, then return it. On a write, you update the database and invalidate the relevant cache key.

def get_product(product_id):
    key = f"product:{product_id}"
    product = cache.get(key)
    if product is not None:
        return product

    product = db.query_product(product_id)
    cache.set(key, product, ttl_seconds=300)
    return product


def update_product_price(product_id, price):
    db.update_product_price(product_id, price)
    cache.delete(f"product:{product_id}")

The small but important detail is delete, not update. Updating the cached value sounds fresher, but two writers can race and leave an older computed value in cache after a newer database write. Deleting is safer because deletes commute: if two writers delete the same key, the next read goes back to the database and repopulates from the latest committed state.

Here is the read and write flow I would draw before talking about Redis settings.

Fig. 12

Cache-aside read, write and the invalidation race

Cache-aside read, write and the invalidation raceA cache-aside read checks Redis first, falls back to the database on miss, then fills the cache. Writes update the database and delete the cache key. Updating the cache directly can race and leave an older value in cache after a newer database write.Read pathWrite pathWhy not update the cached value directly?1 gethitmissfill cache1 write DB2 delete keyAppGET product:42Cachehit: returnDatabasemiss: readAppPUT price=4500DB updatesource of truthCache deleteinvalidate keyT1 writesDB = 2T2 writesDB = 3T2 cachecache = 3T1 late cachecache = 2Badcache 2DB 3Delete is saferTwo deletes commute; two cache writescan arrive out of order.Still use TTLInvalidation events can be lost;TTL is the backstop.Default: update the database, then delete cache. The next read repopulates.
Cache-aside keeps the database authoritative. Reads hit cache first and fill on miss; writes update the database and delete the key, avoiding the stale overwrite race that can happen when writers update cache directly.
Many candidates cache first and reason later

That is backwards. Start by naming the source of truth, the tolerated staleness window and the invalidation trigger. Then choose TTL, cache-aside, write-through or write-behind. If you cannot explain what happens when the cache is wrong, you have not designed the cache yet.

TTL, event invalidation and versioned keys

TTL-based invalidation is the cheapest backstop. If you cache product:42 for 300 seconds, the worst-case staleness is roughly five minutes, assuming clocks and eviction behave normally. A shorter TTL reduces staleness but increases database load. A longer TTL protects the database but risks users seeing old data.

Event-driven invalidation makes the stale window smaller. When a product changes, the product service publishes ProductUpdated, and cache listeners delete product:42. This is more work because the event path must be monitored, retried and made idempotent, but it is the right move for data where five minutes is too stale.

Versioned keys avoid in-place invalidation. Instead of product:42, the app reads product:42:v7, where v7 comes from the database row version. A new product version uses a new cache key, so stale keys become unreachable and expire naturally. The cost is extra key lookup and old versions consuming memory until TTL or LRU eviction clears them.

PatternGood fitMain riskDefault I'd pick
Cache-asideRead-heavy data where misses can tolerate a DB round trip.Stale reads after writes if invalidation is missed.Most product data, with TTL plus delete-on-write.
Write-throughData that must be present in cache immediately after write.Higher write latency and cache pollution for rarely read keys.Use selectively for hot session or config data.
Write-behindView counts, analytics counters and other loss-tolerant writes.Cache failure before flush can lose data.Avoid for money, inventory and user-visible state.
Versioned keysObjects with clear versions and long read lifetimes.More keys and a dependency on version lookup.Great for rendered pages, product cards and immutable blobs.

Stampedes and negative caching

A cache stampede happens when a hot key expires and many requests miss at the same time. If 1,000 requests for product:42 all miss at once, the database receives 1,000 identical reads and the cache gets 1,000 identical fills. This is how a cache meant to protect the database becomes the thing that takes it down.

The usual fixes are request coalescing, stale-while-revalidate and TTL jitter. Request coalescing lets one request rebuild the key while the rest wait briefly or serve stale data. Stale-while-revalidate returns the old value immediately and refreshes in the background. TTL jitter adds randomness, for example 270 to 330 seconds instead of exactly 300, so a batch of keys does not expire together.

Negative caching is the same idea for absence. If attackers or buggy clients ask for millions of non-existent users, cache NOT_FOUND for a short TTL such as 30 or 60 seconds. Be careful with permissions: caching "not found" for a private object can leak or hide access changes if the key does not include the caller context.

The caching answer that works in interviews

Say: "I'll use cache-aside for read-heavy product data, delete on writes, add a TTL as a safety net, and prevent stampedes with single-flight rebuilds or stale-while-revalidate. I will not cache payment finality unless the UI can show pending state." That tells the interviewer you understand both speed and correctness.

Members-only article

Keep reading with the Article Pass

You've read the free part. The other 12 sections of this guide, with every diagram and worked example, are for members. One Article Pass unlocks every members-only article on Engineering Atlas: the ones live today and every one I publish while your pass is active.

  • Database scaling: climb the cheap rungs before sharding
  • Microservices and database ownership: split data only when you can own the consequences
  • Distributed transactions and sagas: choosing between atomic commit and recoverable workflow
  • Event-driven data systems: logs, outboxes and replay
  • Search, analytics and OLAP: move scans off the request database
  • Schema design for HLD: entities, access paths and failure behavior
  • Reliability and disaster recovery: RPO, RTO and the restore drill
  • Common HLD database patterns: holds, idempotency and leases
  • Worked database examples: three designs I would defend
  • Interview decision framework: the database flow I use
  • Pitfalls and cheat sheets: the fast review before your interview
  • Interview questions

From ₹49 · one-time payment, no auto-renewal · separate from course passes