🍃 Certification Study Guide

MongoDB Associate Developer

A complete, community-made guide to everything covered on the MongoDB Associate Developer exam — data modeling, CRUD, aggregation, indexing, and every schema design pattern with a worked example.

Written after passing the exam — sharing what actually mattered for study, so the next person has an easier path.
53
Questions
75
Minutes
150$
To Register

1. Exam Overview

The MongoDB Associate Developer exam (exam code C100DEV) validates practical, hands-on knowledge of building applications with MongoDB. It's multiple-choice and scenario-based — expect questions that show you a document or query and ask what happens, rather than pure definitions.

Good news: nearly every topic on the exam has a matching free course on MongoDB University (learn.mongodb.com). Working through those labs plus this guide covers the syllabus well.

2. MongoDB Fundamentals

MongoDB is a document database. Instead of rows in tables, data is stored as BSON documents (a binary superset of JSON) grouped into collections, inside a database.

Core Terminology

SQL MongoDB
Database Database
Table Collection
Row Document
Column Field
Primary Key _id field
Join Embedding or $lookup

Sample Document

{
  _id: ObjectId("64f1a2b3c4d5e6f7a8b9c0d1"),
  name: "Ada Lovelace",
  age: 28,
  active: true,
  skills: ["math", "programming"],
  address: { city: "London", country: "UK" },
  joined: ISODate("2024-01-15T00:00:00Z")
}

Common BSON Types

Know these for the exam — questions often ask which type a field should be:

Type Example
Double / Int32 / Int64 / Decimal128 3.14, NumberLong(10)
String "hello"
Object { a: 1 }
Array [1, 2, 3]
ObjectId ObjectId() — 12-byte unique id, default _id
Boolean true / false
Date ISODate() — stored as ms since epoch
Null null
Exam detail: the _id field is immutable, indexed automatically, and unique per collection. If you don't supply one on insert, the driver auto-generates an ObjectId.

3. CRUD Operations

Create

db.users.insertOne({ name: "Ada", age: 28 })
db.users.insertMany([{ name: "Ada" }, { name: "Alan" }])

Read

db.users.find({ age: { $gte: 18 } })
db.users.findOne({ name: "Ada" })
db.users.find({ skills: "math" }).sort({ age: -1 }).limit(5)

Common Query Operators

Operator Meaning
$eq, $ne equal / not equal
$gt, $gte, $lt, $lte comparison
$in, $nin value in / not in array
$and, $or, $not, $nor logical
$exists field is present
$regex pattern match
$elemMatch array element matches multiple conditions

Querying Arrays

Array queries are a heavily tested corner of CRUD, because the matching rules are less obvious than scalar fields. Given { tags: ["red", "blue", "green"] }:

Query Meaning
{ tags: "blue" } Matches if any element of the array equals "blue" — no special operator needed.
{ tags: ["red", "blue", "green"] } Matches only if the array is exactly this array, in this order.
{ tags: { $all: ["red", "blue"] } } Array must contain all of these values, any order, extras allowed.
{ tags: { $size: 3 } } Array has exactly 3 elements (cannot combine with range operators; no index can satisfy $size directly).
{ "tags.0": "red" } Matches the element at a specific index.
{ scores: { $elemMatch: { $gt: 80, $lt: 90 } } } A single element satisfies all the conditions together (without $elemMatch, MongoDB would allow different elements to satisfy each condition independently).
// Array of embedded documents
db.students.find({
  grades: { $elemMatch: { subject: "Math", score: { $gte: 90 } } }
})
// Without $elemMatch this could wrongly match a doc where
// one grade element has subject "Math" and a DIFFERENT element has score >= 90
Exam trap: { "grades.score": { $gte: 90 }, "grades.subject": "Math" } (two separate conditions) can match a document even if no single array element satisfies both — because MongoDB checks each condition against the array independently. $elemMatch is what forces "same element."

Array Projection

