MongoDB interviews test your understanding of document modeling, indexing, and when a document database is — and isn't — the right fit.
In a relational database you spread an entity's data across multiple normalized tables and reassemble it with joins at query time, whereas MongoDB stores a whole related record as a single BSON document, often with nested arrays and subdocuments. That means a `users` collection can hold a user's addresses, preferences, and order history all in one document instead of four separate tables. The tradeoff is you're moving normalization decisions from write time to design time: you decide upfront how much to denormalize based on how the data is actually read. Documents in the same collection also don't have to share an identical shape, since MongoDB doesn't enforce a rigid table schema by default. This makes the document model a natural fit when your data is hierarchical or object-like and mostly accessed as a unit, and less natural when you need strict relational integrity across many entities.
Embed when the related data is only ever accessed together with its parent, has a bounded size, and doesn't need to be queried independently, like an address inside a user document or line items inside an order. Reference when the related data is large, unbounded, shared across multiple parents, or updated independently of the parent, similar to how a `comments` collection would reference a `posts` collection rather than embedding thousands of comments inside a post. The rule of thumb is that data accessed together should be stored together, but you weigh that against the 16MB document size limit and how often the embedded data changes. A good gut check is the cardinality of the relationship: one-to-few relationships usually embed well, one-to-many or many-to-many usually reference. Getting this wrong is one of the most common causes of MongoDB performance problems, so it's worth modeling around actual query patterns rather than mirroring a relational schema.
An index in MongoDB is a B-tree-like structure built on one or more fields that lets the query planner avoid a full collection scan, or `COLLSCAN`, and jump straight to matching documents. A single-field index speeds up queries and sorts on that one field, like an index on `email` for login lookups. A compound index covers multiple fields in a defined order, such as `{ userId: 1, createdAt: -1 }`, and it can serve queries that filter on a prefix of those fields or that filter on the first field and sort on the second without an extra in-memory sort. Indexes matter for performance because without them MongoDB has to scan every document to find matches, which scales linearly with collection size and gets painful fast. The tradeoff is that every index adds overhead to writes and consumes memory, so you index based on your actual query patterns rather than adding one for every field.
The aggregation pipeline is MongoDB's framework for transforming and analyzing data by passing documents through a sequence of stages, each of which takes the previous stage's output as its input. Common stages include `$match` to filter documents early, `$group` to compute aggregates like sums or counts per key, `$project` to reshape or compute fields, `$sort`, `$lookup` to join in data from another collection, and `$unwind` to flatten arrays into individual documents. Because it's a pipeline, ordering matters a lot for performance: putting `$match` and `$sort` as early as possible lets MongoDB use indexes and shrink the working set before the expensive stages run. It's essentially MongoDB's answer to SQL's `GROUP BY`, joins, and window-style computations, but expressed as a composable sequence of operators instead of a single declarative statement. For anything beyond simple filtering, the aggregation framework is the right tool rather than pulling documents into the application and processing them in code.
Sharding is MongoDB's mechanism for horizontal scaling: it partitions a collection's data across multiple servers, called shards, based on a shard key, so no single machine has to hold or serve the entire dataset. A `mongos` router sits in front of the shards and routes each query to the shard or shards that actually hold the relevant data, ideally just one if the query includes the shard key. This lets both storage capacity and read/write throughput grow roughly linearly as you add shards, which vertical scaling on a single replica set can't do past a point. The hard part is picking a shard key with high cardinality and even access distribution, because a poor choice creates a hot shard that gets a disproportionate share of traffic. Sharding is typically reached for once a single replica set can no longer handle the working set in memory or the write throughput of one primary, not as a default starting architecture.
A replica set is a group of `mongod` instances holding copies of the same data, usually one primary that accepts all writes and multiple secondaries that replicate from it asynchronously via the oplog. If the primary becomes unreachable, the remaining members hold an election and promote a secondary to primary, typically within seconds, so the application can keep writing once it reconnects. This gives you both high availability and read scaling, since reads can optionally be directed to secondaries. Elections use a heartbeat and majority-voting protocol, so you generally want an odd number of voting members to avoid ties. It's worth noting replication is asynchronous by default, so a failover can lose the most recent writes that hadn't replicated yet, which is exactly what write concern settings are meant to control.
MongoDB follows a schema-on-read approach: the database itself doesn't force every document in a collection to match a rigid, pre-declared structure, so you can evolve your data model as the application changes without a blocking migration. Relational databases take the opposite stance, schema-on-write, where the table structure is defined and enforced up front and every row must conform to it. In practice this doesn't mean MongoDB applications have no schema at all; you still design one deliberately, you just enforce it at the application layer or with optional JSON Schema validation rules on the collection instead of the storage engine rejecting mismatched rows. The real philosophical shift is that schema design in MongoDB is driven by how the data will be queried and accessed, not by normalization rules meant to eliminate redundancy. That's why the same logical data can look very different modeled in MongoDB versus modeled in a normalized relational schema.
Yes, since MongoDB 4.0 it supports multi-document ACID transactions on replica sets, and since 4.2 across sharded clusters too, using a syntax similar to `session.startTransaction()` and `commitTransaction()`. Before that, MongoDB only guaranteed atomicity at the single-document level, which is one reason the embedding pattern was so encouraged: a single document write was always atomic even without formal transactions. Multi-document transactions are useful for cases like moving a balance between two documents or updating several related collections consistently, but they carry a real performance cost and are meant to be used sparingly and kept short-lived. Good practice is still to model data so that most operations only need single-document atomicity, and reach for multi-document transactions only for the genuinely cross-document cases. It's also worth remembering that transactions have a default timeout and can hold locks that increase contention if held open too long.
Every document in MongoDB must have a unique `_id` field, which acts as its primary key within the collection, and MongoDB automatically builds a unique index on it. If you don't supply one, MongoDB generates an `ObjectId` by default, a 12-byte value made up of a 4-byte timestamp, a 5-byte random value, and a 3-byte incrementing counter. Because the timestamp is the leading component, ObjectIds are roughly sortable by creation time, which is a handy side effect for things like default sort order or approximate created-at queries. You aren't required to use an ObjectId, though; `_id` can be any unique value you choose, like a string or a UUID, which is common when you want a natural key. It's immutable once set, so you can't update the `_id` field of an existing document.
Read concern controls the consistency and durability guarantee a query gets on the data it reads, independent of which node serves the read. `local`, the default, returns whatever data is on the node handling the query, even if that data could later be rolled back after a failover. `majority` only returns data that has been acknowledged by a majority of the replica set and is therefore durable even across a failover. `linearizable` is the strictest, guaranteeing the read reflects all writes that completed before it started, at the cost of extra latency since it has to confirm with a majority before returning. Choosing between them is a tradeoff between how fresh and durable you need the data to be versus how much latency you're willing to accept.
Write concern controls how many replica set members must acknowledge a write before MongoDB reports it back to the client as successful. The default, `w: 1`, only requires acknowledgment from the primary, which is fast but means the write could be lost if the primary fails before replicating it. Setting `w` to `majority` waits for acknowledgment from a majority of voting members, which is much safer against data loss during a failover and is the recommended setting for anything important. You can also set `w: 0` for fire-and-forget writes with no acknowledgment at all, which is rarely appropriate outside of things like high-volume logging. There's also a `j` option to require the write be committed to the on-disk journal before acknowledging, which protects against loss from a process crash even on a single node, so overall it's a direct tradeoff between write latency and durability.
MongoDB tends to be a weaker choice when your data is inherently tabular and relational, with many entities that need to be joined together in varied, ad hoc ways, since that's exactly what SQL and normalized schemas were built for, and `$lookup` isn't as efficient or flexible as a real join engine at scale. It's also less ideal when you need strong, complex multi-row invariants enforced by the database itself, like foreign key constraints across many tables, since MongoDB expects you to enforce most of that in application logic. Reporting-heavy workloads with lots of unpredictable, ad hoc analytical queries across many dimensions often fit a relational or dedicated OLAP system better than a document store optimized around known access patterns. Multi-document transactions exist in MongoDB now, but if transactional integrity across many entities is the core of the workload rather than the exception, a relational database's transaction model is generally a more natural and better-tested fit. Basically, if you can't describe your primary access patterns up front and you need heavy normalization with strict integrity, that's a signal to lean relational.
This happens when you embed a one-to-many relationship as an array inside a document, but the array has no natural upper bound and keeps growing indefinitely, like storing every event or log line for a user inside that user's document. Once a document approaches the 16MB size limit writes start failing, and even before that, every update that pushes into a large array requires MongoDB to rewrite the whole document on disk, which gets slower as the array grows. It also bloats your working set, since fetching the parent document means pulling every embedded element even when you only care about a few. The fix is usually to move the unbounded side of the relationship into its own collection and reference the parent's `_id`, or to bucket the data, for example grouping a sensor's readings into per-hour or per-day documents instead of one document per relationship. The underlying lesson is that embedding works great for bounded, one-to-few relationships, but it turns into an anti-pattern the moment the many side is truly unbounded.
The oplog, short for operations log, is a special capped collection on the primary that records every write operation in order, and it's the mechanism secondaries use to replicate data by continuously tailing it and applying the same operations. Because it's capped, it only holds a rolling window of recent operations, which means a secondary that falls too far behind can exhaust the oplog window and need a full resync instead of catching up incrementally. The oplog is also what backs features like change streams, which let applications subscribe to a live feed of data changes instead of polling the collection. Its size is configurable, and in write-heavy systems it's common to size it larger than the default to give secondaries, and things like backup tools, more headroom before falling out of sync. In short, the oplog is the backbone of MongoDB's replication and change-tracking story.
A covered query is one where every field requested by the query and its projection is present in the index being used, so MongoDB can satisfy the entire query from the index alone without ever touching the actual documents on disk. That matters because index structures are typically smaller than the full documents and more likely to already be resident in memory, so skipping the document-fetch step cuts both I/O and latency. To design for one, you build a compound index that includes both the filter fields and the returned fields, and explicitly exclude `_id` from the projection if it isn't part of the index and isn't needed. You can confirm a query is covered by running `.explain()` and checking that `totalDocsExamined` is zero even though documents were returned. It's a nice optimization for hot, narrow queries, but it's not something to force onto every query since it just adds more indexes to maintain.
A good shard key needs high cardinality, meaning enough distinct values that data and load spread evenly across shards rather than piling onto one; a low-cardinality key like a boolean or status field creates a small number of oversized chunks that can't split further. It also needs low frequency, avoiding values that repeat across a huge number of documents, and ideally non-monotonic behavior, since a monotonically increasing key like a timestamp or auto-incrementing ID sends all new writes to whichever shard currently owns the top of the range, creating a hot shard. Beyond distribution, the shard key should also match your dominant query pattern, ideally one that includes the shard key in its filter, because queries that omit it get broadcast to every shard in a scatter-gather pattern, which is much slower. A common fix for a monotonic key is hashed sharding, where MongoDB hashes the key's values to spread writes evenly, at the cost of losing efficient range queries on that field. Because a shard key is difficult and expensive to change after the fact, even though newer versions support resharding, it's one of the decisions worth spending real design time on upfront.