with one click
database-mongodb-document-design
Designing MongoDB schemas
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Designing MongoDB schemas
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
| name | database-mongodb-document-design |
| description | Designing MongoDB schemas |
Scope: Document modeling, embedding vs referencing, schema patterns Lines: ~280 Last Updated: 2025-10-18
Activate this skill when:
MongoDB stores data as BSON documents (JSON-like):
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "Alice",
"email": "alice@example.com",
"created_at": ISODate("2024-01-15T10:30:00Z")
}
Key differences from relational:
Relational: Normalize first, denormalize for performance MongoDB: Model for your access patterns, embed related data
Goal: Minimize queries by structuring data as it's retrieved.
Pattern: Store related data inside the document.
// User document with embedded address
{
"_id": ObjectId("..."),
"name": "Alice",
"email": "alice@example.com",
"address": {
"street": "123 Main St",
"city": "San Francisco",
"state": "CA",
"zip": "94105"
}
}
When to embed:
Pros:
Cons:
Pattern: Store reference to another document (like foreign key).
// User document
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "Alice",
"email": "alice@example.com"
}
// Order document with reference
{
"_id": ObjectId("..."),
"user_id": ObjectId("507f1f77bcf86cd799439011"), // Reference
"total": 99.99,
"items": [...]
}
When to reference:
Pros:
Cons:
Start: How is the data accessed?
│
├─ Always accessed together?
│ ├─ Yes → Embed
│ └─ No → Reference
│
├─ How often does related data change?
│ ├─ Rarely → Embed
│ └─ Frequently → Reference
│
├─ How many related items?
│ ├─ One or few (1:1, 1:few) → Embed
│ ├─ Bounded (1:many, predictable) → Embed or Reference
│ └─ Unbounded (1:many, grows indefinitely) → Reference
│
├─ Is related data shared across documents?
│ ├─ Yes → Reference
│ └─ No → Embed
│
└─ Document size concerns?
├─ Will exceed 16MB? → Reference
└─ Stays small → Embed
Use case: User profile
{
"_id": ObjectId("..."),
"email": "alice@example.com",
"profile": {
"first_name": "Alice",
"last_name": "Smith",
"bio": "Software engineer",
"avatar_url": "https://..."
},
"created_at": ISODate("...")
}
Why embed: Profile is always accessed with user, doesn't change often.
Use case: Blog post with comments (limited)
{
"_id": ObjectId("..."),
"title": "My Blog Post",
"content": "Lorem ipsum...",
"author": "Alice",
"comments": [
{
"author": "Bob",
"text": "Great post!",
"created_at": ISODate("...")
},
{
"author": "Charlie",
"text": "Thanks for sharing",
"created_at": ISODate("...")
}
]
}
Why embed: Few comments (10-20), retrieved with post.
Constraint: Limit embedded array size (e.g., max 100 comments).
Use case: Product with reviews (potentially many)
Option 1: Embed first N reviews, reference rest
// Product document
{
"_id": ObjectId("..."),
"name": "Widget",
"price": 29.99,
"recent_reviews": [ // Embed first 10 for quick display
{
"user": "Alice",
"rating": 5,
"text": "Great product!",
"created_at": ISODate("...")
}
],
"review_count": 150
}
// Separate reviews collection for full list
db.reviews.find({ product_id: ObjectId("...") })
Option 2: Reference all reviews
// Product document
{
"_id": ObjectId("..."),
"name": "Widget",
"price": 29.99,
"review_count": 150
}
// Reviews collection
{
"_id": ObjectId("..."),
"product_id": ObjectId("..."), // Reference
"user": "Alice",
"rating": 5,
"text": "Great product!"
}
Query with $lookup (join):
db.products.aggregate([
{ $match: { _id: ObjectId("...") } },
{ $lookup: {
from: "reviews",
localField: "_id",
foreignField: "product_id",
as: "reviews"
}}
])
Use case: User with orders (grows indefinitely)
// User document
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "Alice",
"email": "alice@example.com"
}
// Orders collection
{
"_id": ObjectId("..."),
"user_id": ObjectId("507f1f77bcf86cd799439011"), // Reference
"items": [...],
"total": 99.99,
"created_at": ISODate("...")
}
// Query user's orders
db.orders.find({ user_id: ObjectId("507f1f77bcf86cd799439011") })
Why reference: Orders grow indefinitely, not always accessed with user.
Use case: Users and roles
Option 1: Embed role IDs in user
// User document
{
"_id": ObjectId("..."),
"name": "Alice",
"role_ids": [
ObjectId("role1"),
ObjectId("role2")
]
}
// Roles collection
{
"_id": ObjectId("role1"),
"name": "admin",
"permissions": ["read", "write", "delete"]
}
// Query with $lookup
db.users.aggregate([
{ $match: { _id: ObjectId("...") } },
{ $lookup: {
from: "roles",
localField: "role_ids",
foreignField: "_id",
as: "roles"
}}
])
Option 2: Separate junction collection
// Users
{ "_id": ObjectId("user1"), "name": "Alice" }
// Roles
{ "_id": ObjectId("role1"), "name": "admin" }
// User-Roles junction
{
"user_id": ObjectId("user1"),
"role_id": ObjectId("role1"),
"granted_at": ISODate("...")
}
Problem: Embedding unbounded arrays leads to document growth and performance issues.
// ❌ BAD: Unbounded array
{
"_id": ObjectId("..."),
"user": "Alice",
"posts": [ // Could grow to thousands
{ "title": "Post 1", "content": "..." },
{ "title": "Post 2", "content": "..." },
// ... thousands more
]
}
Solutions:
// User
{ "_id": ObjectId("user1"), "name": "Alice" }
// Posts (separate collection)
{ "_id": ObjectId("..."), "user_id": ObjectId("user1"), "title": "Post 1" }
{ "_id": ObjectId("..."), "user_id": ObjectId("user1"), "title": "Post 2" }
Use case: Time-series data (sensor readings, logs)
// Instead of one document per reading:
// { sensor_id: 1, reading: 23.5, timestamp: ... }
// Bucket readings by hour:
{
"_id": ObjectId("..."),
"sensor_id": 1,
"bucket_hour": ISODate("2024-01-15T10:00:00Z"),
"readings": [
{ "value": 23.5, "minute": 0 },
{ "value": 23.7, "minute": 1 },
// ... up to 60 readings per hour
],
"reading_count": 60
}
Benefits:
Use case: Products with varying review counts (most have few, some have thousands)
// For products with < 100 reviews: embed
{
"_id": ObjectId("product1"),
"name": "Widget",
"reviews": [
{ "user": "Alice", "rating": 5, "text": "..." }
],
"review_count": 10
}
// For products with > 100 reviews: reference
{
"_id": ObjectId("product2"),
"name": "Popular Widget",
"review_count": 5000,
"has_outlier_reviews": true // Flag
}
// Separate reviews collection for outlier products
db.reviews.find({ product_id: ObjectId("product2") })
MongoDB supports JSON Schema validation to enforce structure.
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["email", "name", "created_at"],
properties: {
email: {
bsonType: "string",
pattern: "^.+@.+$",
description: "must be a valid email"
},
name: {
bsonType: "string",
minLength: 1,
maxLength: 100
},
age: {
bsonType: "int",
minimum: 0,
maximum: 120
},
created_at: {
bsonType: "date"
}
}
}
}
})
Validation modes:
strict (default): Reject invalid documentsmoderate: Validate new documents, allow existing invalid onesdb.users.createIndex({ email: 1 }) // Ascending
db.users.createIndex({ created_at: -1 }) // Descending
db.orders.createIndex({ user_id: 1, created_at: -1 })
Order matters: Can use for user_id alone, but not created_at alone.
db.users.createIndex({ email: 1 }, { unique: true })
Use case: Index field that doesn't exist in all documents.
db.users.createIndex({ phone: 1 }, { sparse: true })
Only indexes documents with phone field.
Use case: Session data, temporary records.
db.sessions.createIndex(
{ created_at: 1 },
{ expireAfterSeconds: 3600 } // Delete after 1 hour
)
db.posts.createIndex({ title: "text", content: "text" })
// Search
db.posts.find({ $text: { $search: "mongodb tutorial" } })
❌ Embedding unbounded arrays - Document grows indefinitely ✅ Use referencing or bucketing pattern
❌ Normalizing like relational DB - Loses MongoDB benefits ✅ Embed related data when accessed together
❌ No indexing on reference fields - Slow lookups
✅ Index foreign key fields (user_id, etc.)
❌ Exceeding 16MB document limit - Insert/update fails ✅ Reference large data, use GridFS for files
❌ Using $lookup for every query - Slow (like SQL joins) ✅ Embed data when possible, minimize lookups
❌ No schema validation - Data inconsistency ✅ Use JSON Schema validation for structure
| Relational | MongoDB |
|---|---|
| Table | Collection |
| Row | Document |
| Column | Field |
| Primary Key | _id field |
| Foreign Key | Reference (ObjectId) or embedded |
| JOIN | $lookup or embedded |
| Index | Index |
Relational:
CREATE TABLE users (id, name, email);
CREATE TABLE posts (id, user_id, title, content);
CREATE TABLE comments (id, post_id, user_id, text);
MongoDB (Embedded):
// posts collection
{
"_id": ObjectId("..."),
"title": "My Post",
"content": "...",
"author": { // Embedded user (denormalized)
"name": "Alice",
"email": "alice@example.com"
},
"comments": [ // Embedded comments
{
"author": "Bob",
"text": "Great post!",
"created_at": ISODate("...")
}
]
}
MongoDB (Referenced):
// users collection
{ "_id": ObjectId("user1"), "name": "Alice", "email": "..." }
// posts collection
{
"_id": ObjectId("post1"),
"user_id": ObjectId("user1"), // Reference
"title": "My Post",
"content": "..."
}
// comments collection
{
"_id": ObjectId("..."),
"post_id": ObjectId("post1"), // Reference
"user_id": ObjectId("user1"), // Reference
"text": "Great post!"
}
Decision: Embed if data accessed together, reference if queried independently.
Schema Design:
[ ] Modeled for access patterns (not normalization first)
[ ] Embedded data that's always accessed together
[ ] Referenced unbounded or frequently changing data
[ ] Avoided unbounded arrays
[ ] Considered bucketing for time-series data
[ ] Added schema validation for critical fields
Indexes:
[ ] _id indexed automatically
[ ] Foreign key fields (user_id, etc.) indexed
[ ] Frequently queried fields indexed
[ ] Compound indexes for multi-field queries
[ ] Unique indexes where appropriate
Performance:
[ ] Documents stay under 16MB
[ ] Minimize use of $lookup (prefer embedding)
[ ] Use projections to return only needed fields
[ ] Consider capping collections for logs/events
postgres-schema-design.md - Relational schema design for comparisondatabase-selection.md - When to use MongoDB vs PostgreSQLredis-data-structures.md - Complementary data store for cachingorm-patterns.md - ORM usage with MongoDB (Mongoose, etc.)Last Updated: 2025-10-18 Format Version: 1.0 (Atomic)
Index of Build Systems Skills
Coordination patterns for distributed dataflow systems including barriers, epochs, and distributed snapshots
Windowing, sessionization, time-series aggregation, and late data handling for streaming systems
Comprehensive guide to GNU Debugger (GDB) for debugging C/C++/Rust programs. Covers breakpoints, stack traces, variable inspection, TUI mode, .gdbinit customization, Python scripting, remote debugging, and core file analysis.
Paxos consensus algorithm including Basic Paxos, Multi-Paxos, roles, phases, and practical implementations
Gossip protocols for disseminating information, failure detection, and eventual consistency in large-scale distributed systems