// Return only the first 2 elements of an array field
db.students.find({}, { grades: { $slice: 2 } })
// Return only the array element(s) that matched the query
db.students.find(
  { grades: { $elemMatch: { subject: "Math" } } },
  { "grades.$": 1 }
)

Update

db.users.updateOne({ name: "Ada" }, { $set: { age: 29 } })
db.users.updateMany({ active: false }, { $set: { archived: true } })
db.users.updateOne({ name: "Ada" }, { $inc: { loginCount: 1 } })
db.users.updateOne({ name: "Ada" }, { $push: { skills: "AI" } })
Watch for: replaceOne() replaces the whole document (except _id), while updateOne() with $set only modifies named fields. The exam likes to test this distinction.

Updating Arrays

The exam tests these array update operators specifically — know what each does to duplicates and ordering:

Operator Behavior
$push Appends a value (allows duplicates). Combine with $each to push multiple values at once.
$addToSet Appends a value only if it doesn't already exist (no duplicates). Also supports $each.
$pull Removes all array elements matching a condition.
$pullAll Removes all instances of the exact listed values.
$pop Removes the first (-1) or last (1) element.
// Push multiple values, keep sorted, cap at 5 (a leaderboard-style update)
db.users.updateOne(
  { _id: 1 },
  { $push: { scores: { $each: [95, 88], $sort: -1, $slice: 5 } } }
)

// Add without duplicating
db.users.updateOne({ _id: 1 }, { $addToSet: { skills: "MongoDB" } })

// Remove all elements matching a condition
db.users.updateOne({ _id: 1 }, { $pull: { scores: { $lt: 50 } } })

// Remove the last element
db.users.updateOne({ _id: 1 }, { $pop: { scores: 1 } })

Positional Operators — Updating Specific Array Elements

Operator Updates
$ (positional) The first array element that matched the query filter.
$[] (all positional) Every element in the array.
$[<identifier>] (filtered positional) Only elements matching a condition in arrayFilters.
// $ — update the one grade element that matched
db.students.updateOne(
  { _id: 1, "grades.subject": "Math" },
  { $set: { "grades.$.score": 95 } }
)

// $[] — give every element a 10% bonus
db.students.updateOne(
  { _id: 1 },
  { $mul: { "grades.$[].score": 1.1 } }
)

// $[] — only bump grades below 60, using arrayFilters
db.students.updateOne(
  { _id: 1 },
  { $set: { "grades.$[elem].score": 60 } },
  { arrayFilters: [{ "elem.score": { $lt: 60 } }] }
)

Delete

db.users.deleteOne({ name: "Ada" })
db.users.deleteMany({ active: false })

Upsert

db.users.updateOne(
  { name: "Grace" },
  { $set: { age: 40 } },
  { upsert: true } // inserts if no match found
)

4. Indexes

Indexes let MongoDB scan a smaller structure instead of every document (a COLLSCAN). Every collection has a default index on _id.

How Indexes Work: B-Trees

MongoDB's default storage engine (WiredTiger) implements every index as a B-tree (technically a B+tree variant). Understanding the shape of that structure explains almost every indexing rule the exam tests.

  • A B-tree keeps keys sorted across a balanced tree of nodes, so it can find a value, a range, or "the next value in order" in O(log n) disk/memory reads instead of scanning every document.
  • Each entry in the tree stores the indexed field's value plus a pointer to the document's location on disk — so the database reads the small, sorted tree first, then jumps straight to matching documents instead of touching every document.
  • Because the tree is sorted, it naturally satisfies equality (jump straight to the value), range (walk the tree between two bounds), and sort (read the tree in order — no separate in-memory sort needed) — but only when the query lines up with how the tree is built.
  • A compound index is one B-tree built on concatenated field values, sorted by the first field, then the second within each first-field value, and so on. This is exactly why field order in a compound index changes what it can efficiently support.

Types of Indexes

