Document Modeling
Backend Fundamentals · Databases · 10 min read
1. What problem does this solve?
Motivation
When building a backend, you must decide how to store the data that represents your domain.
Document modeling is the discipline of designing the shape of JSON‑like documents that live in a document store (MongoDB, Couchbase, DynamoDB, etc.). It directly influences:
- Query performance – how fast you can read the data you need.
- Write performance – how many writes you can sustain.
- Consistency & integrity – how you enforce relationships.
- Scalability – how the data grows across shards or partitions.
What problem exists?
Without a clear model, you end up with:
- Large, unwieldy documents that hit size limits or slow down reads.
- Fragmented data spread across many collections that require expensive joins or multiple round‑trips.
- Inconsistent state when related data is duplicated or not updated atomically.
- Poor scalability because the data layout doesn’t match the access patterns.
Why the problem matters
Backends that serve millions of requests per day must keep latency low and throughput high. A bad document model can become the single bottleneck, forcing costly infrastructure changes or leading to data corruption.
What happens if this concept is not used?
- Performance regressions: queries become slower as data grows.
- Data anomalies: stale or duplicated data.
- Operational headaches: difficult migrations, hard‑to‑debug bugs.
- Cost overruns: over‑provisioned storage or compute to compensate for inefficiencies.
Where this concept appears in real backend systems
- E‑commerce: product catalogs with embedded reviews vs. referenced reviews.
- Social media: user profiles with embedded posts or separate collections.
- IoT: sensor readings stored as embedded arrays or separate time‑series documents.
- Microservices: each service owns its own document schema but must coordinate with others via references.
2. Mental model
Analogy: The Library vs. The Filing Cabinet
| Library | Filing Cabinet |
|---|---|
| Books are documents that contain all the information you need to read a story. | Folders are documents that contain a subset of information; you need to look up other folders to get the full picture. |
| Shelves are collections that group related books. | Drawers are collections that group related folders. |
| Catalog is an index that lets you find books quickly. | Index cards are indexes that let you find folders quickly. |
Key takeaways from the analogy
- Embedding (Library): All related data lives together. You open one book and see everything. Fast reads, but if the book gets too big, it’s hard to handle.
- Referencing (Filing Cabinet): Data is split into folders. You may need to look up multiple folders, but each stays small and manageable.
3. Deep explanation
Internals
- BSON: MongoDB stores documents in BSON (Binary JSON). It supports nested objects, arrays, and binary data.
- Collections: Logical groupings of documents. Each document has a unique
_id. - Indexes: B‑tree structures that accelerate queries on specified fields. Compound indexes can cover multiple fields.
Important terminology
| Term | Definition |
|---|---|
| Document | A single JSON/BSON object. |
| Collection | A set of documents. |
| Embedding | Storing related data directly inside a parent document. |
| Referencing | Storing a reference (usually an _id) to another document. |
| Denormalization | Storing redundant data to avoid joins. |
| Normalization | Storing data in separate documents to avoid duplication. |
| Atomicity | Operations on a single document are atomic. |
| Eventual Consistency | In sharded clusters, replicas may lag behind writes. |
| Document Size Limit | 16 MB in MongoDB (current limit). |
Trade‑offs
| Strategy | Pros | Cons |
|---|---|---|
| Embedding | • Single read for all data • Atomic updates • No network hops | • Document size grows • Write amplification (update entire doc) • Harder to query sub‑documents |
| Referencing | • Small documents • Easier to update parts independently • Flexible relationships | • Requires multiple round‑trips or $lookup aggregation• No atomicity across docs • More complex queries |
Failure modes
- Over‑embedding: A document exceeds the size limit or becomes a hotspot for writes.
- Under‑embedding: Too many round‑trips lead to latency spikes.
- Missing indexes: Queries become full‑collection scans.
- Stale references: Deleted referenced documents cause orphaned data.
Limitations
- 16 MB doc size (MongoDB) forces you to split data.
- No multi‑document transactions (pre‑4.0) – you must design for eventual consistency.
- Sharding: Documents must fit into a shard key range; large embedded arrays can cause “hot” shards.
Where it fits in a backend architecture
- Data layer: Document modeling is the schema design that sits between the application code and the database.
- API layer: The shape of request/response payloads often mirrors the document shape.
- Caching layer: Frequently accessed documents can be cached; embedding can reduce cache misses.
- Search layer: If you use ElasticSearch or similar, you may denormalize data into a search document.
Interaction with related concepts
| Concept | Interaction |
|---|---|
| Indexes | Must be designed to support the chosen embedding strategy. |
| Sharding | Embedding can cause hot shards; referencing can spread load. |
| Transactions | Embedding simplifies atomicity; referencing requires multi‑document transactions or application‑level consistency. |
| Caching | Embedded data reduces cache misses; referenced data may need multiple cache lookups. |
| API contracts | The document shape often defines the API payloads. |
4. How it works
Step‑by‑step flow for a post with comments
-
Create a post
- Client sends
POST /postswith title, body. - Server inserts a document into
postscollection. - Response returns the new
_id.
- Client sends
-
Add a comment (embedding)
- Client sends
POST /posts/:id/comments. - Server performs
updateOnewith$pushto add comment tocommentsarray inside the post document. - Entire post document is rewritten (atomic).
- Client sends
-
Add a comment (referencing)
- Server creates a new document in
commentscollection withpostId. - Server updates the post document to push the comment’s
_idintocommentIdsarray.
- Server creates a new document in
-
Read a post with comments (embedding)
findOneonpostsreturns the post and its comments array in one round‑trip.
-
Read a post with comments (referencing)
findOneonpostsreturns post and array of comment IDs.- Server performs
$lookupaggregation or two separate queries to fetch comments.
Mermaid diagram (CRUD flow)
sequenceDiagram
participant Client
participant API
participant Mongo
Client->>API: POST /posts
API->>Mongo: insertOne(post)
Mongo-->>API: insertedId
API-->>Client: 201 {id}
Client->>API: POST /posts/:id/comments
API->>Mongo: updateOne({_id:id}, {$push:{comments:comment}})
Mongo-->>API: nModified
API-->>Client: 200
Client->>API: GET /posts/:id
API->>Mongo: findOne({_id:id})
Mongo-->>API: postWithComments
API-->>Client: 200 {post}
5. Practical implementation
Below is a minimal Node.js example using Mongoose (TypeScript).
It demonstrates both embedding and referencing for a simple blog.
// src/models/post.ts
import { Schema, model, Types } from 'mongoose';
export interface Comment {
_id?: Types.ObjectId;
author: string;
text: string;
createdAt: Date;
}
// ---------- Embedding ----------
const CommentSchema = new Schema<Comment>({
author: { type: String, required: true },
text: { type: String, required: true },
createdAt: { type: Date, default: Date.now }
});
const PostSchema = new Schema({
title: { type: String, required: true },
body: { type: String, required: true },
comments: [CommentSchema] // embedded array
});
export const Post = model('Post', PostSchema);
// ---------- Referencing ----------
const RefPostSchema = new Schema({
title: { type: String, required: true },
body: { type: String, required: true },
commentIds: [{ type: Schema.Types.ObjectId, ref: 'Comment' }]
});
export const RefPost = model('RefPost', RefPostSchema);
export const CommentModel = model('Comment', CommentSchema);
CRUD examples
// src/controllers/postController.ts
import { Request, Response } from 'express';
import { Post, RefPost, CommentModel } from '../models/post';
import { Types } from 'mongoose';
// ----- Create a post (embedded) -----
export const createPost = async (req: Request, res: Response) => {
const { title, body } = req.body;
const post = await Post.create({ title, body, comments: [] });
res.status(201).json(post);
};
// ----- Add comment (embedded) -----
export const addCommentEmbedded = async (req: Request, res: Response) => {
const { id } = req.params;
const { author, text } = req.body;
await Post.updateOne(
{ _id: id },
{ $push: { comments: { author, text } } }
);
res.sendStatus(200);
};
// ----- Add comment (referenced) -----
export const addCommentReferenced = async (req: Request, res: Response) => {
const { id } = req.params;
const { author, text } = req.body;
const comment = await CommentModel.create({ author, text });
await RefPost.updateOne(
{ _id: id },
{ $push: { commentIds: comment._id } }
);
res.sendStatus(200);
};
// ----- Get post with comments (referenced) -----
export const getPostWithComments = async (req: Request, res: Response) => {
const { id } = req.params;
const post = await RefPost.findById(id).populate('commentIds').lean();
res.json(post);
};
Tip: Use
lean()for read‑only queries to avoid Mongoose document overhead.
6. Production considerations
| Concern | What to watch for | Mitigation |
|---|---|---|
| Scalability | Hot shards when many writes target the same document. | Use a sharding key that distributes writes; consider referencing for high‑write collections. |
| Reliability | Single document size limit (16 MB). | Split large arrays into separate documents; use pagination. |
| Security | Unintended data exposure via embedded documents. | Enforce field-level access controls; use Mongoose schemas with select: false. |
| Observability | Hard to trace multi‑document updates. | Log operation IDs; use transaction sessions for referenced writes. |
| Performance | Full collection scans due to missing indexes. | Create compound indexes on frequently queried fields; monitor explain() output. |
| Error handling | Partial failures in referenced writes. | Wrap writes in transactions (MongoDB 4.0+); retry on transient errors. |
| Resource usage | Large documents increase I/O and memory. | Keep documents below 1 MB when possible; compress binary data. |
| Operational concerns | Schema evolution. | Use versioned schemas; keep backward compatibility. |
| Failure recovery | Orphaned references after a crash. | Periodic consistency checks; use TTL indexes to clean stale data. |
| Deployment | Versioned migrations. | Use migration tools (e.g., migrate-mongo) and CI pipelines. |
7. Common mistakes
| Mistake | Why it’s problematic |
|---|---|
| Embedding everything | Documents grow beyond size limits; writes become expensive. |
| Referencing everything | Requires many round‑trips; queries become slow. |
| Ignoring indexes | Reads degrade to full scans; latency spikes. |
| Not considering write patterns | Hot spots cause shard imbalance. |
Using $lookup for every read | Aggregation pipeline can be slow; better to embed for read‑heavy paths. |
| Assuming atomicity across references | Without transactions, updates can leave data inconsistent. |
| Hard‑coding field names | Schema drift leads to bugs when fields change. |
| Over‑normalizing | Too many collections make maintenance hard. |
8. Senior engineer perspective
What changes at scale?
- Sharding strategy: Choose a shard key that balances reads and writes. For example, use a user ID for user‑centric data.
- Batching & bulk writes: Use
bulkWriteto reduce round‑trips. - Multi‑document transactions: Leverage MongoDB 4.0+ transactions for critical consistency.
- Monitoring: Track
documentSize,writeConcern,indexBuilds, andshardKeyDistribution. - Cost/performance: Embedding reduces network traffic but can increase storage cost due to duplication; referencing reduces storage but increases read cost.
- Bottlenecks: Document size limit, write amplification, and index maintenance.
- Failure scenarios: Hot shards, replica lag, and network partitions.
- When NOT to use: If you need strong ACID guarantees across many documents, consider a relational DB or a hybrid approach.
9. Interview questions
1BeginnerWhat is the difference between embedding and referencing in MongoDB?
_id). Embedding gives atomic writes and fast reads but can lead to large documents; referencing keeps documents small but requires extra queries or $lookup.2BeginnerWhy does MongoDB have a 16 MB document size limit?
3SeniorHow would you design a schema for a social media platform where users can post, comment, and like posts, considering millions of writes per second?
post collection with minimal fields; a comment collection with postId; a like collection with postId and userId. Use sharding on postId or userId depending on access patterns. Use bulk writes and transactions for atomic updates when needed. Add indexes on postId, userId, and compound indexes for query patterns.4SeniorExplain how you would handle eventual consistency when using referencing for comments in a sharded cluster.
5Scenario/DebuggingYou notice that reads for a post with many comments are slow, but writes are fast. What could be causing this, and how would you fix it?
$lookup or a separate query to fetch comments. Add indexes on postId in the comments collection to speed up lookups.10. Hands‑on task
Build a simple blog API that supports:
- Creating a post (
POST /posts). - Adding a comment to a post (
POST /posts/:id/comments). - Retrieving a post with its comments (
GET /posts/:id).
Requirements
- Use embedding for comments in the
postscollection. - Add a second endpoint that uses referencing (
POST /posts/:id/comments-refandGET /posts/:id/ref). - Measure the size of a post document after 100 comments.
- Add an index on the
titlefield and explain its effect on afind({ title: 'foo' })query.
Deliverables
- A GitHub repo with the Node.js project.
- A README explaining how to run the API and the benchmark script.
- A short report (max 200 words) on the trade‑offs observed.
11. Quick revision
- Embedding: All related data in one document → atomic writes, fast reads, risk of large docs.
- Referencing: Separate documents linked by
_id→ smaller docs, need extra queries, no atomicity. - Document size limit: 16 MB (MongoDB) → split large arrays or use referencing.
- Indexes: Essential for query performance; compound indexes for multi‑field queries.
- Sharding: Choose a shard key that distributes writes; avoid hot keys.
- Transactions: Available from MongoDB 4.0; use for multi‑document atomicity.
- Bulk writes:
bulkWritereduces round‑trips and improves throughput. - Monitoring: Track
documentSize,writeConcern,indexBuilds,shardKeyDistribution. - Common pitfalls: Over‑embedding, missing indexes, ignoring size limits, assuming atomicity across references.