MongoDB
The canonical document database — see [[SQL vs NoSQL]] for the general decision framework this fits into. This page is MongoDB-specific mechanics: the document model, replica sets, sharding, and the aggregation pipeline.
Document Model — BSON, Not Rows
// A single document — nested structure, no separate JOIN needed for embedded data
{
_id: ObjectId("..."),
name: "Alice",
email: "alice@example.com",
addresses: [
{ type: "home", city: "Pune" },
{ type: "work", city: "Mumbai" }
]
}
Documents are stored as BSON (binary JSON) — richer than plain JSON (native types for dates, binary data, ObjectId), but conceptually still "a nested, schema-flexible object per record." Schema-flexible, not schema-less: different documents in the same collection can have different fields, but in practice a real application still has an implicit schema it depends on — MongoDB just doesn't enforce it at the database layer the way a relational CREATE TABLE does (schema validation rules can be added explicitly if desired).
Embedding vs. Referencing — the Core Modeling Decision
// Embed — when the nested data is always accessed together with the parent, and bounded in size
{ _id: 1, name: "Order #1", items: [{sku: "A1", qty: 2}, {sku: "B2", qty: 1}] }
// Reference — when the related data is large, shared across many parents, or grows unbounded
{ _id: 1, name: "Order #1", customer_id: ObjectId("...") }
The decision test: is this "one-to-few" data that's always read together (embed — one document fetch, no JOIN needed) or "one-to-many/unbounded" data, or data referenced by many different parents (reference — a separate collection, queried by ID, conceptually like a foreign key but without database-enforced referential integrity). Embedding too much creates documents that can hit the 16MB document size limit and grow unboundedly (a product with an ever-growing embedded array of every review it's ever received, for instance); referencing too much re-introduces JOIN-like query patterns MongoDB isn't optimized for.
Indexes
db.orders.createIndex({ user_id: 1 }) // ascending single-field index
db.orders.createIndex({ user_id: 1, created_at: -1 }) // compound — same left-prefix rule as SQL (see SQL Indexes & Query Performance)
db.orders.createIndex({ location: "2dsphere" }) // geospatial — see Uber/Ride-Sharing's geo-indexing discussion
db.articles.createIndex({ content: "text" }) // full-text search
The same left-prefix rule from [[SQL Indexes & Query Performance]] applies identically to compound indexes here — a query filtering only on created_at gets no benefit from a {user_id: 1, created_at: -1} index.
Replica Sets — Built-In High Availability
Primary (accepts writes) --replicates oplog--> Secondary 1
--replicates oplog--> Secondary 2
Every write is recorded in the oplog (operations log, MongoDB's equivalent of a WAL — see [[Postgres Internals]]'s WAL discussion for the same underlying idea); secondaries continuously replay it. If the primary fails, the remaining members hold an election and promote a new primary — automatic failover is a built-in default behavior, not an add-on, unlike a typical Postgres deployment where streaming replication + failover requires additional tooling (Patroni, repmgr) layered on top.
Read preference lets a query explicitly choose to read from secondaries (readPreference: "secondaryPreferred") — trading potential replication lag (the same staleness trade-off as any read replica, see [[Database Replication & Sharding]]) for reduced load on the primary.
Sharding — Horizontal Scale
mongos (query router) → routes to the correct shard based on the shard key
Shard 1: chunk range [minKey, X)
Shard 2: chunk range [X, Y)
Shard 3: chunk range [Y, maxKey]
Data is partitioned by a shard key — chosen once, hard to change later, and the single most consequential modeling decision in a sharded MongoDB deployment. A poorly chosen shard key (e.g., a monotonically increasing field like created_at or an auto-incrementing ID) creates a hot shard: all new writes land on whichever shard currently owns the highest range, exactly the range-based-sharding hotspot problem described generally in [[Database Replication & Sharding]]. A well-distributed shard key (e.g., a hashed field, or a naturally high-cardinality field like user_id) spreads writes evenly — the same hash-vs-range sharding trade-off covered there, made concrete in MongoDB's specific terminology (mongos, chunks, shard key).
Chunk splitting and migration: as data grows, MongoDB automatically splits an oversized chunk and can migrate chunks between shards to rebalance load — an automated version of the resharding concern raised in [[Database Replication & Sharding]]'s sharding trade-offs section.
Aggregation Pipeline — MongoDB's Answer to GROUP BY/Window Functions
db.orders.aggregate([
{ $match: { status: "delivered" } }, // like SQL WHERE
{ $group: { _id: "$user_id", total: { $sum: "$amount" } } }, // like SQL GROUP BY + SUM
{ $sort: { total: -1 } }, // like SQL ORDER BY
{ $limit: 5 } // like SQL LIMIT
])
Each stage receives the previous stage's output and transforms it — conceptually a pipeline of composable transformations, directly analogous to chaining CTEs in [[SQL Subqueries & CTEs]], but expressed as an ordered array of stage objects rather than named WITH clauses. $lookup performs a JOIN-like operation against another collection when a query genuinely needs to combine two collections despite the embed-vs-reference modeling preference favoring avoiding this.
Transactions (Since 4.0)
const session = client.startSession();
session.startTransaction();
try {
await accounts.updateOne({_id: 1}, {$inc: {balance: -100}}, {session});
await accounts.updateOne({_id: 2}, {$inc: {balance: 100}}, {session});
await session.commitTransaction();
} catch (e) {
await session.abortTransaction();
}
Multi-document ACID transactions exist since MongoDB 4.0 (single-document writes were always atomic even before this) — but reaching for multi-document transactions frequently is often a modeling smell: it usually means the embed-vs-reference decision put related data that's written together into separate documents/collections when embedding it together (making the write naturally atomic as a single document write) might have been the better model in the first place.
When MongoDB Genuinely Wins Over Postgres+JSONB
Postgres's JSONB column type narrows this gap considerably (see [[SQL vs NoSQL]]) — the honest, non-reflexive answer to "MongoDB vs Postgres" is: reach for MongoDB when the access pattern is overwhelmingly document-shaped (fetch/write one whole nested object at a time, rarely querying across the internal structure of many documents relationally) and horizontal write scale genuinely exceeds what a well-tuned, sharded Postgres setup comfortably handles — not by default preference. For most applications with meaningful relational structure and moderate scale, Postgres+JSONB for the occasional flexible field is the more defensible starting choice, with the burden of proof on justifying MongoDB, not the reverse.
Interview Drills
- A social media "user profile" document embeds an array of every post the user has ever made. What breaks as the user becomes prolific? — The document can hit MongoDB's 16MB document size limit, and even before that, every read/write of the profile document has to move the entire growing posts array — this is a "one-to-many, unbounded" relationship that should be referenced (a separate
postscollection keyed byuser_id), not embedded. - A sharded collection uses
created_at(a monotonically increasing timestamp) as its shard key. What operational problem emerges under sustained write load? — All new writes land on whichever shard currently owns the highest timestamp range — a hot shard, identical in mechanism to the range-based-sharding hotspot problem in [[Database Replication & Sharding]]; a hashed or higher-cardinality shard key distributes writes evenly instead. - Why might frequent reliance on multi-document transactions in a MongoDB application signal a modeling problem rather than just "good defensive coding"? — It often means data that's always written together atomically was split across separate documents/collections when the embed-vs-reference decision should have kept it together in one document — a single-document write is atomic by default with no transaction needed at all; reaching for multi-document transactions repeatedly is a signal to revisit that modeling choice.
Cross-links
[[SQL vs NoSQL]] · [[Database Replication & Sharding]] · [[SQL Indexes & Query Performance]] · [[Postgres Internals]] · [[Consistent Hashing]]