NoSQL, MongoDB & Spring Data
Where relational databases start to hurt, what NoSQL actually means, the shift from tables to documents, why MongoDB specifically, CRUD against a collection, and the Spring Data MongoDB layer. Includes interview Q&A.
Assumes you know
After this you should be able to
- Compare document and relational models
- Use Spring Data MongoDB
Java 301 · Session 5 — Databases
Why relational tables stop being the right tool at scale, what a document database does instead, and how to talk to MongoDB — from the raw shell all the way up to Spring Data repositories in a QEats-style service.
Choose your route#
Study the same NoSQL, MongoDB & Spring Data explanation with a different purpose: build and verify it in Learn deeply, or retrieve and defend it in Prepare for interviews.
- Compare document and relational models
- Use Spring Data MongoDB
Shared core#
The chapter below remains the canonical explanation for both goals. Keep its model, examples, and trade-offs shared; only the deliberate practice changes with the route.
Learn deeply: build evidence#
Use this chapter’s TaskFlow work to make one small, observable change. Run the stated acceptance checks before recording practice.
Prepare for interviews: explain the trade-off#
Without notes, explain the boundary this chapter protects, one concrete implementation decision, and the cost of a tempting shortcut. Then answer one follow-up using the chapter’s own example.
SQL forces you to design a rigid schema up front and stitch data back together with JOINs; MongoDB stores each entity as one self-contained document so you read it in a single lookup, change its shape without a migration, and spread it across machines by default. You trade guaranteed cross-table consistency for flexibility, read speed, and horizontal scale.
Notebook Summary#
Hand-copy this. Terse rules and the boxed markers; the full reasoning is in Section 2.
Why not SQL (at scale)?#
- Scalability → data split across many related tables → horizontal scaling hard
- Flexibility → fixed schema → hours designing up front, painful to change
- Performance → complex JOINs = CPU-heavy → need indexes (cost = write speed + space)
What is NoSQL?#
- Non-relational · schema-less · big serialized-object store
- No JOINs → no complicated multi-table queries
- Distributed out of the box (scale-out is the default)
4 families →
- Key-Value → Redis, Memcached
- Column → BigTable, Cassandra, HBase
- Document → MongoDB, CouchDB, Riak
- Graph → Neo4j
doodle: 4 boxes labelled KV / COL / DOC / GRAPH; circle DOC.
SQL ↔ Mongo term map#
| SQL | MongoDB |
|---|---|
| Database | Database |
| Table | Collection |
| Row | Document (BSON/JSON) |
| Column | Field |
| JOIN | Embed / $lookup |
| Primary key | _id |
Why MongoDB#
- Document = key–value pairs (JSON-shaped, stored as BSON)
- Collection = set of documents ≈ a table (but no fixed columns)
- Related data embedded in one doc → one read, no JOIN
doodle: one box "product { name, price, reviews[…] }" — reviews nested inside.
CRUD verbs#
| Op | Mongo |
|---|---|
| Create | insertOne / insertMany |
| Read | find / findOne |
| Update | updateOne + $set |
| Delete | deleteOne / deleteMany |
- Query = filter document →
find({ price: { $gt: 500 } }) - Operators start with $ → $gt $lt $in $set
- Empty filter
find({})→ returns all
Spring Data MongoDB#
- @Document → class = collection; @Id → _id field
- Extend MongoRepository<T, ID> → CRUD for free
- Derived queries →
findByPriceGreaterThan(500)(method name = query)
Markers to box#
- ⭐ Model by access pattern, not by entities — embed what you read together.
- ⚠
updatewithout$setreplaces the whole document. - ⚠ Schema-less ≠ no design — bad shape = duplicated data you must keep in sync.
- 💡 No JOINs is a feature: it's what lets Mongo shard across machines.
Section 2
Detailed Study Notes#
You are building the product catalog for QEats — an app that lists restaurants and dishes. You start where
every backend course starts: a relational database. A restaurants table, a
menu_items table, an attributes table, a reviews table, all wired
together with foreign keys. Rendering one restaurant page means joining five tables. Then the product team
says: "some restaurants have opening hours per weekday, some have a single flat time, and a few now have
delivery zones." Your neat schema does not have a column for that, and adding one means an
ALTER TABLE on a table with millions of rows. This is the moment the relational model starts
fighting you — and the reason document databases exist.
2.1 Where relational databases start to hurt#
A relational database (SQL) stores data as rows in fixed-column tables and rebuilds related data on demand by joining tables together. That model is excellent for well-understood, highly structured data with strong consistency needs — banking, inventory, anything where a row must never be half-written. It stops being comfortable in three specific ways, and the lesson named all three.
Hard to scale out#
One entity is spread across many related tables. Splitting that across machines (horizontal scaling) means your JOINs now cross the network — very difficult to do well.
Rigid schema#
The schema is fixed and defined up front. You spend hours designing it before writing code, and changing it later — when requirements shift — is painful.
Costly JOINs#
Complex joins are CPU-intensive. You add indexes at many levels to stay fast, but each index costs write speed and storage.
A relational database is a filing cabinet with fixed dividers. Every document must be torn apart and filed by type — names here, addresses there, phone numbers in a third drawer. To read one person back you open every drawer and reassemble them (that reassembly is the JOIN). Add a new field for everyone and you must re-cut every divider.
The Amazon-products test [+]#
The session ran a thought experiment: look at two very different Amazon listings — a running shoe and a
smartphone. The shoe has size, color, material. The phone has RAM, storage, screen size, battery. Will
one fixed RDBMS schema hold both? Either you create a wide table full of columns that are
NULL for most products, or you build an "attributes" side-table and JOIN it back for every
read. Both are awkward. This is the classic case where each product is naturally one flexible
document with whatever fields it needs — and nothing more.
"SQL vs NoSQL" is rarely about which is better — it is about fit. Structured data with strict consistency (payments) → relational. Varied, evolving, high-volume, read-heavy data (product catalogs, activity feeds, user profiles) → document. Saying "it depends on the access pattern" is the answer interviewers want.
2.2 What NoSQL actually means#
NoSQL is a family of non-relational databases that drop the fixed-table, JOIN-based model in favor of flexible, self-contained records that are easy to distribute across many machines. "NoSQL" is best read as "not only SQL" — not "SQL is forbidden," but "the relational model is no longer the only option." The shared traits the lesson emphasized:
- Non-relational — data is not forced into rows-and-columns with foreign keys.
- Schema-less — records in the same collection can have different fields; you are not locked to one shape.
- Basically a large serialized-object store — you save an object roughly as-is, instead of shredding it across tables.
- No JOINs, so no complicated multi-table queries — you fetch a record whole.
- Distributed out of the box — scaling across machines is the default assumption, not an afterthought.
Dropping JOINs looks like a limitation, but it is the enabling feature. A JOIN needs all the related rows reachable together; the moment you split a table across machines, JOINs get slow or impossible. Because a document holds its related data inside itself, any single machine can answer a read alone — which is exactly what lets Mongo shard (spread data across servers) cleanly.
The four families#
NoSQL is an umbrella. The lesson grouped it into four types by how a record is shaped. MongoDB lives in the document family.
| Family | Record shape | Good for | Examples |
|---|---|---|---|
| Key–Value | key → opaque value | caches, sessions | Redis, Memcached |
| Column | wide, sparse rows | huge write-heavy tables | Cassandra, HBase, BigTable |
| Document | JSON-like documents | catalogs, profiles, content | MongoDB, CouchDB, Riak |
| Graph | nodes + relationships | networks, recommendations | Neo4j |
2.3 The mental model shift: tables → documents#
MongoDB stores data as documents — JSON-shaped bundles of key–value pairs — grouped into collections, where a collection plays the role a table played in SQL. The single most useful thing you can carry out of this lesson is the vocabulary map between the two worlds, because every Mongo concept has a relational cousin.
| Relational (SQL) | MongoDB | What it is |
|---|---|---|
| Database | Database | Top-level container |
| Table | Collection | A group of similar records |
| Row / record | Document | One entity, stored whole |
| Column | Field | One key inside a document |
| Primary key | _id | Unique id, auto-added by Mongo |
| JOIN | Embed / $lookup | Relate data — usually by nesting it |
Two ideas deserve their own paragraph. First, a document is just key–value pairs — the same JSON you already send over HTTP. Mongo actually stores it as BSON (Binary JSON), a compact binary form that adds types like dates and 64-bit ints, but you read and write it as JSON. Second, a collection holds a set of documents and behaves like a table — except it does not impose fixed columns, so two documents in the same collection can legitimately have different fields.
"Schema-less" tempts beginners into skipping data modeling entirely. But if you embed the same data in many documents (a restaurant's name copied into every order), you now own the job of keeping every copy in sync — the database will not do it for you. You still design; you just design around how you read the data instead of around normalization.
2.4 Why MongoDB specifically#
MongoDB is the leading document database: it stores each entity as one JSON-shaped document, lets related data live nested inside that document, and distributes collections across machines by default. Restating the two building blocks the session drilled:
- Documents consist of key–value pairs. The unit of storage is one JSON object.
- Collections contain sets of documents and are almost equivalent to relational tables — but without a fixed column list forced on every row.
The practical payoff: for a read-heavy catalog, you fetch a whole product — attributes, reviews, images — in
a single query against a single collection. No join planner, no five-table read path, and when the shoe
needs a material field the phone will never have, you simply add it to the shoe documents.
In SQL you model by entities and normalize. In Mongo you model by access pattern: ask "what do I read together, in one screen?" and embed exactly that into one document. Data read together should be stored together. This one reframing is what makes Mongo fast and what most SQL-trained engineers get wrong first.
2.5 CRUD — talking to Mongo directly#
CRUD is the four things you ever do to stored data — Create, Read, Update, Delete — and Mongo gives each a method you call on a collection. Before any framework hides it, it is worth seeing the raw shell commands, because Spring Data is only a thin wrapper over exactly these.
// CREATE — insert one document (Mongo adds _id automatically)
db.products.insertOne({ name: "Shoe", price: 2499, color: "red" })
// CREATE many at once
db.products.insertMany([
{ name: "Phone", price: 10999, ram: 6 },
{ name: "Cap", price: 399 }
])
// READ — filter is itself a document; {} matches everything
db.products.find({ color: "red" })
db.products.findOne({ name: "Phone" })
// READ with an operator — price greater than 500
db.products.find({ price: { $gt: 500 } })
// UPDATE — $set changes only the named fields
db.products.updateOne(
{ name: "Shoe" },
{ $set: { price: 1999 } }
)
// DELETE
db.products.deleteOne({ name: "Cap" })Two patterns run through all of it. A query is just a document describing what to match —
{ color: "red" } means "field color equals red." And query operators start with
$: $gt greater-than, $lt less-than, $in
in-a-list, $set set-fields. Once you see that filters and updates are themselves documents, the
whole query language stops feeling like new syntax.
A Mongo query is a "wanted" poster. You hand the collection a partial description — "red, priced over 500" — and it returns every document that fits the description. The more fields on the poster, the narrower the match.
If you write updateOne({name:"Shoe"}, {price:1999}) — the second argument without
$set — Mongo treats it as a full replacement and the resulting document becomes just
{price:1999}, silently dropping name and every other field. Always reach for
an update operator ($set, $inc, $push) unless you truly mean to
replace the whole document.
2.6 Spring Data MongoDB — the Java layer#
Spring Data MongoDB lets you work with documents as plain Java objects: you annotate a class, extend a repository interface, and Spring generates the CRUD and query code for you. This is how QEats actually talks to Mongo — you almost never write shell commands in the app. Three pieces do all the work.
Class = collection#
Marks a POJO as a Mongo document. Each instance becomes one document in the named collection.
CRUD for free#
Extend it and save, findById, findAll, delete
already exist — no implementation.
@Document(collection = "products")
public class Product {
@Id
private String id; // maps to Mongo's _id
private String name;
private int price;
private String color;
// constructors, getters, setters …
}public interface ProductRepository extends MongoRepository<Product, String> {
// Derived query — Spring parses the METHOD NAME into a query.
// This becomes: find({ color: ? })
List<Product> findByColor(String color);
// Becomes: find({ price: { $gt: ? } })
List<Product> findByPriceGreaterThan(int price);
}Spring reads the repository method name and derives the query from it. findByColor →
filter on color; findByPriceGreaterThan → {price: {$gt: …}};
findByColorAndPriceLessThan → both. You write a name, Spring writes the query. Get the
field names exactly right — a typo like findBycolour fails at startup, not at call time
(which is actually the good outcome: fail fast).
@Service
public class ProductService {
private final ProductRepository repo;
public ProductService(ProductRepository repo) {
this.repo = repo; // constructor injection
}
public Product add(Product p) {
return repo.save(p); // CREATE (and UPDATE if id exists)
}
public List<Product> redProducts() {
return repo.findByColor("red"); // derived query
}
}The layered path a QEats request takes down to the document store.
repo.save(p) inserts when the object has no id and updates when it does. There
is no separate "insert vs update" call at this layer — Spring decides based on whether the
_id already exists. This is the "upsert" behavior worth knowing for interviews.
Section 2 · practice
Interview & Practice Q&A#
The follow-ups this material invites. Try to answer aloud before expanding.
Q1When would you pick SQL over MongoDB?
When the data is highly structured and relationships are central, and — most importantly — when you need strong, multi-record consistency and transactions, such as financial ledgers or inventory. Relational databases enforce a schema and guarantee that related rows stay consistent, which is exactly what those domains require. Reach for Mongo when data is varied, evolving, read-heavy, and needs to scale horizontally.
Q2Why does "no JOINs" actually help rather than hurt?
Because a JOIN needs all related rows reachable together, it becomes slow or impossible once a table is split across machines. Documents carry their related data inside themselves, so any single machine can satisfy a read on its own. That self-containment is precisely what lets MongoDB shard — spread a collection across many servers — without cross-machine coordination on every query.
Q3What is the difference between a document and a collection?
A document is a single record — a JSON-shaped bundle of key–value pairs representing one entity, stored physically as BSON. A collection is a group of documents, playing the role a table plays in SQL. The key difference from a table is that a collection imposes no fixed column list, so documents within it can have different fields.
Q4How do you model a one-to-many relationship in Mongo?
Two options. Embed the "many" as an array inside the "one" document (reviews nested inside a
product) — best when you always read them together and the array stays bounded. Or reference
by storing ids and resolving them separately (or with $lookup) — better when the "many"
is large, shared, or changes independently. The choice follows the access pattern, not a rule.
Q5What does "schema-less" really mean, and what's the catch?
It means the database does not enforce a fixed shape, so you can add or omit fields per document without a migration. The catch is that all responsibility for consistency moves to you: if you embed duplicated data across documents, keeping every copy in sync is now your application's job. Schema-less removes the migration pain but not the need to design.
Q6In Spring Data, how does findByPriceGreaterThan become a
query?
Spring Data parses the method name at startup: findBy starts a query, Price
names the field, and GreaterThan is the operator, producing
{ price: { $gt: ? } } with the argument bound in. You write no body. A misspelled field
name fails at application startup rather than at runtime — fast feedback that the query is wrong.
Q7What do @Document and @Id do?
@Document marks a Java class as a Mongo document and names the collection it maps to.
@Id marks the field that maps to Mongo's primary key _id. Together they
let Spring convert between your POJO and the stored BSON automatically.
Q8Why is forgetting $set on an update dangerous?
An update document without an operator is treated as a full replacement: the matched document is
replaced by exactly what you passed, so every field you did not include is dropped. Using
$set (or $inc, $push) tells Mongo to modify only the named
fields and leave the rest intact.
Q9What is BSON and why not just store JSON?
BSON is Binary JSON — the on-disk format Mongo uses. It keeps JSON's shape but adds a length-prefixed binary encoding for fast traversal and richer types like dates, 64-bit integers, and binary blobs that plain JSON lacks. You still think and query in JSON; BSON is the efficient representation underneath.
Key Takeaways#
- SQL hurts at scale on three axes: hard horizontal scaling, rigid schema, costly JOINs.
- NoSQL = non-relational, schema-less, no JOINs, distributed by default; four families, Mongo is document.
- Term map: table→collection, row→document, column→field, PK→
_id, JOIN→embed. - Model by access pattern — store together what you read together.
- A query is a document; operators start with
$;$setupdates only named fields. - Spring Data:
@Document+@Id+MongoRepository= CRUD and derived queries for free.
Section 3
Microexercises#
Reading proves you recognize it; these prove you can use it. Attempt each before opening the walkthrough.
3.1 Build-it — a Spring Data repository query#
Given the Product entity below, add repository methods so QEats can:
- Find all products of a given
color. - Find all products cheaper than a given price.
- Find all products of a given color that are also under a given price.
Write no method bodies — use Spring Data derived query names only. Expected: calling
findByColorAndPriceLessThan("red", 1000) returns every red product with
price < 1000.
@Document(collection = "products")
public class Product {
@Id private String id;
private String name;
private int price;
private String color;
// getters / setters …
}
public interface ProductRepository extends MongoRepository<Product, String> {
// TODO: add the three derived queries
}Show the full walkthrough
Step 1 — recall the grammar. A derived query name is findBy +
<Field> + optional <Operator>, joined with
And/Or. The field names must match the entity's fields exactly (case
matters): color and price.
Step 2 — map each requirement to a name. "of a given color" is a plain equality →
findByColor. "cheaper than" is the less-than operator →
findByPriceLessThan. "color and under a price" chains both with And →
findByColorAndPriceLessThan.
Step 3 — write the interface. Each method's parameters line up, in order, with the fields named in the method:
public interface ProductRepository extends MongoRepository<Product, String> {
List<Product> findByColor(String color);
List<Product> findByPriceLessThan(int price);
List<Product> findByColorAndPriceLessThan(String color, int price);
}Why — the method name is the query. You never implement these; Spring parses each
name into a Mongo filter (findByPriceLessThan → {price:{$lt:?}}) and
binds the arguments by position, so the whole data-access layer is declarative.
3.2 Diagnose-it — the vanishing fields#
A teammate runs the shell commands below against a fresh products collection, then
queries the document and is shocked. Predict the exact result of the final
findOne, name the root cause, and fix the update so price changes while
everything else survives.
db.products.insertOne({ name: "Shoe", price: 2499, color: "red" })
db.products.updateOne(
{ name: "Shoe" },
{ price: 1999 } // meant to just lower the price
)
db.products.findOne({ name: "Shoe" }) // what comes back?Show the full walkthrough
Predict the failure first. The final findOne({ name: "Shoe" }) returns
nothing — null. And a findOne({}) would show the document is
now just { _id: …, price: 1999 }. The name and color
fields are gone.
Root cause — replacement, not modification. The second argument to updateOne
has no update operator. When the update document contains no $-operator, Mongo
treats it as a full replacement: it swaps the matched document for exactly
{ price: 1999 } (keeping only _id). Since name no longer
exists, the later filter on name: "Shoe" matches nothing.
The fix — wrap the change in $set. $set tells Mongo to modify
only the named fields and leave the rest untouched:
db.products.updateOne(
{ name: "Shoe" },
{ $set: { price: 1999 } }
)
// now findOne({ name: "Shoe" }) returns:
// { _id: …, name: "Shoe", price: 1999, color: "red" }Why — in Mongo an update is a document too, and an update document without an operator
means "replace me with this." $set is the difference between editing a record and
overwriting it — the single most common Mongo write bug.