Back to Notes

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

  1. 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 posts collection keyed by user_id), not embedded.
  2. 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.
  3. 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]]