SYSTEM DESIGN · TECHNOLOGIES · 3 OF 12

MongoDB: Documents, Replica Sets and Choosing a Shard Key

How MongoDB stores JSON-like documents, when to embed and when to reference, the WiredTiger engine and indexes, replica sets with elections, write and read concerns, sharding through mongos with ranged or hashed shard keys, multi-document transactions, the traps that hurt in production and how to justify it in an interview.

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
1 / 16

Press play to hear the narration, or step through at your own pace.

Notes

What it is

MongoDB stores documents, which are JSON-like objects saved in a binary format called BSON, grouped into collections. There is no fixed table schema: two documents in one collection may have different fields, though you can add validation rules. You query by any field, including fields nested deep inside. The company runs it as a service called Atlas.

MongoDB in one line
  • document database
  • database → collections →
  • documents (BSON = binary JSON)
  • no fixed schema; optional
  • JSON Schema validation
  • managed: MongoDB Atlas
  • API-compatible: Amazon
  • DocumentDB, Azure Cosmos DB

Data model

The core idea: store together what you read together. An order with its line items and shipping address is one document, so showing the order is one read with no joins. Compare that with a relational design that needs three tables and a join. Each document has an underscore id, by default an ObjectId that starts with a timestamp, so ids roughly sort by creation time.

One order document
{ _id: ObjectId("..."),
  user_id: 42,
  status: "paid",
  items: [
    { sku: "A1", qty: 2, price: 499 },
    { sku: "B7", qty: 1, price: 1299 } ],
  ship_to: { city: "Pune", pin: "411001" },
  created_at: ISODate("2026-10-24") }

Embed or reference

The design question is always embed or reference. Embed when the child is read with its parent, has a small bounded count, and belongs to that one parent, like line items in an order. Reference by id when the child list grows without limit, is shared between parents, or changes on its own, like all posts by a user. A post array inside the user document would grow forever and hit the sixteen megabyte limit.

Embed vs reference
  • EMBED when the child
  • is read with the parent
  • is bounded (≤ hundreds)
  • belongs to one parent
  • e.g. order → line items
  • REFERENCE when the child
  • grows without limit
  • is shared or updated alone
  • e.g. user → all their posts

Storage + indexes

Under the hood, the WiredTiger engine stores collections and indexes as compressed B-trees, with document-level concurrency and a journal for crash safety. Indexes look familiar: compound, multikey for arrays, text, geospatial and TTL indexes that delete old documents automatically. For compound indexes, follow the E S R rule: fields matched by equality first, then the sort field, then range filters.

Under the hood
  • engine: WiredTiger
  • B-tree files, compressed
  • document-level concurrency
  • journal (WAL) for crash safety
  • indexes: single, compound,
  • multikey (arrays), text,
  • 2dsphere (geo), TTL, partial
  • ESR rule for compounds:
  • Equality, Sort, Range

Replica sets

Every production MongoDB is a replica set, usually three members. The primary takes writes and records each change in the oplog, a capped log of operations. Secondaries pull the oplog and replay it. If the primary disappears, the members hold an election using a Raft-style protocol, and the most up-to-date secondary becomes primary, typically in around ten to twelve seconds. Drivers retry writes across that gap.

Replica set
  • 3 members (or 5): 1 primary,
  • 2 secondaries
  • primary writes to the oplog,
  • a capped log of operations
  • secondaries pull and replay it
  • primary dies → members vote,
  • a secondary with the latest
  • data wins (Raft-like)
  • election: about 10-12 s

Write and read concern

You control durability per operation. Write concern one means only the primary confirmed, so a failover can roll that write back. Write concern majority, the default since version five, waits for most members, so the write survives any failover. On reads, read concern majority returns only data that cannot be rolled back, and read preference secondary spreads reads at the cost of seeing slightly old data.

Durability knobs
writeConcern { w: 1 }
  primary only: fast, can roll back
writeConcern { w: "majority" }
  most members: survives failover
  (default since 5.0)

readConcern "majority"
  only data that cannot roll back
readPreference "secondary"
  spreads reads, may be stale

Sharding

To scale writes, you shard a collection by a shard key. Data is split into chunks, each a range of key values. Config servers keep the map of which chunk lives on which shard. The stateless mongos router reads that map and sends each query to the right shard, and a balancer moves chunks around to keep shards even. Each shard is itself a replica set.

Sharded cluster
  • shard key: e.g. { user_id: 1 }
  • data split into chunks
  • (key ranges)
  • config servers: which chunk
  • lives on which shard
  • mongos: stateless router
  • balancer: moves chunks to
  • even out shards
  • each shard = a replica set

Shard key traps

The shard key decides everything. Shard on a timestamp or ObjectId and every new document lands in the last chunk, so one shard takes all writes. A hashed key spreads writes evenly, but range queries must ask every shard. A query that does not include the shard key is a scatter-gather to every shard, which gets slower as you add shards. Pick a key with many values, even spread, and present in most queries, like user id. You can reshard online since version five, but it is heavy.

Choosing the shard key
  • monotonic key (timestamp,
  • ObjectId) → all inserts hit
  • the last chunk: hot shard
  • hashed key → even writes,
  • but range queries scatter
  • query without the key →
  • scatter-gather to every shard
  • best: high cardinality, even,
  • in most queries (user_id)