Type Use case Example
Single field Query on one field db.users.createIndex({ age: 1 })
Compound Query on multiple fields together db.orders.createIndex({ status: 1, createdAt: -1 })
Multikey Indexing array fields db.users.createIndex({ skills: 1 })
Text Full-text search db.articles.createIndex({ body: "text" })
Wildcard Unknown/dynamic field names db.logs.createIndex({ "$**": 1 })
Unique Enforce uniqueness db.users.createIndex({ email: 1 }, { unique: true })
TTL Auto-expire documents db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })

The ESR Rule (Equality, Sort, Range) — and Why It Works

For compound indexes, order fields as: fields used for Equality matches first, then fields used to Sort, then fields used for Range filters. This is one of the most heavily tested performance concepts on the exam — and it falls directly out of how the B-tree is built:

  1. Equality first: an equality match (status: "shipped") lets the engine jump straight to one narrow slice of the tree — everything after that field is now sorted within that slice, essentially "for free."
  2. Sort second: because the tree is already sorted by the next field within that slice, MongoDB can read entries in that order directly off the index — no separate, memory-hungry in-memory sort stage required.
  3. Range last: a range condition (price: {$gt:10,$lt:100}) has to scan a contiguous span of the tree rather than jump to one point. Doing this last means the equality and sort benefits above still apply before the scan narrows further. Put a range field earlier and it breaks the sorted order the later fields depend on, forcing extra work.
// Query: status == "shipped", sort by createdAt, price between 10 and 100
db.orders.find({ status: "shipped", price: { $gt: 10, $lt: 100 } }).sort({ createdAt: -1 })

// Correct index order (Equality, Sort, Range):
db.orders.createIndex({ status: 1, createdAt: -1, price: 1 })

// A "wrong order" index still works, but is slower — e.g. range before sort:
db.orders.createIndex({ status: 1, price: 1, createdAt: -1 })
// forces MongoDB to sort createdAt in memory instead of reading it off the index

Indexing Arrays (Multikey Indexes)

When you index a field that holds an array, MongoDB automatically creates a multikey index — it adds one B-tree entry per array element, all pointing back to the same document.

// Document
{ _id: 1, name: "Ada", skills: ["math", "programming", "writing"] }

// Index
db.users.createIndex({ skills: 1 })
// Under the hood, the B-tree gets 3 entries, one per element:
// "math" -> doc 1
// "programming" -> doc 1
// "writing" -> doc 1

db.users.find({ skills: "programming" }) // uses the index, IXSCAN
Key rule the exam loves: a compound index can have at most one array (multikey) field. db.users.createIndex({ skills: 1, hobbies: 1 }) throws an error if both skills and hobbies are arrays — MongoDB would otherwise need one tree entry per combination of elements across both arrays, which it disallows.

