Lessons
1 · System Design Foundations
2 · APIs and Communication
3 · Capacity Estimation
4 · Core Building Blocks
5 · Databases
- 12Relational Databases and Data Modeling for System Design
- 13Database Indexes and Query Performance: How to Make Reads Fast
- 14Transactions, Isolation Levels and Concurrency Control: Preventing Race Conditions
- 15Database Replication: Primary-Replica, Read Replicas, Lag and Failover
- 16Database Sharding and Partitioning: How It Works and When You Need It
- 17SQL vs NoSQL: Data Models and How to Choose the Right Database
6 · Distributed Systems
- 18Why Distributed Systems Are Hard: Partial Failure, Timeouts and Clocks
- 19CAP Theorem and Consistency Models Explained Without the Myths
- 20Idempotency, Retries and Message Delivery Guarantees
- 21Distributed Transactions: Two-Phase Commit, the Outbox Pattern and Sagas
- 22Distributed Locks, Leases, Leader Election and Consensus
7 · Reliability and Operations
8 · Architecture and Security
9 · Interview Method and Foundation Case Studies
10 · Real-World Case Studies
- 33Design a Notification System: Email, SMS and Push at Scale
- 34Design a Chat System Like a Messaging App
- 35Design a News Feed: Fan-Out, Ranking and the Celebrity Problem
- 36Design Search Autocomplete (Typeahead)
- 37Design a Distributed Job Scheduler
- 38Design a Ticket Booking System Without Overselling
- 39Design a Payment System: Idempotency, Ledgers and Reconciliation
11 · Large-Scale Case Studies
- 40Design a Video Streaming Platform: Upload, Transcoding and Adaptive Streaming
- 41Design a Ride-Hailing System: Location, Matching and Real-Time Updates
- 42Design a Food Delivery Platform: Orders, Restaurants and Delivery Partners
- 43Design an E-Commerce System: Catalog, Cart, Checkout, Inventory and Flash Sales
- 44Design a Web Search Engine: Crawling, Inverted Indexes and Ranking
12 · Interview Readiness
Lesson 17 · Databases
SQL vs NoSQL: Data Models and How to Choose the Right Database
A trade-off-driven guide to choosing databases: relational, key-value, document, wide-column, graph, search, time-series and analytical stores, what each is good and bad at, query-first modelling for NoSQL, the myths ("NoSQL scales, SQL doesn't"), polyglot persistence, and a decision process you can use in interviews.
For
Interview candidates who need to justify a database choice, and developers deciding between relational and non-relational stores for a new system.
Level
Before you start
Read articles 12 to 16 of this course: data modelling, indexes, transactions, replication and sharding. The access-pattern method from article 12 is the basis of every choice here.
On this page
Key takeaways
- "SQL versus NoSQL" is the wrong question; choose by data shape, access patterns, consistency and transaction needs, scale and the team's ability to operate the store.
- Relational databases are the right default for structured data with relationships and transactions; specialised stores earn their place for specific access patterns.
- Many non-relational stores scale out easily because they restrict queries and transactions; model them query-first, often duplicating data per query.
- Search engines, caches and analytical stores are usually derived copies fed from a source of truth, not the source of truth themselves.
- Every additional database adds operations work and a synchronisation problem, so add one only when a requirement justifies it.
"Should we use SQL or NoSQL?" is one of the most common questions in design interviews, and one of the most commonly mis-answered. "NoSQL, because it scales" is a slogan, not a reason. "SQL, because it's what I know" is honest, but still not a reason. The real question is: given these access patterns, this data shape, these consistency needs and this scale, which store makes the important operations simple, fast and correct?
This article gives you a working map of the database families, the trade-offs each one makes, and a process for choosing that you can say out loud in an interview.
What you will learn
#- What "NoSQL" actually covers, and the main database families.
- What each family is good at, bad at, and typically used for.
- The common myths, and what is true behind them.
- Query-first modelling for key-value, document and wide-column stores.
- Polyglot persistence: using several stores, and its costs.
- A decision process, applied to worked examples.
What "NoSQL" means
#"NoSQL" is an umbrella term for databases that do not use the relational table model as their primary interface. It covers very different systems with different data models, guarantees and query abilities. Treating them as one category is the first mistake. A more useful map:
Family | Data model | Strong at | Weak at | Examples |
|---|---|---|---|---|
Relational | Tables, rows, joins, constraints | Transactions, relationships, ad hoc queries, integrity | Scaling writes beyond one machine without extra machinery | PostgreSQL, MySQL, SQL Server, Oracle; managed versions |
Key-value | A value looked up by its key | Very fast get and put at huge scale; simple | Queries other than by key | Redis, Amazon DynamoDB (key-value and document), etcd |
Document | JSON-like documents grouped in collections | Flexible, nested data read as a whole; per-document atomic updates | Many-to-many relationships, cross-document joins | MongoDB, Firestore, Couchbase |
Wide-column | Rows in partitions, sorted by a clustering key | Massive write throughput, large data, predictable queries by key and range | Ad hoc queries, joins, frequent cross-partition operations | Apache Cassandra, ScyllaDB, Google Bigtable, HBase |
Graph | Nodes and relationships with properties | Multi-hop traversals: friends of friends, fraud rings, recommendations | Bulk aggregations over everything; very high write rates | Neo4j, Amazon Neptune |
Search engine | Inverted indexes over documents | Full-text search, relevance ranking, faceting | Being the source of truth; transactions | Elasticsearch, OpenSearch, Apache Solr |
Time-series | Measurements indexed by time | High-rate appends, time-window queries, downsampling, retention | General-purpose queries | TimescaleDB, InfluxDB, Prometheus |
Analytical (columnar) | Columns stored separately, compressed | Aggregations over billions of rows | Many small transactional writes and point lookups | BigQuery, Amazon Redshift, ClickHouse, Snowflake |
Object storage | Blobs by key | Huge files, durability, cost | Querying contents; small frequent updates | Amazon S3, Google Cloud Storage, Azure Blob Storage |
The families in a little more depth
#Key-value stores
#The simplest model: put(key, value) and get(key). Because every operation names its key, the store can partition data by key and route each request to one node, which scales very well. Redis adds rich data structures in memory (article 09); DynamoDB adds sort keys, secondary indexes and durability as a managed service.
Use when access is almost always by a known key: sessions, user preferences, shopping carts, feature flags, idempotency records, rate-limit counters.
Document stores
#Each record is a document, often JSON, which can nest arrays and sub-objects. An order and its items can live in one document, read and written as a unit. MongoDB's data-modelling guidance discusses the central choice: embed related data in one document when it is read together, or reference it in separate documents when it is large, shared or grows without bound (MongoDB: Data Modeling).
"Flexible schema" means the database does not force every document to have the same fields. The schema still exists; it lives in your application code and must be versioned there ("schema on read").
Use for: content and catalogue data with varied attributes, user profiles with nested settings, data naturally shaped as self-contained documents.
Wide-column stores
#Data lives in partitions identified by a partition key; within a partition, rows are sorted by a clustering key. Reads are efficient when they name the partition and a range of the clustering key ("the latest 50 messages of conversation X"). These systems descend from Google's Bigtable, described as a distributed storage system for structured data designed to scale to petabytes across thousands of machines (Bigtable), and from Amazon's Dynamo. Apache Cassandra's documentation explains how it adopts Dynamo-style partitioning with consistent hashing, replication and tunable consistency (Cassandra: Dynamo).
Use for: very high write volumes with predictable queries: messages, activity feeds, event logs, IoT readings, time-ordered data per entity.
Graph databases
#When the important questions are about connections ("how is A connected to B within 3 hops?", "accounts sharing devices with this flagged account"), relational joins become deep and slow. Graph databases store relationships as first-class data, so traversals follow pointers instead of joining tables. Neo4j's documentation introduces this model of nodes, relationships and properties (What is a graph database?).
Use for: social graphs with multi-hop queries, fraud detection, recommendation paths, knowledge graphs. Not for simple "who do I follow" lookups, which a relational or key-value table handles perfectly well.
Search engines
#A search engine builds an inverted index: a map from each term to the documents containing it, with information for ranking. Elastic's documentation describes how documents are stored in indices and made searchable (Index basics). Search engines answer "find products matching 'red running shoes', ranked by relevance, filtered by size and price" far better than a relational LIKE query.
They are usually derived stores: the source of truth lives in a database, and an indexing pipeline copies changes into the search engine, so search results can lag by seconds. Articles 36 and 44 build on this.
Time-series and analytical stores
#Time-series stores optimise for appending measurements and querying windows of time, with automatic rollups and retention. Columnar analytical stores store each column separately and compress it, so "sum revenue by city for last year" reads only two columns from billions of rows. Both are typically fed from operational systems; they do not replace them.
This is the OLTP versus OLAP split: online transaction processing (many small reads and writes for users, which relational and NoSQL operational stores do) versus online analytical processing (large scans and aggregations for reports, which warehouses and columnar stores do).
Myths and what is actually true
#Myth | What is actually true |
|---|---|
"NoSQL scales; SQL does not." | Relational databases scale reads with replicas and can be sharded (articles 15 and 16), and distributed SQL databases exist. Many NoSQL stores make scale-out easier by restricting what you can do: no joins, limited transactions, queries mostly by key. That restriction is the trade, not magic. |
"NoSQL is schemaless." | The schema moves from the database into application code. You still need to version and migrate it. |
"NoSQL has no transactions." | Several popular non-relational databases now support transactions, often with limits on scope, size or performance. Read the documentation for the exact guarantees. |
"Relational databases are slow." | For indexed point lookups and short transactions they are very fast. Slowness usually comes from missing indexes, poor queries or the wrong access pattern. |
"Use NoSQL for big data." | The volume alone does not decide. Access patterns, consistency and query needs do. |
An honest summary: a distributed database such as Google Spanner shows that strong consistency and horizontal scale can be combined, using carefully engineered clock infrastructure (Google documents how its TrueTime system supports external consistency (Spanner: TrueTime and external consistency)). The costs move into latency, complexity or price; they do not disappear.
Modelling query-first
#Relational modelling starts from entities and normalises. Key-value, document and especially wide-column modelling start from queries: for each important query, design a structure that answers it with one lookup or one range read, even if that duplicates data.
Example: chat messages in a wide-column store
#Access pattern: "show the latest 50 messages of a conversation, then older ones as the user scrolls."
-- Cassandra Query Language (CQL), which looks like SQL but follows wide-column rulesCREATE TABLE messages_by_conversation ( conversation_id uuid, month_bucket text, -- for example '2026-10'; keeps partitions from growing forever message_seq bigint, -- per-conversation sequence number sender_id bigint, body text, sent_at timestamp, PRIMARY KEY ((conversation_id, month_bucket), message_seq)) WITH CLUSTERING ORDER BY (message_seq DESC);- The partition key
(conversation_id, month_bucket)puts a conversation's messages for a month together on one set of replicas. - The clustering key
message_seq DESCstores them newest first, so "latest 50" is one sequential read. - The month bucket prevents one very long-lived conversation from becoming one enormous partition.
- A second query such as "all messages sent by user X" is not served by this table; if it were needed, you would write the same message to a second table keyed by sender. Duplication per query is normal here.
Example: an order as a document
#{ "_id": "ord_77", "customer_id": 42, "status": "shipped", "items": [ {"product_id": 9, "name": "Running shoes", "qty": 1, "unit_price_paise": 349900}, {"product_id": 15, "name": "Socks", "qty": 2, "unit_price_paise": 19900} ], "shipping_address": {"city": "Chennai", "pin": "600001"}, "created_at": "2026-10-04T18:22:07Z"}Items and the address are embedded because they are always read with the order and belong to it. The product catalogue is referenced by product_id because products are shared and change independently.
Polyglot persistence
#Large systems often use several stores, each for what it does best: a relational database as the source of truth, Redis for caching and counters, object storage for files, a search engine for search, a warehouse for analytics. This is called polyglot persistence.
The costs are real:
- Synchronisation. Derived stores must be updated when the source changes, usually through events or change data capture, and they lag. A product renamed in the database may show the old name in search for a few seconds (articles 21 and 27).
- Consistency across stores. There is no transaction spanning PostgreSQL and Elasticsearch.
- Operations. Each store needs monitoring, backups, upgrades, capacity planning, security and people who understand it.
Keep the number of stores small, and know which one is the source of truth for each piece of data.
A decision process
#Work through these questions in order. Each narrows the choice.
- What is the data's shape? Structured with many relationships, self-contained documents, simple key-values, time-ordered events, a graph, or large blobs?
- What are the hot access patterns? By key? By key and time range? Ad hoc filtering and joins? Full-text? Multi-hop traversals? Aggregations?
- What consistency and transactions are required? Must several records change atomically? Must reads always see the latest write?
- What is the scale? Data size, write rate, read rate (from your estimates). Can one well-provisioned relational primary handle it, with replicas and caching?
- What can the team operate? A managed service the team understands beats an exotic store nobody can run at 3 a.m.
Situation | Reasonable choice | Why |
|---|---|---|
Users, orders, payments, inventory | Relational | Relationships, constraints, transactions |
Sessions, carts, rate limits, idempotency keys | Key-value (often Redis or DynamoDB) | Access by key, high rate, simple |
Product catalogue with varied attributes | Document store, or relational with JSON columns | Flexible attributes read as a unit |
Chat messages, activity logs at very high write volume | Wide-column | Partition by entity, range by time, huge writes |
"People you may know", fraud rings | Graph (often alongside a relational source of truth) | Multi-hop traversals |
Product and content search | Search engine fed from the source of truth | Relevance and full-text |
Metrics and monitoring data | Time-series store | Appends, windows, retention |
Business reporting | Columnar warehouse fed by pipelines | Large aggregations without hurting OLTP |
When in doubt, start with a relational database, model it well, and move specific access patterns to specialised stores when estimates or measurements show the need. That is a mature, defensible default, not a lack of imagination.
Worked example 1: Pixly's stores
#Data | Store | Reasoning |
|---|---|---|
Users, photos metadata, follows | PostgreSQL (primary, standby, read replicas) | Relationships, constraints (unique usernames, one follow per pair), transactions; a few TB per year fits (article 07) |
Photo files | Object storage plus CDN | Petabytes of blobs |
Cache, sessions, like counters, precomputed feeds | Redis | Key-based, very hot, tolerant of brief loss (with persistence for sessions if needed) |
Likes at large scale (if the relational table becomes too write-heavy) | A wide-column store keyed by | 1,700 writes per second at peak today is fine for PostgreSQL; revisit if it grows 10 times |
Search (later) | OpenSearch or Elasticsearch fed by photo events | Captions and hashtags, relevance |
Analytics | A warehouse fed from database changes and events | Reports without touching the primary |
Notice that Pixly does not start with Cassandra "for scale". The estimates show one relational primary is enough today; the design names exactly what would trigger a change.
Worked example 2: a chat app's message store
#Estimates from articles 06 and 07: about 92,000 messages per second at peak, about 1.2 PB per two years before replication; access is almost always "messages of one conversation, newest first".
- Relational, sharded by conversation: workable, but you would be building and operating sharding, rebalancing and routing yourself.
- Wide-column (Cassandra-like) with partition key (conversation, time bucket): matches the access pattern exactly, scales writes by adding nodes, and replicates across nodes with tunable consistency. Users, contacts and group membership still live in a relational database, where relationships and constraints matter.
Choice: wide-column for messages, relational for accounts and groups, Redis for presence and connection routing (article 34). Each store matches one access pattern.
Failure modes and operational surprises
#Problem | Where it bites | Mitigation |
|---|---|---|
Eventual consistency surprises | Reads after writes in leaderless or replicated stores | Choose consistency levels per query; read-your-writes routing (article 19) |
Unbounded partitions | Wide-column partitions that grow forever (one huge chat) | Time buckets in the partition key |
Hot partitions | Celebrity keys, time-based keys | Better keys, salting, caching (article 16) |
Schema drift | Documents with many versions of their shape | Version fields, migration jobs, validation rules |
Derived store out of sync | Search shows deleted items | Reliable change pipelines, periodic reconciliation |
Using a cache or search engine as the only copy | Data loss on restart or reindex | Keep a durable source of truth |
Trade-offs
#Choice | You gain | You pay |
|---|---|---|
Relational | Integrity, transactions, flexible queries, mature tooling | Write scale-out needs sharding or a distributed SQL product |
Key-value | Speed and simple scale-out | Few query options |
Document | Natural shapes, flexible fields | Relationships and cross-document consistency are harder |
Wide-column | Write throughput and size | Query-first modelling, duplicated data, fewer ad hoc queries |
Graph | Fast traversals | Narrow purpose, separate operations |
Several stores | Best tool per job | Synchronisation, consistency and operations costs |
Common mistakes
#- Choosing NoSQL "because it scales" without estimates or access patterns.
- Choosing a document store for heavily relational data with many-to-many relationships and strong integrity needs.
- Using a search engine or a cache as the system of record.
- Modelling a wide-column store like a relational one and then needing joins.
- Adding a new database for every feature.
- Ignoring who will operate the store.
In an interview
#- Do not open with a product name. Open with the access pattern: "messages are written at 90,000 per second and read by conversation, newest first."
- Then name the family and one example: "a wide-column store partitioned by conversation and time bucket, such as Cassandra; or sharded PostgreSQL if the team knows it better."
- Say what you give up (ad hoc queries, joins) and why that is acceptable.
- Name the source of truth when several stores hold related data, and how they stay in sync.
Exercises
#Exercise 1
Choose a store
#Choose a database family (and say why) for:
- A bank's account balances and transfers.
- Driver location pings every 4 seconds from 500,000 drivers, queried as "this driver's path over the last hour".
- A recipe site where each recipe has a different set of attributes, read as a whole page.
- Detecting rings of accounts that share phone numbers and devices.
- Autocomplete for 10 million product names.
Exercise 2: Model it query-first
#A ride-hailing app needs: (a) a rider's trip history, newest first; (b) a driver's trip history, newest first. Using a wide-column store, design the table or tables, with partition and clustering keys, and explain why one table is not enough.
Exercise 3: Critique the design
#A candidate proposes: "We'll use Elasticsearch as our only database for the e-commerce site, since it handles search and scales well. Orders, payments and inventory all go in it." List the problems and propose a better split.
Exercise 4: Embed or reference?
#In a document store for a blogging platform, decide whether to embed or reference, and why:
- A post's tags.
- A post's comments, where popular posts get 50,000 comments.
- The author's profile (name, avatar, bio).
- Revision history of a post.
Show solutionHide solution
- Relational. Transfers must be atomic across rows, balances must never be wrong, and auditing needs constraints and history (articles 14 and 39).
- Wide-column or time-series, partitioned by
driver_idplus a time bucket, clustered by timestamp. Very high write rate, simple "one driver, a time range" queries. Live "drivers near me" queries need a separate geospatial index (article 41). - Document store, or relational with a JSON column for the variable attributes. Each recipe is a self-contained document read as one unit.
- Graph database (fed from the source of truth). The question is multi-hop: account to device to account to phone to account.
- A search engine or a purpose-built prefix index (article 36), built from the product database, with caching for popular prefixes.
Exercise 2
Show solutionHide solution
Two tables, one per query, because each query has a different partition key:
CREATE TABLE trips_by_rider ( rider_id bigint, year_month text, requested_at timestamp, trip_id uuid, driver_id bigint, fare_paise bigint, PRIMARY KEY ((rider_id, year_month), requested_at, trip_id)) WITH CLUSTERING ORDER BY (requested_at DESC, trip_id ASC);CREATE TABLE trips_by_driver ( driver_id bigint, year_month text, requested_at timestamp, trip_id uuid, rider_id bigint, fare_paise bigint, PRIMARY KEY ((driver_id, year_month), requested_at, trip_id)) WITH CLUSTERING ORDER BY (requested_at DESC, trip_id ASC);A single table partitioned by rider would make "driver's history" a scan of every partition; partitioned by driver, the rider query has the same problem. Each trip is written to both tables (and ideally to a relational or event store that remains the source of truth). The year_month bucket keeps a frequent rider's or driver's partition bounded; trip_id in the clustering key makes rows unique when two trips share a timestamp.
Exercise 3
Show solutionHide solution
Problems:
- Transactions: placing an order must atomically reserve stock and record payment state; search engines are not designed as transactional systems of record.
- Durability and correctness: search clusters are typically rebuilt and reindexed; treating them as the only copy risks losing orders.
- Consistency: indexing is near-real-time, so a just-placed order may not be visible immediately, which is unacceptable for "my orders" and payment flows.
- Constraints: no foreign keys or unique constraints to prevent duplicates and orphans.
Better split: a relational database for orders, payments and inventory (source of truth with transactions); a search engine for product search, fed by product-change events; a cache for product pages; object storage for images. Search results can be seconds stale; orders cannot.
Exercise 4
Show solutionHide solution
- Embed. Tags are small, bounded and always shown with the post.
- Reference (a separate comments collection keyed by post id and time). 50,000 comments would make the document huge, exceed size limits, and turn every new comment into a rewrite of a large document. Paginate comments separately.
- Reference, possibly with a small embedded copy of the display name and avatar URL for fast rendering. The profile is shared across all of the author's posts and changes independently; if you embed a copy, accept that old posts may show an old avatar or update them asynchronously.
- Reference (a revisions collection). History grows without bound and is rarely read with the post.
Where to go next
#- Next: Why Distributed Systems Are Hard: Partial Failure, Timeouts and Clocks starts Module 6: what changes once data and work are spread over many machines.
- Later: consistency levels and quorums (article 19), keeping derived stores in sync (articles 21 and 27), and search systems (articles 36 and 44).
Sources & further reading
- Technology
- Software Engineering
- #databases
- #distributed systems
- #NoSQL
- #roadmap
- #scalability
- #SQL
- #system design
- #system design interview
Connected lessons
Linked from 1 guide
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
SQL and PostgreSQL Fundamentals for Java Developers
Guide · Technology
Inheritance, Overriding and Overloading
Guide · Career
A LinkedIn Profile That Gets Recruiter Messages