Transactions

Writes to one document are always atomic, which is another reason to embed. Since version four, MongoDB also supports multi-document transactions with snapshot isolation, including across shards. They work, but they add latency and have a sixty second default limit. If most of your writes need transactions across many documents, that is a sign your data is relational and Postgres may suit you better.

Atomicity
  • one document: always atomic
  • (update many fields at once)
  • multi-document transactions:
  • replica sets (4.0), shards
  • (4.2), snapshot isolation
  • cost: more latency, 60 s
  • default limit; design so
  • most writes touch one doc

Great at

MongoDB is great for records that are naturally nested and read as a whole: product catalogs with varying attributes, user profiles, content management, game state. Adding a field needs no migration, so teams move quickly. Replication and sharding are built in. The aggregation pipeline handles reporting-style queries, and change streams let other services react to every change.

Where MongoDB shines
  • • nested records read whole:
  • catalogs, profiles, CMS
  • • fields change often;
  • no migrations to add one
  • • built-in replication and
  • sharding
  • • aggregation pipeline,
  • change streams, TTL, geo
  • • fast for small teams

Where it hurts

Where it hurts. Joins exist, but they are limited and get slow, so many-to-many data like followers or enrolments is awkward. When you copy data into many documents, the copies drift unless you update them all. Arrays that grow without limit hit the document cap and slow updates. A wrong shard key is expensive to fix. And schemaless really means the schema lives in every piece of code that reads the data.

Pain points
  • • joins ($lookup) are limited
  • and slow at scale
  • • many-to-many relations
  • • duplicated data drifts
  • when you denormalise
  • • 16 MB document limit;
  • unbounded arrays
  • • wrong shard key = pain
  • • "schemaless" = schema in
  • every reader’s code

Use it when

Use MongoDB when your data is shaped like documents, the shape varies or keeps changing, and most reads fetch one record by id or by a few fields. Avoid it when the data is highly relational with many joins, or when core flows like payments and inventory need transactions across many records. That is relational territory. Postgres with a JSONB column is often a fine middle ground.

Use it when / avoid it when
  • USE when
  • records are nested trees
  • shape varies or evolves
  • reads fetch one record
  • you want built-in sharding
  • AVOID when
  • heavy joins, many-to-many
  • cross-entity transactions
  • (money, inventory) → SQL
  • analytics → column store

Numbers

Numbers to keep in mind. Documents max out at sixteen megabytes. A replica set can have up to fifty members but only seven vote. Elections usually complete in about ten to twelve seconds. The default write concern is majority since version five. Transactions have a sixty second default limit. And an ObjectId is twelve bytes that begin with a timestamp. A single node handles tens of thousands of simple operations a second.

Numbers worth knowing
max document size       16 MB
replica set members     max 50, 7 vote
typical election        ~10-12 s
default write concern   majority (5.0+)
transaction limit       60 s default
ObjectId                12 bytes,
                        timestamp first
simple ops per node     10k-100k/s

Say it like this

How to say it. Product listings have different attributes per category and are always read as a whole, so I store each as a MongoDB document with the attributes embedded. A three-member replica set with majority write concern so nothing is lost on failover. Shard on a hashed product id for even writes, and add an index on seller id for the seller dashboard. That covers model, durability and scale.

In the interview
  • "Product listings vary by
  • category and are read whole,
  • so MongoDB documents with
  • attributes embedded. Replica
  • set of 3, majority writes.
  • Shard on hashed product_id
  • for even writes; seller
  • queries use an index on
  • seller_id."

Interview questions

When would you embed a sub-document and when would you reference it?

Embed when the child is read together with the parent, its count is small and bounded, and it belongs to that parent only. Reference by id when the child grows without limit, is shared by many parents or is updated independently.

How does a MongoDB replica set handle a primary failure?

Secondaries notice missing heartbeats and hold an election; the most up-to-date eligible member becomes primary, usually within about ten to twelve seconds. Drivers with retryable writes resend operations that failed during the switch.

What is the difference between w: 1 and w: majority?

With w: 1 only the primary acknowledges the write, so it can be rolled back if the primary fails before replicating it. With w: majority most voting members have it, so the write survives any failover.

What makes a good shard key?

High cardinality, an even spread of writes and presence in most queries, so that queries target one shard. A monotonically increasing key creates a hot last chunk, and a key missing from queries forces scatter-gather.

What is a scatter-gather query?

A query that does not include the shard key, so mongos must send it to every shard and merge the results. Its latency is set by the slowest shard and its cost grows as you add shards.

Does MongoDB support transactions?

Single-document operations are always atomic, and multi-document ACID transactions with snapshot isolation are supported on replica sets since 4.0 and sharded clusters since 4.2. They carry extra latency, so schemas are designed so most writes touch one document.

What is the 16 MB limit and how do you avoid hitting it?

A single BSON document cannot exceed 16 MB. Avoid unbounded arrays inside documents; move growing lists like comments or events into their own collection keyed by the parent id.

MongoDB or PostgreSQL with JSONB?

MongoDB fits when the whole data set is document-shaped and you want built-in sharding; Postgres with JSONB fits when most data is relational with a few flexible fields and you need joins and transactions.