Other array-index details worth knowing:

  • Indexing an array of embedded documents (e.g. grades: [{subject, score}]) creates multikey entries over the whole subdocument, or over specific subfields if you index "grades.score" directly.
  • $size queries cannot use a multikey index (there's no single "array length" value stored per entry) — they always trigger a collection scan unless combined with other indexed conditions.
  • Multikey indexes support equality, range, and $elemMatch queries on the array efficiently, but each additional array element indexed adds index storage and slows writes to that document.

Explain Plans

db.orders.find({ status: "shipped" }).explain("executionStats")
// Look for: IXSCAN (index used) vs COLLSCAN (full scan, usually bad)
// executionStats.totalKeysExamined vs totalDocsExamined — big gaps between
// them (docs >> keys) are a sign the index isn't selective enough

5. Data Modeling & Schema Design

This is the heart of the MongoDB exam and the part people most often underestimate. MongoDB's flexible schema means the "right" model depends entirely on your application's access patterns — not on normalization rules from relational databases. Design around how data is read and written, not just how it's structured.

Embedding vs. Referencing

Embedding nests related data inside a single document. Best when data is read together and doesn't grow unbounded.

// Embedded one-to-few (a user with a few addresses)
{
  _id: 1,
  name: "Ada",
  addresses: [
    { city: "London", zip: "E1 6AN" },
    { city: "Paris", zip: "75001" }
  ]
}

Referencing stores an _id pointer to a document in another collection. Best for one-to-many/many-to-many, large or independently-growing data, or data reused across many parents.

// Referenced one-to-many (a user with many orders)
// users collection
{ _id: 1, name: "Ada" }

// orders collection
{ _id: 101, userId: 1, total: 49.99 }
{ _id: 102, userId: 1, total: 12.50 }
Use Embedding when... Use Referencing when...
Data is always accessed together Data is accessed independently
"Contains" / one-to-few relationship One-to-many or many-to-many at scale
Child data doesn't grow unbounded Child data grows continuously (e.g. logs)
You want atomic single-document writes Related data needs independent updates
16MB limit: a single BSON document cannot exceed 16MB. Unbounded arrays that embed growing child data are a classic anti-pattern (the "Massive Arrays" anti-pattern) — this is exam-relevant.

Beyond the basic embed/reference decision, MongoDB documents a set of named schema design patterns for recurring modeling problems. The exam expects you to recognize each pattern from a scenario description. Below is each pattern with a concrete example.

Pattern

Attribute Pattern

Problem: documents have many similar fields, but only a subset applies to each document, or you need to search/sort across fields whose names vary.

Solution

Move the variable fields into a key/value array. This lets you index the array once instead of indexing every possible field name.

Example — product specs
// Before: hard to index every possible spec, many are null per product
{ name: "Camera X100", resolution: "24MP", waterproof: true, batteryLife: null }

// After: Attribute Pattern
{
  name: "Camera X100",
  specs: [
    { k: "resolution", v: "24MP" },
    { k: "waterproof", v: true }
  ]
}
// One index covers all specs:
db.products.createIndex({ "specs.k": 1, "specs.v": 1 })
Pattern

Bucket Pattern (Group Data)

Problem: high-volume time-series or event data (IoT sensors, stock ticks, logs) would create one document per reading — too many small documents.

Solution

Group readings into "buckets" covering a fixed time window, with the individual readings as an embedded array plus precomputed summary fields.

Example — hourly sensor bucket
{
  sensorId: "sensor-42",
  hour: ISODate("2026-07-26T09:00:00Z"),
  readings: [
    { minute: 0, temp: 21.4 },
    { minute: 1, temp: 21.5 }
    // ... up to 60 readings
  ],
  count: 60,
  avgTemp: 21.6,
  minTemp: 20.9,
  maxTemp: 22.3
}

Modern MongoDB also offers native timeseries collections that implement this pattern automatically — worth knowing the built-in feature exists alongside the manual pattern.

Pattern

Outlier Pattern

Problem: the vast majority of documents are small, but a rare "outlier" (a viral tweet, a bestselling product) would otherwise need an enormous embedded array.

Solution

Embed as normal for the common case, but add a flag once a document becomes an outlier, and overflow the extra data into a separate collection.

Example — a book's reviews
// Typical book: reviews embedded directly
{ _id: 1, title: "Normal Book", reviews: [ /* a handful of reviews */ ] }

// Bestseller: flagged, extra reviews live elsewhere
{
  _id: 2,
  title: "Bestseller",
  reviews: [ /* first ~100 reviews */ ],
  hasExtraReviews: true
}
// book_reviews_overflow collection
{ bookId: 2, review: "..." }
Pattern

Computed Pattern

Problem: recalculating an expensive value (totals, averages, view counts) on every read wastes CPU, especially when reads vastly outnumber writes.

Solution

Compute the value once — on write, or on a schedule — and store the result on the document so reads are a simple field lookup.

Example — movie view count
// Instead of counting views on every page load:
db.movies.updateOne(
  { _id: movieId },
  { $inc: { viewCount: 1 } }
)
// Reads just do:
db.movies.findOne({ _id: movieId }, { viewCount: 1 })
Pattern

Subset Pattern

Problem: a document embeds a large array (e.g. thousands of reviews) but the application usually only needs the first few — loading all of it wastes memory (the "working set").

Solution

Embed only a small, frequently-accessed subset, and keep the full set in a separate collection queried on demand.

Example — product page reviews
// products collection: only the 10 most helpful reviews embedded
{
  _id: 1,
  name: "Wireless Mouse",
  topReviews: [ /* 10 most recent/helpful reviews */ ],
  totalReviews: 4381
}

// reviews collection: full history, paginated on demand
{ productId: 1, rating: 5, text: "..." }
Pattern

Extended Reference Pattern

Problem: pure referencing means every read needs a $lookup join just to show a few frequently-needed fields (like a customer's name on an order).

Solution

Duplicate only the handful of fields you display often directly into the referencing document, alongside the reference id — avoiding a join for common reads while keeping the source of truth elsewhere.

Example — order with embedded customer summary
{
  _id: 501,
  customerId: 77,          // reference to the full customer doc
  customer: {               // extended reference: just what the order needs
    name: "Ada Lovelace",
    shippingCity: "London"
  },
  items: [ { sku: "A1", qty: 2 } ],
  total: 59.98
}
Trade-off: duplicated fields can go stale if the source changes. Only extend fields that rarely change (name, not phone number) or that you're fine denormalizing.
Pattern

Single Collection Pattern

Problem: an application has many related but differently-shaped entity types (e.g. a social app with users, posts, and comments) that are frequently queried together, and separate collections would require multiple round trips or $lookups.

Solution

Combine the polymorphic and extended reference ideas: store all related entity types in one collection with a type discriminator plus reference fields, so one query can retrieve a mixed result set sorted or filtered together.

Example — social feed
{ _id: 1, type: "post", authorId: 9, text: "Hello world", createdAt: ISODate() }
{ _id: 2, type: "comment", postId: 1, authorId: 12, text: "Nice!", createdAt: ISODate() }
{ _id: 3, type: "like", postId: 1, authorId: 4, createdAt: ISODate() }

// Single query returns everything related to a post, sorted by time
db.feed.find({ $or: [{ _id: 1 }, { postId: 1 }] }).sort({ createdAt: 1 })
Pattern

Archive Pattern

Problem: a collection accumulates data that is rarely accessed after a certain age (old orders, closed tickets), but it still consumes RAM/working-set and slows down queries on the active data.

Solution

Periodically move old/cold documents to a separate "archive" collection (often on cheaper storage or a different cluster tier), keeping the primary collection lean and fast for current data.

Example
// Move orders older than 2 years to an archive collection
const cutoff = new Date(); cutoff.setFullYear(cutoff.getFullYear() - 2);
const old = db.orders.find({ createdAt: { $lt: cutoff } }).toArray();
db.orders_archive.insertMany(old);
db.orders.deleteMany({ createdAt: { $lt: cutoff } });

Modeling Relationships — Quick Reference

Relationship Typical approach
One-to-One Embed (or reference if the child is large/optional)
One-to-Few Embed as an array
One-to-Many Reference (child references parent's _id)
One-to-Squillions Parent reference on the child; never embed unboundedly
Many-to-Many Reference both ways, or an array of ids on one side

Schema Design Anti-Patterns to Recognize

  • Massive arrays — arrays that grow without bound (risking the 16MB limit and slow updates).
  • Massive number of collections — thousands of collections (e.g. one per customer) adds overhead and hits storage engine limits.
  • Unnecessary indexes — every index has a write-performance and storage cost; unused indexes should be removed.
  • Bloated documents — storing rarely-used data inline instead of applying the Subset or Outlier pattern.
  • Separating data accessed together — over-normalizing to the point every read needs multiple $lookups.
  • Case-insensitive queries without a matching index — causes collection scans.

6. Aggregation Framework

The aggregation pipeline processes documents through a sequence of stages, each transforming the data for the next. Think of it as a data-processing pipe.

Common Stages

Stage Purpose
$match Filter documents (like find())
$group Group by a key and compute aggregates
$project Reshape documents, include/exclude/compute fields
$sort Order results
$limit / $skip Pagination
$lookup Left outer join to another collection
$unwind Flatten an array into one document per element
$addFields Add or overwrite fields without removing others
$count Count documents at that pipeline stage
$set / $unset Alias for $addFields / remove fields
$bucket / $bucketAuto Group documents into ranges ("buckets")
$facet Run multiple sub-pipelines in parallel, in one pass
$replaceRoot Promote an embedded document to become the top-level document

Example — revenue per customer

db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: { _id: "$customerId", totalSpent: { $sum: "$total" }, orders: { $sum: 1 } } },
  { $sort: { totalSpent: -1 } },
  { $limit: 10 }
])

