Skip to content
Back to skills

2723 Mongodb Document Design Eeb6a4ae

ASecurity

Designing MongoDB schemas

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
databasesjavascriptgojavasqldatabaseperformance

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 2723-mongodb-document-design_eeb6a4ae --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 2723 Mongodb Document Design Eeb6a4ae?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 2723 Mongodb Document Design Eeb6a4ae
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-2723-mongodb-document-design-eeb6a4ae/badge)](https://www.skillsdirectory.com/skills/tools-only-2723-mongodb-document-design-eeb6a4ae)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
---
name: database-mongodb-document-design
description: Designing MongoDB schemas
---


# MongoDB Document Design

**Scope**: Document modeling, embedding vs referencing, schema patterns
**Lines**: ~280
**Last Updated**: 2025-10-18

## When to Use This Skill

Activate this skill when:
- Designing MongoDB schemas
- Choosing between embedding and referencing
- Modeling relationships in document databases
- Handling large or unbounded arrays
- Optimizing for MongoDB query patterns
- Migrating from relational to document model

## Core Concepts

### Document Model Fundamentals

MongoDB stores data as **BSON documents** (JSON-like):

```javascript
{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "name": "Alice",
  "email": "alice@example.com",
  "created_at": ISODate("2024-01-15T10:30:00Z")
}
```

**Key differences from relational**:
- No fixed schema (documents in same collection can vary)
- Supports nested documents and arrays
- No enforced relationships (application-level)
- Optimized for document retrieval (not joins)

### Design Philosophy

**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.

---

## Embedding vs Referencing

### Embedding (Denormalization)

**Pattern**: Store related data inside the document.

```javascript
// 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**:
- Related data is always accessed together
- Related data doesn't change often
- Related data size is bounded
- One-to-one or one-to-few relationships

**Pros**:
- Single query to retrieve all data
- Atomicity (update entire document in one operation)
- Better performance (no joins)

**Cons**:
- Duplicates data if embedded document is shared
- Document size can grow large (16MB limit per document)
- Harder to update embedded data across multiple documents

### Referencing (Normalization)

**Pattern**: Store reference to another document (like foreign key).

```javascript
// 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**:
- Related data changes frequently
- Related data is large
- Unbounded relationships (one-to-many with many)
- Many-to-many relationships
- Need to query related data independently

**Pros**:
- No data duplication
- Smaller documents
- Can update related data once (affects all references)

**Cons**:
- Requires multiple queries or $lookup (join-like)
- No foreign key enforcement (application responsibility)
- Slower than embedding

---

## Decision Tree: Embed or Reference?

```
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
```

---

## Common Patterns

### Pattern 1: One-to-One (Embed)

**Use case**: User profile

```javascript
{
  "_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.

### Pattern 2: One-to-Few (Embed)

**Use case**: Blog post with comments (limited)

```javascript
{
  "_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).

### Pattern 3: One-to-Many (Bounded) - Hybrid

**Use case**: Product with reviews (potentially many)

**Option 1**: Embed first N reviews, reference rest

```javascript
// 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

```javascript
// 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):

```javascript
db.products.aggregate([
  { $match: { _id: ObjectId("...") } },
  { $lookup: {
      from: "reviews",
      localField: "_id",
      foreignField: "product_id",
      as: "reviews"
  }}
])
```

### Pattern 4: One-to-Many (Unbounded) - Reference

**Use case**: User with orders (grows indefinitely)

```javascript
// 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.

### Pattern 5: Many-to-Many (Reference with Junction)

**Use case**: Users and roles

**Option 1**: Embed role IDs in user

```javascript
// 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

```javascript
// 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("...")
}
```

---

## Avoiding Unbounded Arrays

**Problem**: Embedding unbounded arrays leads to document growth and performance issues.

```javascript
// ❌ BAD: Unbounded array
{
  "_id": ObjectId("..."),
  "user": "Alice",
  "posts": [  // Could grow to thousands
    { "title": "Post 1", "content": "..." },
    { "title": "Post 2", "content": "..." },
    // ... thousands more
  ]
}
```

**Solutions**:

### Solution 1: Reference Instead

```javascript
// 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" }
```

### Solution 2: Bucketing Pattern

**Use case**: Time-series data (sensor readings, logs)

```javascript
// 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**:
- Reduces document count (60 readings → 1 document)
- Bounded array size (max 60 readings)
- Efficient queries (one document per hour)

### Solution 3: Outlier Pattern

**Use case**: Products with varying review counts (most have few, some have thousands)

```javascript
// 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") })
```

---

## Schema Validation

MongoDB supports **JSON Schema validation** to enforce structure.

```javascript
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 documents
- `moderate`: Validate new documents, allow existing invalid ones

---

## Indexing Strategies

### Single Field Index

```javascript
db.users.createIndex({ email: 1 })  // Ascending
db.users.createIndex({ created_at: -1 })  // Descending
```

### Compound Index

```javascript
db.orders.createIndex({ user_id: 1, created_at: -1 })
```

**Order matters**: Can use for `user_id` alone, but not `created_at` alone.

### Unique Index

```javascript
db.users.createIndex({ email: 1 }, { unique: true })
```

### Sparse Index

**Use case**: Index field that doesn't exist in all documents.

```javascript
db.users.createIndex({ phone: 1 }, { sparse: true })
```

Only indexes documents with `phone` field.

### TTL Index (Auto-Delete)

**Use case**: Session data, temporary records.

```javascript
db.sessions.createIndex(
  { created_at: 1 },
  { expireAfterSeconds: 3600 }  // Delete after 1 hour
)
```

### Text Index (Full-Text Search)

```javascript
db.posts.createIndex({ title: "text", content: "text" })

// Search
db.posts.find({ $text: { $search: "mongodb tutorial" } })
```

---

## Common Pitfalls

❌ **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

---

## Migration from Relational

### Relational → MongoDB Mapping

| Relational | MongoDB |
|------------|---------|
| Table | Collection |
| Row | Document |
| Column | Field |
| Primary Key | `_id` field |
| Foreign Key | Reference (ObjectId) or embedded |
| JOIN | $lookup or embedded |
| Index | Index |

### Example: Blog Schema

**Relational**:
```sql
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)**:
```javascript
// 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)**:
```javascript
// 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.

---

## Quick Reference

### Embed When:
- Data accessed together
- One-to-one or one-to-few relationship
- Related data doesn't change often
- Related data is small

### Reference When:
- Data queried independently
- One-to-many (unbounded)
- Many-to-many
- Related data changes frequently
- Related data is large or shared

### Design Checklist

```
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
```

---

## Related Skills

- `postgres-schema-design.md` - Relational schema design for comparison
- `database-selection.md` - When to use MongoDB vs PostgreSQL
- `redis-data-structures.md` - Complementary data store for caching
- `orm-patterns.md` - ORM usage with MongoDB (Mongoose, etc.)

---

**Last Updated**: 2025-10-18
**Format Version**: 1.0 (Atomic)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.