Practice this topic in a realistic system design interview
In this chapter, we will learn what a document database is, how it stores and queries data, how to model data with embedding and references, and how document databases scale.
Loading simulation...
Suppose we are building a product catalog for an online store.
The store sells many kinds of products. A shoe has sizes and a material, a laptop has a processor and memory, and a book has an author and a page count.
In a relational database, fitting all of these into one table is awkward:
(product_id, attribute_name, attribute_value), but that makes queries complex and slow.There is a second problem. Displaying a single product page might require data from the products table, a variants table, an images table, and a specifications table. The application has to join all of them just to rebuild one product.
What if we could store the whole product, with all its details, as a single record that matches how the application already uses it? That is the idea behind a document database.
A document database stores data as documents. A document is a self-contained record, usually in a JSON-like format. MongoDB, for example, stores documents in a binary form of JSON called BSON.
Documents are grouped into collections. A collection is roughly similar to a table in a relational database.
Each document has a unique identifier. In MongoDB, this is a field called _id.
Popular document databases include MongoDB, Couchbase, Google Firestore, and Azure Cosmos DB. Amazon DynamoDB can also store document-style items, although it is usually described as a key-value and document database.
The choice of _id matters more than it seems:
ObjectId) are convenient and roughly time-ordered.Time-ordered IDs keep recent records close together in the index, but can create a write hotspot if used as a shard key (more on that in Section 11).
Let's look at what a product document might contain.
specifications object with the material and the weight.All of this lives in one document. So when the application needs the product page, it reads one document and gets everything it needs.
There are no joins, and the data is stored together on disk, so the database can usually load it in a single read. This is often called data locality.
The document matches the shape of the object in the application code, which makes it natural to work with.
The second big feature of document databases is a flexible schema.
In a relational database, every row in a table has the same columns. In a document database, two documents in the same collection can have completely different fields. The shoe document and the laptop document can both live in the same products collection, each with its own fields.
Adding a new field does not require a migration. The application simply starts writing documents with the new field.
This is useful when the data varies naturally, or when a product is changing quickly and the data model changes with it.
If the database does not enforce structure, the application must handle it. Code that reads documents has to deal with missing fields, old formats, and unexpected types.
For this reason, many teams still add rules. MongoDB, for example, supports schema validation, where you can require certain fields and types while still allowing others to vary:
In production, "schemaless" should not mean "anything goes". Documents in a collection usually share a predictable core shape, with variation only where it is natural.
Unlike a key-value store, a document database understands what is inside each document. So we can query by any field, not just the id.
For more complex questions, MongoDB provides an aggregation pipeline. A pipeline is a series of stages, where each stage transforms the data and passes it to the next.
For example:
MongoDB also has a $lookup stage that can join data from another collection. But it is generally more efficient to design documents so that most queries do not need joins at all.
Just like relational databases, document databases need indexes to query efficiently. Without an index, a query has to scan every document in the collection.
Document databases support familiar index types, such as indexes on a single field and compound indexes on multiple fields. They also support indexes that are specific to documents.
In MongoDB, an index on an array field is called a multikey index. It creates an index entry for each value in the array.
So if a product has sizes 8, 9, and 10, the index contains an entry for each size, and a search for size 9 can use the index directly.
Indexes on nested fields work as well.
| Index Type | Use Case | Example |
|---|---|---|
| Single field | Query by one field | brand |
| Compound | Query or sort by multiple fields | { brand, price } |
| Multikey | Query values inside arrays | sizes, variants.color |
| Nested field | Query inside embedded objects | specifications.material |
| Text | Basic text search | name, description |
| TTL | Expire old documents automatically | sessions, temporary tokens |
The trade-offs are the same as in any database. Indexes speed up reads, but every write must also update them, and they use extra memory and disk. Multikey indexes on large arrays can grow especially fast, since one document creates many entries.
The most important decision when modeling data in a document database is whether to embed related data or reference it.
Embedding means storing related data inside the same document. For example, an order document can embed its line items and the shipping address.
Referencing means storing related data in a separate document and linking to it by id, similar to a foreign key. For example, an order document can store a customer_id that points to a separate customer document.
A useful rule of thumb:
Line items belong to one order, they are always displayed with the order, and an order has a limited number of them. So embedding them makes sense.
A customer places many orders, and their details can change. If we embedded the full customer in every order, updating their email would mean updating every order they ever placed. So we reference the customer instead.
| Factor | Favor Embedding | Favor Referencing |
|---|---|---|
| Access pattern | Read together most of the time | Read independently |
| Ownership | Owned by the parent | Shared by many documents |
| Relationship size | One-to-one or one-to-few | One-to-many or many-to-many |
| Growth | Has a clear limit | Can grow without limit |
| Updates | Changes with the parent | Changes independently |
A common design is a mix of both. For example, an order might reference the customer by id, but also embed a snapshot of the shipping address as it was when the order was placed:
The address and item prices are saved copies. If Alice moves or the product price changes later, this order still shows what was true at purchase time, which is usually exactly what you want for order history.
In short, embedding gives you a single read and atomic updates, at the risk of duplicated data and documents that keep growing. Referencing keeps one official copy and smaller documents, at the cost of extra reads and no database-enforced foreign keys.
Let's apply this to a blog. Each post has a title, an author, some tags, and comments.
At first, embedding comments in the post seems natural, because we show them together. But a popular post might receive tens of thousands of comments. The document would keep growing, and every new comment would rewrite a larger and larger document. MongoDB also limits a single document to 16 MB.
So comments are usually stored in their own collection, with each comment referencing the post id. The post might still embed the three most recent comments, so the page can load quickly without a second query.
This shows the general idea. Model documents around how the application reads and writes data, not around a fixed set of rules.
Document databases can update part of a document without rewriting the whole thing from the application. For example, we can set a single field, increment a counter, or push a new item onto an array.
An important guarantee is that operations on a single document are atomic. If we update three fields in one document, either all three change or none do.
This is one reason embedding is useful. If an order and its line items live in one document, updating them together is automatically safe.
For changes across multiple documents, MongoDB added multi-document ACID transactions in version 4.0, and extended them to sharded clusters in version 4.2. So we can, for example, create an order and reduce stock in a separate product document within one transaction.
But these transactions have more overhead than single-document operations. So a good document design keeps most related changes inside one document when possible.
Document databases usually replicate data across multiple servers for availability. In MongoDB, this is called a replica set.
A replica set has one primary and usually two or more secondaries.
Applications can also control how safe each operation should be.
majority means the write is only confirmed after most members have it. That protects the write even if the primary fails right after.When a collection becomes too large or too busy for one replica set, document databases can shard it. The collection is split across multiple shards, and each shard is its own replica set.
The data is divided using a shard key, which is a field present in every document. For example, an orders collection might be sharded by customer_id.
MongoDB supports two strategies:
A router (mongos in MongoDB) receives queries from the application and sends them to the right shard.
So the shard key should match your most common queries.
A poor shard key can also create hot spots, where one shard receives most of the traffic.
| Shard Key | Problem |
|---|---|
status | Only a few values, so data cannot spread across many shards |
created_at or a time-ordered _id (ranged) | Every new write lands on the shard holding the newest range |
country | One large country dominates one shard |
customer_id (hashed) | Spreads writes evenly, and "orders for this customer" stays a targeted query |
Let's compare document databases with relational databases.
The line between them has also blurred. PostgreSQL supports JSON columns with indexing, and MongoDB supports joins and transactions.
So the choice is less about features and more about which model fits your data and access patterns best.
A document database is a good fit when:
It is less suited to:
And if you find yourself referencing everything and joining collections on almost every query, that is a sign the data may be more relational than document-shaped.
10 quizzes