Example — $lookup join

db.orders.aggregate([
  {
    $lookup: {
      from: "customers",
      localField: "customerId",
      foreignField: "_id",
      as: "customer"
    }
  },
  { $unwind: "$customer" }
])

$unwind — flattening arrays

$unwind takes a document with an array field and outputs one document per array element, copying every other field. It's what makes it possible to $group, $match, or $sort on individual array elements instead of the whole array.

// Before $unwind — one order document, 3 items
{ _id: 1, customerId: 7, items: [
  { sku: "A1", qty: 2, price: 10 },
  { sku: "B2", qty: 1, price: 25 },
  { sku: "C3", qty: 4, price: 5 }
]}

db.orders.aggregate([
  { $unwind: "$items" }
])

// After $unwind — three separate documents, one per item
{ _id: 1, customerId: 7, items: { sku: "A1", qty: 2, price: 10 } }
{ _id: 1, customerId: 7, items: { sku: "B2", qty: 1, price: 25 } }
{ _id: 1, customerId: 7, items: { sku: "C3", qty: 4, price: 5 } }

// Common pairing: unwind, then group to get per-SKU totals across all orders
db.orders.aggregate([
  { $unwind: "$items" },
  { $group: { _id: "$items.sku", totalQty: { $sum: "$items.qty" } } }
])
Exam detail: by default, $unwind drops any document whose array field is missing, null, or an empty array. Pass { path: "$items", preserveNullAndEmptyArrays: true } instead of the plain string form to keep those documents (with items set to null).

