Warming up the neural circuits...
By the end of this chapter you will:
MongoDB is the most popular NoSQL database — and the most misunderstood. Used correctly, it's a rocket ship for rapid development with flexible schemas. Used incorrectly, it's a data corruption machine with no guardrails. This chapter teaches you which side of the line you're on.
In PostgreSQL, you have spreadsheets — tables with rigid columns, every row must match. In MongoDB, you have a filing cabinet. Each folder (document) can contain completely different papers inside. One folder might have a resume, a photo, and a handwritten note. Another might just have a sticky note.
This flexibility is MongoDB's superpower — and its greatest danger. When you don't know the shape of your data upfront, MongoDB lets you start now and figure it out later. But if you do know the shape, that flexibility becomes a liability — no type checking, no constraints, data corruption waiting to happen.
MongoDB uses different words for the same concepts. Translating between and MongoDB is the first thing to master:
| SQL (PostgreSQL) | MongoDB | Meaning |
|---|---|---|
| Database | Database | Same — a container for collections |
| Table | Collection | A group of documents (analogous to rows) |
| Row | Document | A single BSON (Binary JSON) record |
| Column | Field | A key-value pair inside a document |
| Primary Key | _id field | Every document has one, auto-generated ObjectId |
| Index | Same concept, different implementation | |
| JOIN | $lookup (in aggregation) | Combines documents from different collections |
| Session / Multi-Document Transaction | ACID since MongoDB 4.0 (2018) |
A MongoDB document is JSON-like, but stored as BSON (Binary JSON) — a binary format that extends JSON with additional types:
{
"_id": ObjectId("6645a1b2c3d4e5f6a7b8c9d0"),
"name": "Alice Chen",
"email": "alice@example.com",
"age": 34,
"joinedAt": ISODate(
BSON adds types JSON lacks: Date, ObjectId, Binary, Decimal128 (exact decimals for money), Timestamp, Long (64-bit integer). When you use MongoDB from Node.js, the driver handles BSON ↔ conversion automatically.
The single most important design decision in MongoDB. Do you embed child data inside the parent document, or store it separately with a reference?
// EMBEDDING — comments live inside the post
// Best when: comments are always accessed with the post, max ~100 per post
{
_id: ObjectId("..."),
title: "My Post",
body: "...",
comments: [
{ user: "alice", text:
| Factor | Embed | Reference |
|---|---|---|
| Read speed | 1 query (fast) | 2+ queries (slower, but use $lookup) |
| Write contention | High (updating one comment writes the whole document) | Low (each comment is a separate document) |
| Max size | MongoDB documents max at 16 MB | No limit (each comment is its own document) |
| Query flexibility | Can only filter comments inside a single post | Can search all comments across all posts |
| Data that changes independently | Bad fit | Good fit |
Embed when: the child is always accessed with the parent, never independently, and won't exceed 16 MB.
Reference when: the child is queried independently, can exceed 100 per parent, or gets updated independently.
Both when: you embed a summary (last 3 comments, total comment count) and reference the full data (all comments in their own collection). This is called the Extended Reference Pattern and it's the most common production pattern.
MongoDB's aggregation pipeline is the NoSQL equivalent of SQL — but instead of writing a statement, you build a pipeline of stages. Each stage transforms documents and passes them to the next stage.
// SQL:
// SELECT status, COUNT(*) as count, AVG(total) as avgTotal
// FROM orders
// WHERE createdAt > '2025-01-01'
// GROUP BY status
// HAVING COUNT(*) > 5
// ORDER BY count DESC
// MongoDB aggregation pipeline:
db.orders.aggregate([
//
Key aggregation stages:
| Stage | What it does | SQL equivalent |
|---|---|---|
$match | Filter documents | WHERE |
$group | Group and aggregate | GROUP BY |
$sort | Order results | ORDER BY |
$project | Reshape documents (include/exclude fields, compute new ones) | SELECT |
$limit / $skip | LIMIT / OFFSET | |
$lookup | Join with another collection | JOIN |
$unwind | Expand an array into multiple documents |
MongoDB optimizes by pushing $match stages as early as possible — but only if you place them early. A $match at stage 5 filters 10 documents instead of 10 million. Always put $match and $limit as early in the pipeline as possible. Test with .explain("executionStats") to see how many documents each stage processes.
MongoDB lets you trade consistency for speed. You set this per operation:
Write concern — how many nodes must confirm a write before returning:
// Fast but risky: write to primary only, don't wait for journal
db.orders.insertOne(doc, { writeConcern: { w: 1, j: false } });
// Safe: write must reach primary + 2 secondaries, journaled
db.orders.insertOne(docw: 1 — primary only (fast, data lost if primary crashes before replication)w: "majority" — primary + enough secondaries to have majority (safe, slower)j: true — write must be in the journal on disk (survives power loss)Read concern — what data can be read:
local — read whatever is on this node, even uncommitted data (fast, might read rolled-back data)majority — only read data confirmed by a majority of nodes (safe, might be slightly stale)linearizable — guarantee that you read the absolute latest write (slowest, one-node-at-a-time)For most web apps: w: "majority" on writes, readConcern: "local" on reads from primary, readConcern: "majority" on reads from secondaries.
Uber's architecture is a fascinating mix. They use PostgreSQL for high-consistency data (payments, trips, accounting) and MongoDB for high-flexibility data:
Rider preferences — every rider has custom settings. Some want "quiet mode," some want preferred music, some want specific pickup instructions. The schema varies wildly per city, per rider type. MongoDB's flexible schema is perfect here.
City configurations — Uber operates in 10,000+ cities, each with different pricing rules, surge multipliers, service types (UberX, Black, Pool). One document per city, each with a different shape. PostgreSQL would need hundreds of nullable columns or an EAV pattern (entity--value — slow and painful to query).
Real-time event logging — every ride generates hundreds of events (driver accepted, rider picked up, GPS pings, drop-off). MongoDB's high write throughput handles millions of inserts per second across sharded clusters.
Uber moved away from MongoDB for their core trip data in 2016 (to a custom schemaless datastore built on MySQL), which teaches an important lesson: MongoDB excels at flexible, read-heavy, or write-heavy workloads with variable schema. It's weaker at highly relational data that requires complex transactions across multiple entities.
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Using MongoDB for everything because "it's web scale" | MongoDB is terrible at relational data — multi-collection JOINs ($lookup) are slow and limited | Use MongoDB for flexible-schema, document-shaped data. Use PostgreSQL for relational data. Many companies use both. |
| Embedding unbounded arrays | A document with 50,000 comments grows past 16 MB and throws errors on every write | Reference large or unbounded children. Use the Extended Reference Pattern. |
| No indexes, ever | MongoDB's _id index doesn't help queries on email or userId — every query becomes a collection scan | Index every field you query by. Use explain() to verify. |
Using $lookup everywhere | $lookup is not a SQL JOIN — it's slower, has no query planner optimization, and can't use indexes on the joined collection the same way | Restructure your data model to minimize cross-collection lookups. If you need frequent JOINs, use PostgreSQL. |
Write concern w: 1 in production | A primary crash loses all writes that haven't replicated yet. Data is gone forever. | Use w: "majority" for any write you can't afford to lose. |
| No schema | MongoDB's flexibility means your application is the only thing enforcing data shape. One buggy deploy writes malformed documents forever. | Use MongoDB Schema Validation () as a safety net. It's available since MongoDB 3.6. |
security.authorization: enabled.maxPoolSize — usually 100 per application instance is enough..project() to limit returned fields.$lookup is slow by design. It doesn't use indexes on the joined collection the way SQL JOINs do. Restructure your schema to avoid frequent $lookup usage.allowDiskUse: true, which is dramatically slower. Add $match and $limit stages early to keep the working set small.$match, use compound indexes for multi-field queries.$where (JavaScript execution in queries) exists and is dangerous. Never build $where clauses from user .// DANGEROUS: user input goes directly into $where
db.users.find({ $where: `this.email == '${req.body.email}'` });
// Attacker input: '; while(true){}; //' → infinite loop on the database
// SAFE: use $eq or other operators
db.usersblog database with posts and comments collections. Insert 3 posts and 5 comments (referenced, not embedded). Write a query with $lookup to get each post with its comments.comments.postId. Use .explain("executionStats") to compare query performance with and without the index.$group, $lookup, $group again, and $sort.messages collection for inserts and push new messages to connected clients.What is a document in MongoDB? How does it differ from a row in PostgreSQL? A document is a BSON (Binary JSON) object stored in a collection. Unlike a row, documents in the same collection can have different fields and structures. There's no enforced schema by default.
What is the _id field? Every MongoDB document has an _id field that serves as the primary key. If not provided, MongoDB auto-generates an ObjectId — a 12-byte value encoding timestamp, machine ID, process ID, and a counter, ensuring global uniqueness.
What's the difference between find() and aggregate()? find() is for simple queries (filtering, sorting, limiting). aggregate() is for complex data transformations through a pipeline of stages: filtering, grouping, joining, reshaping.
When would you choose MongoDB over PostgreSQL, and when would you not? Choose MongoDB when: your data schema varies per document (user preferences, product catalogs with varied attributes), you need very high write throughput, or your data is naturally document-shaped (content management, event logs). Don't choose MongoDB when: your data is highly relational (ORDER BY user, products with categories), you need complex multi-table transactions frequently, or you need strong schema enforcement for regulatory compliance.
Explain how MongoDB replication works. MongoDB uses a replica set — one primary and multiple secondaries. All writes go to the primary and are recorded in the oplog (a capped collection of operations). Secondaries continuously tail the oplog and apply operations asynchronously. If the primary fails, the remaining nodes hold an election to choose a new primary. A majority of voting nodes must be available for an election to succeed.
What is the 16 MB document limit and how do you work around it? MongoDB documents cannot exceed 16 MB (BSON size limit inherited from v1). For large data: use GridFS (splits files into 255 KB chunks), use referenced documents instead of embedding large arrays, or store metadata in MongoDB and the actual data in object storage (S3) with a URL reference.
MongoDB is a document database that excels at flexible-schema workloads, rapid prototyping, and high write throughput. Its aggregation pipeline is the NoSQL equivalent of SQL. The key design decisions are: embed vs reference, where to place $match stages, and what write/read concern levels to use. MongoDB and PostgreSQL are complementary — many production systems use both. Use MongoDB where the schema is variable; use PostgreSQL where the data is relational.
$match → $group → $sort → $project. Always push $match and $limit as early as possible.w: "majority" for safety. Read concern "local" for speed, "majority" for consistency.$sort before $match in an aggregation pipeline? MongoDB sorts all documents in the collection before filtering — extremely slow and memory-intensive. Always put $match first.writeConcern: { w: "majority" } guarantee? The write has been acknowledged by a majority of replica set members, meaning it will survive a primary failure.$where with user input. Use typed query operators ($eq, $gt) instead of constructing query strings. Validate and sanitize all user inputs.UNNEST |
$addFields | Add computed fields | Computed column |
$bucket | Group into ranges | CASE WHEN ... GROUP BY |
$jsonSchema| Ignoring the 16 MB document limit | Documents that grow unbounded (logs, arrays) eventually hit the limit and writes fail with cryptic errors | Monitor document sizes. For large data, use GridFS (files) or reference patterns. |