Five databases for one app?
Kabir draws TiffinGo’s architecture from their tech blog for his notes. He expects one database. He finds five: PostgreSQL, Redis, MongoDB, Cassandra and Neo4j. In college they learned only MySQL and normalisation.
Sir, why five databases? Couldn’t they keep everything in one MySQL database with good indexes?
For a small app, yes. But imagine two lakh carts updating every second during a sale, menus whose fields differ for every kitchen, and every rider sending a location every five seconds. A single relational database would struggle with some of these jobs, or cost a fortune.
So the others are “NoSQL” databases? Does that mean they don’t use SQL at all?
“NoSQL” today means not only SQL. They drop some relational features, like fixed schemas or joins, to gain others: massive scale, flexible documents, millisecond look-ups, or easy relationship queries. There are four big families: key-value, document, wide-column and graph.
And the orders stay in PostgreSQL?
Exactly. Money needs strict transactions, which relational databases do best. Choosing the right store for each job is called polyglot persistence. Today you’ll see a cache save a flash sale, design a Cassandra table around its queries, and catch a fraud ring with a graph.
What is a NoSQL database?
A NoSQL (“not only SQL”) database is a non-relational database that stores data in models other than fixed relational tables, typically to achieve horizontal scalability, flexible schemas and high performance for specific access patterns. The four main families are key-value stores, document stores, wide-column stores and graph databases. Many NoSQL systems relax strict ACID guarantees in favour of availability and partition tolerance (BASE), often offering tunable or eventual consistency.
NoSQL databases are databases that don’t force every record into the same table shape. Some store simple key → value pairs, some store whole JSON documents, some store huge numbers of rows spread across many machines, and some store connections between things. Each one is very good at one kind of job.
Words you’ll meet in this lesson
otp:9876 → 482913.The four families and well-known examples
| Key-value | Redis, Amazon DynamoDB, Memcached, etcd · caches, sessions, carts, rate limits, OTPs |
|---|---|
| Document | MongoDB, Couchbase, Google Firestore, Azure Cosmos DB · catalogues, user profiles, content, app data |
| Wide-column | Apache Cassandra, Apache HBase, ScyllaDB, Google Bigtable · time series, IoT, event logs, messaging |
| Graph | Neo4j, Amazon Neptune, TigerGraph · fraud rings, recommendations, social networks, knowledge graphs |
NoSQL at three levels
Beginner Four kinds of storage cupboards
- Key-value is a cloakroom: give a token (key), get your item (value) instantly. Example: Redis storing
cart:U9 → {thali: 1}. - Document is a file folder per customer: each folder holds a whole form with nested sections, and two folders can have different fields. Example: MongoDB storing a kitchen with its whole menu inside.
- Wide-column is a giant register split into many books by a key: each rider’s book holds his location pages in time order. Example: Cassandra storing GPS pings per rider.
- Graph is a friendship map: dots (people, phones, accounts) connected by lines (shares, refers, pays). Example: Neo4j finding who is connected to whom.
Simple rule: money and strict rules → relational (SQL). Speed by key → key-value. Flexible records → document. Huge write streams → wide-column. Relationships → graph.
Intermediate Model data around your queries
- Access patterns first: in NoSQL you list the questions the app will ask (“get a rider’s last 50 positions”) and shape the data to answer each one with a single look-up. Relational design normalises first and joins later.
- Denormalisation: data is copied where it is read (a kitchen’s name stored inside each order document) to avoid joins, accepting extra storage and harder updates.
- Embed or reference (documents): embed things read together and bounded in size (a menu inside a kitchen); reference things that grow without limit (all orders of a kitchen).
- Partition key (wide-column, key-value at scale): decides which machine holds the data. A good key spreads load evenly and keeps one query on one partition.
- TTL: many NoSQL stores can expire data automatically (carts after 30 minutes, OTPs after 5 minutes).
Advanced ACID vs BASE, tunable consistency and storage engines
- ACID vs BASE: relational databases promise Atomicity, Consistency, Isolation, Durability. Many NoSQL systems chose BASE (Basically Available, Soft state, Eventually consistent) to stay available and fast across many machines. Modern MongoDB and DynamoDB do support multi-document transactions, at a cost.
- Tunable consistency: Cassandra lets each query choose how many replicas must answer. With N replicas, if read replicas R plus write replicas W is greater than N, every read sees the latest write (more in Lesson 3.4).
- Storage engines: B-trees (most relational databases) are good at reads and updates in place; LSM trees (Cassandra, HBase, RocksDB) turn writes into fast sequential appends and merge files in the background, which suits huge write volumes.
- Hot partitions: one celebrity account or one city can overload a single partition. Fix with better keys or by splitting (bucketing) hot keys.
- NewSQL: distributed SQL databases (Google Spanner, CockroachDB, YugabyteDB, TiDB) scale out like NoSQL while keeping SQL and ACID transactions.
Exam tip: Answer “types of NoSQL databases” with all four families: data model, example product, typical use, plus one line each on schema flexibility, horizontal scaling and BASE vs ACID. Add a short SQL vs NoSQL comparison table for full marks.
Key-value, document, wide-column and graph
Key-value
A giant dictionary. Get or set by key in about a millisecond. Values are opaque blobs; no queries on their contents.
Fastest look-upsDocument
JSON-like documents with nesting and flexible fields. Query and index by fields inside the document.
Flexible recordsWide-column
Rows grouped into partitions by key and sorted inside each partition. Built for huge write volumes across many nodes.
Massive writesGraph
Nodes and relationships stored directly. Multi-hop questions (“friends of friends”) are fast and natural.
RelationshipsSQL vs NoSQL
| Relational (SQL) | NoSQL | |
|---|---|---|
| Data model | Tables with rows and columns | Key-value, document, wide-column or graph |
| Schema | Fixed, defined in advance | Flexible; can differ per record |
| Scaling | Mostly vertical (bigger server); read replicas | Horizontal (add nodes) by design |
| Joins | Powerful, central to design | Limited or none; data denormalised |
| Transactions | Strong ACID | Often BASE / tunable; some support ACID |
| Design approach | Normalise, then query anything | Start from the queries, shape data for them |
| Best for | Payments, orders, accounting, reports | Caches, catalogues, events, time series, graphs |
Same order, four models
| Family | How order O1043 might be stored |
|---|---|
| Key-value | order:O1043 → {"user": "U9", "total": 420, "status": "DELIVERED"} |
| Document | {"_id": "O1043", "user": {"id": "U9", "name": "Riya"}, "items": [{"dish": "thali", "qty": 1}], "total": 420} |
| Wide-column | partition user_id = U9, rows sorted by order_time DESC: O1043, O1012, O0987… |
| Graph | (Riya)-[:PLACED]->(O1043)-[:FROM]->(Annapurna Kitchen) |
NoSQL is not “better” than SQL. Most companies keep money in a relational database and add NoSQL stores around it. In interviews, always justify a NoSQL choice by the access pattern and scale, not by fashion.
One order across five databases
Key-value: Riya’s cart in Redis
A cart is read and written by one key, many times a minute, and is useless after half an hour. A key-value store with a TTL does exactly this in about a millisecond, and the expired OTP disappears by itself.import time cache = {} def set_key(k, v, ttl): cache[k] = (v, time.time() + ttl) def get_key(k): v, exp = cache.get(k, (None, 0)) return v if time.time() < exp else None set_key("cart:U9", {"thali": 1, "lassi": 2}, ttl=1800) set_key("otp:9876", "482913", ttl=-1) # already expired print(get_key("cart:U9"), get_key("otp:9876")){'thali': 1, 'lassi': 2} NoneDocument: each kitchen is one document
The menu lives inside the kitchen document, so one read fetches the whole page. Kitchens can have extra fields (likekitchens = [ {"_id": "K11", "name": "Annapurna", "veg": True, "menu": [{"dish": "thali", "price": 220}]}, {"_id": "K12", "name": "Spice Box", "veg": False, "menu": [{"dish": "biryani", "price": 280}], "halal": True}, ] veg = [k["name"] for k in kitchens if k["veg"]] print("veg kitchens:", veg, "| K12 extra field halal:", kitchens[1].get("halal"))veg kitchens: ['Annapurna'] | K12 extra field halal: Truehalal) without changing a schema for everyone.Wide-column: rider pings partitioned by rider
Each rider’s pings live together in one partition, kept sorted by time. “Latest position of R7” reads one partition, even when there are billions of pings in total spread over many nodes.pings = {} # partition key = rider, rows kept sorted by time for rider, t, lat in [("R7", 3, 18.52), ("R9", 1, 19.07), ("R7", 1, 18.50), ("R7", 2, 18.51)]: pings.setdefault(rider, []).append((t, lat)) pings[rider].sort() print("R7 partition:", pings["R7"]) print("latest for R7:", pings["R7"][-1])R7 partition: [(1, 18.5), (2, 18.51), (3, 18.52)] latest for R7: (3, 18.52)Graph: who referred whom
Relationships are stored directly, so “referrals of my referrals” is a short walk. In SQL each extra hop is another self-join on a large table.refers = {"Riya": ["Kabir", "Meera"], "Kabir": ["Arjun"], "Meera": [], "Arjun": ["Zoya"]} level1 = refers["Riya"] level2 = [p for f in level1 for p in refers[f]] print("referred by Riya:", level1, "| their referrals:", level2)referred by Riya: ['Kabir', 'Meera'] | their referrals: ['Arjun']Relational: the payment itself
When Riya pays ₹420, the order row, the payment row and the wallet balance change together inside one PostgreSQL transaction: all or nothing.
This is where ACID matters most. If the payment fails half-way, nothing is saved. That is why money stays in a relational database.Polyglot persistence
One order touched five databases, each doing what it does best. Events from all of them also flow into the data lake for analytics.
The cost: more systems to run, monitor and keep in sync. Teams add a new database only when an access pattern truly needs it.
One app session: what Riya sees, and which database answers
Step through the six frames. Left: Riya using the app. Right: the database that serves each step, and the query it runs.
- Riya opens the app and is greeted by name; it already knows her area.
- She opens Annapurna Kitchen and sees its full menu and rating.
- She adds a thali and two lassis; the cart updates instantly with every tap.
- She pays ₹420 by UPI and gets a confirmation.
- She watches the rider move towards her on the map.
- The app shows what her friends ordered and invites her to refer someone.
- The app looks up her session in Redis by its token: a key-value read in about a millisecond.
- The kitchen page is one MongoDB document containing name, rating and the nested menu.
- The cart is a Redis key updated on every tap, with a 30-minute expiry (TTL).
- The payment and order are written in one PostgreSQL ACID transaction: either both are saved or neither is.
- The rider’s phone writes a location every five seconds into Cassandra, partitioned by rider and sorted by time.
- A two-hop graph query finds dishes her friends ordered. (Timings are typical, not exact.)
Real-time scenarios: NoSQL under pressure
Three live scenarios: a key-value cache that saves a flash sale, a Cassandra table designed around its query, and an advanced graph search that exposes a fraud ring.
Real-time scenario 1Flash sale: a key-value cache saves the database
At midnight a sale starts and everyone opens the same few kitchen pages. Watch the cache-aside pattern take the load off the database.
- Two lakh page views a second all ask the database for the same popular menus. Its CPU saturates and pages slow to three seconds.
- Cache-aside: the app first asks Redis. On a miss it reads the database and saves the result in Redis with a short expiry (TTL).
- For the next minute, every request for that menu is a hit, served from memory in about a millisecond.
- With a 99% hit ratio, only 2,000 of the 2 lakh reads per second reach the database.
- When data changes, the app deletes (invalidates) the cached key; the short TTL also limits how stale data can get. (Numbers are illustrative.)
Designing a Cassandra table around its query
TiffinGo stores every rider’s GPS ping, crores per day. Support agents need “the last 50 positions of rider R77”. Watch a natural-looking table fail, and a query-first design succeed.
- The first design partitions rider pings by
city, with time as the clustering key. - Partitions are badly unbalanced: Mumbai’s is several times bigger than the others and grows without limit, so one node is overloaded. This is a hot partition.
- Worse, the support query must scan the whole Mumbai partition and filter for one rider, reading millions of rows to return fifty.
- The better key is
(rider_id, day): many small, similar-sized partitions spread evenly, each with rows sorted newest first. - The query becomes a single-partition read of the first 50 rows: milliseconds at any scale. In NoSQL you design the table for the query, not the other way round. (Sizes are illustrative.)
Catching a cashback fraud ring with a graph
A promotion gives ₹500 to every new user. Fraudsters create many accounts. Each account looks innocent alone; the fraud only shows up in the connections between them.
- Accounts are nodes, and so are the phones, UPI ids and devices they use. An edge means “this account used this phone”.
- Account A1 claims the new-user cashback. Alone, it looks normal.
- Following edges: A1 shares a phone number with A2.
- Continuing: A2 shares a UPI id with A3, and A3 shares a device with A4. Four “new” accounts belong to one ring, so all four claims are held. A5 is untouched.
- A graph database answers this with one traversal in milliseconds. In SQL each hop is another self-join on very large tables, which gets slow quickly as hops increase.
NoSQL in Indian products you use
1. Caches, OTPs and rate limits: key-value
Payment, food and shopping apps use Redis or similar stores for sessions, carts, OTPs with expiry and “max 5 attempts per minute” rate limits.
2. Product catalogues: document stores
Phones, sarees and groceries have completely different attributes. Shopping platforms store each product as a document with its own fields and nested variants.
3. Messages and IoT: wide-column
Chat histories, smart-meter readings and vehicle telemetry are huge append-only streams read by key and time. Wide-column stores such as Cassandra and HBase are built for this.
4. Fraud and recommendations: graphs
Banks and fintechs look for rings of accounts sharing devices and identifiers; e-commerce and social apps recommend “people also bought” from relationship graphs.
Four NoSQL models in plain Python
Each NoSQL family is simulated with ordinary Python data structures: a dictionary for key-value, nested dictionaries for documents, partitions of sorted rows for wide-column, and an adjacency list for a graph. Run it, then try the challenges: cache-aside, denormalisation, choosing a partition key, quorum consistency and graph hops.
# 1. Key-value: get/set by key only
kv = {"session:7f3a": "U9", "cart:U9": {"thali": 1, "lassi": 2}}
print("KV :", kv["cart:U9"])
# 2. Document: nested, flexible records, queried by fields
kitchens = [{"_id": "K11", "city": "Pune", "veg": True, "menu": ["thali", "lassi"]},
{"_id": "K12", "city": "Pune", "veg": False, "menu": ["biryani"], "halal": True}]
print("Document :", [k["_id"] for k in kitchens if k["city"] == "Pune" and k["veg"]])
# 3. Wide-column: partition by key, rows sorted inside the partition
pings = {"R77": [(1, "18.50,73.85"), (2, "18.51,73.86"), (3, "18.52,73.86")],
"R12": [(1, "19.07,72.87")]}
print("Wide-col :", "latest R77 ->", pings["R77"][-1])
# 4. Graph: nodes and relationships
friends = {"Riya": ["Kabir", "Meera"], "Kabir": ["Arjun"], "Meera": ["Zoya"]}
two_hops = [ff for f in friends["Riya"] for ff in friends.get(f, [])]
print("Graph :", "friends of Riya's friends ->", two_hops)print("KV :", kv["cart:U9"])Key-value stores answer one question very fast: “what is stored under this key?”{"_id": "K12", "city": "Pune", "veg": False, "menu": ["biryani"], "halal": True}]Documents can have different fields. K12 hashalal; K11 doesn't, and nothing breaks.print("Wide-col :", "latest R77 ->", pings["R77"][-1])All of R77's rows sit in one partition, already sorted, so “latest” is the last row.two_hops = [ff for f in friends["Riya"] for ff in friends.get(f, [])]Following relationships hop by hop is what graph databases are built for.
Run it on your PC: key-value, documents and a graph
Python's standard library includes a real on-disk key-value store (dbm) and SQLite with JSON functions. This program uses them to store carts by key with an expiry, query JSON kitchen documents by their fields, and find a fraud ring in a small graph. Save it as nosql_lab.py in an empty folder and run it.
NoSQL ideas with the standard library
"""Three NoSQL ideas using only Python's standard library:
1. a real key-value store on disk (dbm) used as a shopping-cart store
2. JSON documents queried with SQLite's JSON functions (a document-store style)
3. a small graph searched for a fraud ring"""
import dbm, json, sqlite3
from collections import deque
# ---- 1. Key-value: carts stored by key, with an expiry time
with dbm.open("carts_db", "c") as kv:
now = 1_000_000 # fixed clock so the output is repeatable
kv["cart:U9"] = json.dumps({"items": {"thali": 1, "lassi": 2}, "expires": now + 1800})
kv["cart:U12"] = json.dumps({"items": {"dosa": 1}, "expires": now - 5}) # already expired
for key in [b"cart:U9", b"cart:U12", b"cart:U99"]:
raw = kv.get(key)
if raw is None:
print(key.decode(), "-> not found")
continue
cart = json.loads(raw)
state = "expired" if cart["expires"] < now else "live"
print(key.decode(), "->", cart["items"], f"({state})")
# ---- 2. Documents: each kitchen is one JSON document with a nested menu
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE kitchens (doc TEXT)")
docs = [
{"_id": "K11", "name": "Annapurna", "city": "Pune", "veg": True,
"menu": [{"dish": "thali", "price": 220}, {"dish": "lassi", "price": 100}]},
{"_id": "K12", "name": "Spice Box", "city": "Pune", "veg": False,
"menu": [{"dish": "biryani", "price": 280}], "delivery_partner": "own riders"},
{"_id": "K13", "name": "Udupi Corner", "city": "Mumbai", "veg": True,
"menu": [{"dish": "dosa", "price": 150}, {"dish": "filter coffee", "price": 60}]},
]
db.executemany("INSERT INTO kitchens VALUES (?)", [(json.dumps(d),) for d in docs])
rows = db.execute("""
SELECT json_extract(doc, '$.name'), json_array_length(doc, '$.menu')
FROM kitchens
WHERE json_extract(doc, '$.city') = 'Pune' AND json_extract(doc, '$.veg') = 1
""").fetchall()
print("\nVeg kitchens in Pune (name, dishes on menu):", rows)
extra = db.execute("SELECT json_extract(doc, '$.name') FROM kitchens WHERE json_extract(doc, '$.delivery_partner') IS NOT NULL").fetchall()
print("Documents with an extra field nobody planned for:", extra)
# ---- 3. Graph: accounts linked by shared phones and UPI handles
edges = [("A1", "phone:98xx01"), ("A2", "phone:98xx01"), ("A2", "upi:x@ok"), ("A3", "upi:x@ok"),
("A3", "device:D7"), ("A4", "device:D7"), ("A5", "phone:77xx22")]
graph = {}
for a, b in edges:
graph.setdefault(a, set()).add(b)
graph.setdefault(b, set()).add(a)
start, seen, queue = "A1", {"A1"}, deque(["A1"])
while queue:
node = queue.popleft()
for nxt in graph[node]:
if nxt not in seen:
seen.add(nxt)
queue.append(nxt)
ring = sorted(n for n in seen if n.startswith("A"))
print("\nAccounts connected to A1 through shared phones, UPI ids or devices:", ring)
# In a terminal: python nosql_lab.pyRecorded from a real run with Python 3.13; any Python 3.9+ gives the same output. The program creates carts_db files (their exact names depend on your operating system). To try the real products, install Redis or MongoDB Community, or use their free cloud tiers, and repeat the same three ideas.
Cache-aside, one line at a time
Three requests arrive for kitchen pages. Watch the first request for each kitchen miss the cache and read the database, and the repeat request hit the cache.
Worked examples: real problems, solved and traced in memory
Four NoSQL design tasks, from choosing a family to finding a ring in a graph. Each runs live and is traced.
Which database family for each feature?
Problem statement
Pick a database family for each feature using its main access pattern.
payments, user sessions, product catalogue, chat history, friend suggestions
payments -> relational (SQL) user sessions -> key-value product catalogue -> document chat history -> wide-column friend suggestions -> graph
Approach
- Write each feature's access pattern.
- Map patterns to families with a dictionary.
- Print the choice.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Embed or reference?
Problem statement
For each child collection, decide whether to embed it inside the parent document or keep it separate: embed if it is always read with the parent and stays small (under 100 items); otherwise reference it.
menu (40 items, read together), reviews (grows forever), addresses (3, read together), orders (grows forever)
menu : embed (small, read with parent) reviews : reference (large or read separately) addresses: embed (small, read with parent) orders : reference (large or read separately)
Approach
- Describe each child with its expected size and whether it is read with the parent.
- Apply the rule.
- Print the decision and why.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Measure partition balance for two key designs
Problem statement
Messages are partitioned either by group_id or by (group_id, month). Compute the number of partitions and the largest partition for each design.
3 groups with 1,200, 90 and 30 messages over 6 months
group : 3 partitions, largest 1200 messages group+month : 18 partitions, largest 200 messages
Approach
- Start from the number of messages per group (spread evenly over 6 months).
- Compute partition sizes for each candidate key.
- Compare the largest partition: smaller is safer.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Find connected groups of accounts
Problem statement
Accounts are linked when they share an identifier. Find every connected group (ring) with more than one account.
5 accounts; pairs that share a phone or device
Rings: [['A1', 'A2'], ['A3', 'A4', 'A5']]
Approach
- Build an undirected adjacency list from the pairs of accounts that share an identifier.
- For each unvisited account, walk the graph to collect its whole group.
- Every group found this way has at least two accounts: a possible ring.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Check yourself
Quick revision
Lesson 2.4 in one screen
- NoSQL = not only SQL: non-relational models, flexible schemas, horizontal scaling.
- Key-value (Redis, DynamoDB) · Document (MongoDB) · Wide-column (Cassandra, HBase) · Graph (Neo4j).
- SQL: fixed schema, joins, ACID, vertical scaling. NoSQL: flexible, denormalised, horizontal, often BASE/tunable.
- Model from queries: denormalise; embed small, bounded children; reference unbounded ones.
- Partition key spreads data and load; avoid hot partitions; clustering key sorts rows inside.
- Cache-aside: check cache → on miss read DB and store with TTL → invalidate on change.
- Quorum: R + W > N for read-your-latest-write. LSM trees for heavy writes. Polyglot persistence = right store per job.
Interview and viva questions
What are the types of NoSQL databases? Give examples.
Key-value stores (Redis, Amazon DynamoDB, Memcached) map unique keys to values for very fast look-ups, used for caches, sessions and carts. Document stores (MongoDB, Couchbase, Firestore) hold JSON-like documents with nested, flexible fields, used for catalogues and profiles. Wide-column stores (Cassandra, HBase, Bigtable) organise rows into partitions sorted by clustering keys across many nodes, used for time series, logs and messaging. Graph databases (Neo4j, Amazon Neptune) store nodes and relationships, used for fraud detection, recommendations and social networks.
Differentiate SQL and NoSQL databases.
SQL databases use tables with predefined schemas, support joins and ACID transactions, and typically scale vertically with read replicas. NoSQL databases use key-value, document, wide-column or graph models, allow flexible schemas, scale horizontally, denormalise data around access patterns and often offer eventual or tunable consistency. SQL suits transactional systems and ad-hoc analysis; NoSQL suits very high scale, flexible data and specific access patterns.
What is the CAP-related trade-off in NoSQL databases?
In a distributed database, when the network partitions, a system must choose between consistency (every read sees the latest write) and availability (every request gets a response). Many NoSQL systems, such as Cassandra, favour availability with tunable consistency, while others, such as HBase, favour consistency. Lesson 3.4 explains the CAP theorem in detail.
When should you not use NoSQL?
When you need complex multi-table transactions (payments, accounting), rich ad-hoc queries and joins across many entities, or strong consistency everywhere without careful design, and when the data comfortably fits a single well-indexed relational database. Adding a NoSQL store also adds operational complexity.
What is polyglot persistence?
Using several kinds of data stores in one system, each chosen for a specific workload: for example a relational database for orders and payments, Redis for caching and sessions, a document store for catalogues, a wide-column store for event history, and a search engine for text search.
Practice problems
NoSQL modelling and access-pattern exercises. The last two are business tasks.
Get with a default
Read a key from a key-value dictionary; print “not found” if it is missing.
keys cart:U9, cart:U99
cart:U9 -> {'thali': 1}
cart:U99 -> not foundShow solution and explanation
kv = {"cart:U9": {"thali": 1}}
for k in ["cart:U9", "cart:U99"]:
print(k, "->", kv.get(k, "not found"))How it works: get with a default avoids an error for missing keys.
Query documents by a field
Print the names of kitchens in Pune.
3 documents
['Annapurna', 'Spice Box']
Show solution and explanation
docs = [{"name": "Annapurna", "city": "Pune"}, {"name": "Udupi", "city": "Mumbai"}, {"name": "Spice Box", "city": "Pune"}]
print([d["name"] for d in docs if d["city"] == "Pune"])How it works: Document stores filter and index by fields inside the documents.
Expired keys
Each key has an expiry time. At time 100, print the keys still alive.
otp1 exp 90, otp2 exp 120, cart exp 300
['otp2', 'cart']
Show solution and explanation
expiry = {"otp1": 90, "otp2": 120, "cart": 300}
now = 100
print([k for k, exp in expiry.items() if exp > now])How it works: This is what a TTL does automatically in Redis or Cassandra.
Latest rows from a partition
Pings are stored per rider. Print the 2 most recent pings of R7.
R7: t=1,2,3,4
[(4, 'D'), (3, 'C')]
Show solution and explanation
pings = {"R7": [(1, "A"), (2, "B"), (3, "C"), (4, "D")], "R9": [(1, "X")]}
print(sorted(pings["R7"], reverse=True)[:2])How it works: In Cassandra, clustering by time descending makes this a cheap read of the first rows.
Friends of friends
Print people two hops from Riya who are not already her friends.
small friend graph
['Arjun', 'Zoya']
Show solution and explanation
g = {"Riya": ["Kabir", "Meera"], "Kabir": ["Arjun", "Meera"], "Meera": ["Zoya"]}
direct = set(g["Riya"])
two = {ff for f in direct for ff in g.get(f, [])} - direct - {"Riya"}
print(sorted(two))How it works: Set operations remove existing friends and Riya herself.
Hit ratio needed
The database handles at most 3,000 reads per second. Traffic is 2,00,000 reads per second. What minimum cache hit ratio is needed?
3,000 vs 2,00,000
Minimum hit ratio: 98.5%
Show solution and explanation
db_max, traffic = 3000, 200000
needed = 1 - db_max / traffic
print(f"Minimum hit ratio: {needed * 100:.1f}%")How it works: Only misses reach the database, so misses must stay at or below 3,000 per second.
Quorum settings
For N = 5 replicas, print every (W, R) pair with R + W > N where both are between 1 and 5, and the smallest such R for W = 3.
N = 5
15 valid pairs smallest R for W=3: 3
Show solution and explanation
N = 5
pairs = [(w, r) for w in range(1, 6) for r in range(1, 6) if r + w > N]
print(len(pairs), "valid pairs")
print("smallest R for W=3:", min(r for w, r in pairs if w == 3))How it works: Stronger writes (bigger W) allow lighter reads (smaller R), and the other way round.
Redis memory for sessions
5 crore daily users, 30% active at peak, each session is 2 KB. How many GB of cache memory are needed (add 30% overhead)?
5 crore, 30%, 2 KB, +30%
Memory needed: 37 GB
Show solution and explanation
users = 5_00_00_000
active = users * 0.30
gb = active * 2 / 1024 / 1024 * 1.3
print(f"Memory needed: {gb:.0f} GB")How it works: Capacity planning for an in-memory store: active keys × size × overhead.
Cache saving
Each database read costs Rs 0.002 in compute; a cache read costs Rs 0.0001. With 1 crore reads a day and a 95% hit ratio, print the daily cost with and without the cache.
1 crore reads, 95% hits
Without cache: Rs 20000 With cache : Rs 1950
Show solution and explanation
reads = 1_00_00_000
no_cache = reads * 0.002
with_cache = reads * 0.95 * 0.0001 + reads * 0.05 * 0.002
print("Without cache: Rs", round(no_cache))
print("With cache : Rs", round(with_cache))How it works: A cache cuts both latency and cost, as long as the data can tolerate a short staleness. (Costs are illustrative.)
Real-world scenario questions (asked in company interviews)
NoSQL design questions from real interviews and projects. Decide your answer first, then open the model answer.
SQL or NoSQL for a new feature?
A ride-hailing app wants to store (a) payments, (b) the live location of every driver, updated every 4 seconds, (c) driver profiles with optional documents, and (d) “riders who travel similar routes”. Which store for each?
The interviewer is checking: can you justify a database choice from access patterns?
Show a strong answer
- Payments: relational (PostgreSQL/MySQL): ACID transactions.
- Live locations: key-value (Redis) keyed by driver for the current position, plus a wide-column store (Cassandra) partitioned by driver and day for history.
- Driver profiles: document store (MongoDB): flexible optional fields.
- Similar routes: a graph or batch analytics in the lake, depending on how real-time it must be.
Then mention the cost of running several systems and keeping them consistent.
Avoid: choosing NoSQL because it is “faster” or “modern”, or MongoDB for money transfers without discussing transactions.
A Cassandra table keeps timing out
A messages table uses PRIMARY KEY (group_id, sent_at). A few huge groups now time out on reads, and their nodes run hot.
The interviewer is checking: do you understand partition sizing and hot partitions?
Show a strong answer
Huge groups create unbounded partitions that live on one set of replicas. Re-key to bound partition size, for example PRIMARY KEY ((group_id, month), sent_at) or add a bucket number for very active groups. Read the latest bucket first and page backwards. Monitor partition sizes and set alerts on large partitions. Migrate by dual-writing to the new table and backfilling.
Avoid: adding more nodes, which doesn’t split a single hot partition.
Customers see old prices
After adding a Redis cache in front of the product database, some customers see yesterday’s prices for hours.
The interviewer is checking: do you know cache invalidation and TTL trade-offs?
Show a strong answer
- Set a sensible TTL so stale entries expire (minutes, not days).
- On every price update, delete or update the cache key in the same code path (cache-aside invalidation), or publish a change event that cache updaters consume.
- For the final checkout price, always read from the source database.
- Monitor hit ratio and staleness.
Avoid: disabling the cache entirely, or caching prices with no expiry.
“What is the difference between SQL and NoSQL?”
A standard opening question in data engineering interviews.
The interviewer is checking: can you compare them fairly, with examples and trade-offs?
Show a strong answer
SQL databases store data in tables with fixed schemas, support joins and strong ACID transactions, and usually scale vertically (plus read replicas). NoSQL databases use other models (key-value, document, wide-column, graph), allow flexible schemas, scale horizontally across many nodes, and are designed around specific access patterns, often with relaxed or tunable consistency. Use SQL for transactional systems and complex ad-hoc queries; use NoSQL for very high scale, flexible records, low-latency key look-ups, time series or relationship-heavy queries. Many systems use both.
Avoid: saying NoSQL has no schema at all, or that SQL can’t scale.