$addFields / $set — computing new fields

$addFields (and its alias $set) adds new fields or overwrites existing ones, while passing every other field through unchanged — unlike $project, you don't have to re-list fields you want to keep.

db.orders.aggregate([
  {
    $addFields: {
      lineTotal: { $multiply: ["$qty", "$price"] },
      isBulkOrder: { $gte: ["$qty", 100] }
    }
  }
])
// Every original field (customerId, items, ...) is still present,
// plus the two new computed fields.

// $unset removes fields you no longer need after computing them
db.orders.aggregate([
  { $addFields: { lineTotal: { $multiply: ["$qty", "$price"] } } },
  { $unset: ["internalNotes", "tempFlag"] }
])

$bucket — grouping into ranges

db.orders.aggregate([
  {
    $bucket: {
      groupBy: "$total",
      boundaries: [0, 50, 100, 500, 1000],
      default: "1000+",
      output: { count: { $sum: 1 }, orders: { $push: "$_id" } }
    }
  }
])
// Produces one document per range: 0-50, 50-100, 100-500, 500-1000, "1000+"

$facet — multiple pipelines in one query

db.orders.aggregate([
  {
    $facet: {
      byStatus: [{ $group: { _id: "$status", count: { $sum: 1 } } }],
      topSpenders: [
        { $group: { _id: "$customerId", total: { $sum: "$total" } } },
        { $sort: { total: -1 } },
        { $limit: 5 }
      ]
    }
  }
])
// Returns one document with two arrays: "byStatus" and "topSpenders"
// — useful for building a dashboard in a single round trip
Exam detail: pipeline stages execute in order, and each stage's output becomes the next stage's input. Put $match as early as possible to shrink the working set before expensive stages like $lookup, $unwind, or $group.

7. Transactions

MongoDB supports multi-document ACID transactions across replica sets and sharded clusters. Most schema designs (thanks to embedding) avoid needing them, but they matter when multiple documents/collections must change atomically.

const session = client.startSession();
session.startTransaction();
try {
  await accounts.updateOne({ _id: "A" }, { $inc: { balance: -100 } }, { session });
  await accounts.updateOne({ _id: "B" }, { $inc: { balance: 100 } }, { session });
  await session.commitTransaction();
} catch (e) {
  await session.abortTransaction();
  throw e;
} finally {
  session.endSession();
}
Design tip tested on the exam: a well-embedded document gets atomic single-document writes for free — no transaction needed. Reach for transactions only when the data genuinely spans multiple documents/collections.

8. Replication

A replica set is a group of mongod processes maintaining the same data set, providing redundancy and high availability.

  • Primary — receives all writes.
  • Secondaries — replicate the primary's oplog; can serve reads if configured.
  • Election — if the primary becomes unavailable, an eligible secondary is elected automatically.
  • Write Concern — how many replica set members must acknowledge a write (e.g. { w: "majority" }).
  • Read Preference — controls whether reads go to the primary, secondaries, or nearest node (e.g. primary, secondaryPreferred, nearest).

Minimum recommended replica set size is 3 members (odd number, to avoid tied elections).

9. Sharding

Sharding horizontally partitions data across multiple machines ("shards") when a data set is too large or has too much throughput for one server.

  • Shard key — the field(s) used to distribute documents; choosing a good one (high cardinality, even distribution, matches query patterns) is critical and heavily tested.
  • mongos — the query router applications connect to; routes operations to the correct shard(s).
  • Config servers — store cluster metadata (which ranges of the shard key live on which shard).
  • Chunks — ranges of shard-key values MongoDB migrates between shards to balance load.
Poor shard key example: a monotonically increasing field (like a timestamp or auto-incrementing counter) causes all new writes to land on a single shard — a common "hot shard" exam scenario.

10. Security

  • Authentication — verifying identity (SCRAM, x.509 certs, LDAP, Kerberos on Enterprise).
  • Authorization (RBAC) — role-based access control; built-in roles like read, readWrite, dbAdmin, clusterAdmin.
  • Encryption in transit — TLS/SSL between clients and the cluster.
  • Encryption at rest — the WiredTiger storage engine can encrypt data files.
  • Field-level encryption — client-side encryption for specific sensitive fields, so even MongoDB never sees plaintext.
  • Auditing — logging of access and administrative actions (Enterprise/Atlas).
// Create a user scoped to one database with a built-in role
db.createUser({
  user: "appUser",
  pwd: "securePassword",
  roles: [{ role: "readWrite", db: "shopDB" }]
})

11. Drivers & Atlas

MongoDB provides official drivers for most languages (Node.js, Python, Java, C#, Go, etc.), all implementing the same core CRUD/aggregation API against the wire protocol.

  • Connection stringmongodb+srv://user:pass@cluster.mongodb.net/dbname for Atlas SRV connections.
  • Atlas — MongoDB's fully managed cloud database service; the exam expects familiarity with basic Atlas concepts (clusters, network access lists, database users).
  • Compass — the official GUI for exploring data, building queries, and analyzing schema/index usage visually.
  • mongosh — the modern MongoDB shell (replaces the legacy mongo shell).

12. Study Tips

What actually helped

  • Do the free MongoDB University learning path end-to-end, including the labs — not just videos.
  • Practice reading explain() output until COLLSCAN vs IXSCAN is instant to spot.
  • Memorize each schema design pattern by its problem, not just its name — the exam describes scenarios, not pattern names.
  • Get comfortable with aggregation stage order and what each stage's output looks like.
  • Know the difference between update operators ($set, $inc, $push, $addToSet, $pull) cold.

Common traps

  • Confusing updateOne vs replaceOne behavior.
  • Picking a shard key without thinking about write distribution.
  • Assuming normalization (SQL instincts) is always "correct" — MongoDB rewards designing for access patterns.
  • Forgetting the 16MB document size limit when justifying embedding choices.
  • Mixing up read preference modes and when each is safe to use.

13. Resources

Good luck — the exam rewards understanding why a pattern fits a scenario, not memorizing syntax. Work through real examples like the ones above and you'll recognize them instantly on test day.

Practice Quiz

60 scenario-style questions across two sets, mirroring the real exam's mix of topics. Click an answer to check it instantly — the explanation appears either way. Original PDFs (with the full answer key) are available in the Code & Data tab.

Score: 0 / 0

Code & Sample Data

A ready-to-run local MongoDB environment with a seeded sample dataset, so you can try every query and pattern from the Study Guide against real documents instead of copy-pasting.

Downloads

docker-compose.yml
Spins up a local MongoDB container (with auth) in one command.
Download
seed.js
mongosh script that creates the sample dataset used throughout this site.
Download
Practice_Quiz_Set1.pdf
Printable version of quiz Set 1, with full answer key.
Download
Practice_Quiz_Set2.pdf
Printable version of quiz Set 2, with full answer key.
Download

Getting Started

  1. Install Docker Desktop if you don't already have it, then download docker-compose.yml above into an empty folder.
  2. Start MongoDB:
    docker compose up -d
    This runs MongoDB on localhost:27017 with username admin / password password.
  3. Download seed.js into the same folder, then load the sample data:
    mongosh "mongodb://admin:password@localhost:27017/mongodb_learning?authSource=admin" seed.js
  4. Explore with mongosh or MongoDB Compass, using the same connection string. Five collections are seeded: categories, products, customers, orders, and sensor_readings — each one built to demonstrate a pattern from the Study Guide.

docker-compose.yml

version: '3.8'

services:
  mongodb:
    image: mongo:latest
    container_name: mongodb-dev-cert
    ports:
      - "27017:27017"
    volumes:
      - ./data:/data/db
    environment:
      - MONGO_INITDB_ROOT_USERNAME=admin
      - MONGO_INITDB_ROOT_PASSWORD=password
    restart: unless-stopped
Note: this bind-mounts ./data for storage, so your data survives container restarts. Delete that local data folder if you want to start from a clean database.

Sample Dataset — What's in seed.js

The seed script isn't random test data — every collection was built to match a specific concept from the Study Guide, so you can query it and see the pattern in action:

Collection Demonstrates
categories Tree Pattern — array-of-ancestors hierarchy (electronics → computers → laptops)
products Attribute Pattern (specs: [{k,v}]) + Subset Pattern (topReviews) + multikey tags index
customers Simple referenced collection with a unique index on email
orders Extended Reference Pattern (embeds a customer summary) + an ESR-ordered compound index
sensor_readings Bucket Pattern — hourly buckets with precomputed min/max/avg
// Try these once seed.js has run:
db.orders.find({ status: "shipped" }).sort({ createdAt: -1 })
db.products.find({ "specs.k": "ramGB", "specs.v": { $gte: 16 } })
db.orders.aggregate([
  { $unwind: "$items" },
  { $group: { _id: "$items.name", totalQty: { $sum: "$items.qty" } } }
])
db.orders.find({ status: "shipped" }).explain("executionStats") // check IXSCAN vs COLLSCAN