Skip to content
Back to all concepts
DatabaseNoSQLMongoDBCouchDB

Document Databases

A common type of NoSQL database. Storing data in flexible, JSON-like documents with embedded structures and dynamic schemas.

By ScaleWikiUpdated 3 min readBeginner

Document Databases

Document databases (like MongoDB, CouchDB, Cosmos DB) store data in documents (JSON, BSON, XML) rather than rows and columns.

Key Characteristics

  1. Flexible Schema: Documents can have different fields, and schema validation can enforce required structure. One document can have email and phone, while the next one only has email and twitter_handle.
  2. Hierarchical Data: You can store arrays and nested objects directly. A normalized relational design might use Users and Addresses; SQL databases can also store JSON or array values. In Document DBs, it's just one object.
json
{
  "_id": 1,
  "name": "Alice",
  "addresses": [
    { "street": "123 Main St", "type": "Home" },
    { "street": "456 Work Ave", "type": "Office" }
  ]
}

Embed vs Reference: The Core Decision

The biggest decision in Document DBs is Embed vs Reference.

1. Embedding (Denormalization)

Storing related data inside the parent document.

Example: E-Commerce Products

javascript
{
  "_id": "prod_123",
  "name": "Laptop",
  "price": 999,
  "reviews": [  // Embedded
    {
      "user": "Alice",
      "rating": 5,
      "text": "Great laptop!",
      "date": "2024-01-15"
    },
    {
      "user": "Bob",
      "rating": 4,
      "text": "Good value"
    }
  ]
}
  • Pros: Fast reads. One query gets everything. No JOINs.
  • Cons: Data duplication. If current product information is duplicated, changes require synchronization. A historical order snapshot should usually preserve the purchased name and price instead.
  • Best For: One-to-Few relationships, data that is read together and rarely changes.

2. Referencing (Normalization)

Storing just the ID of the related data (like a Foreign Key).

Example: Blog Platform

javascript
// Posts collection
{
  "_id": "post_001",
  "title": "System Design 101",
  "author_id": "user_123",  // Reference
  "content": "...",
  "comment_ids": ["comment_001", "comment_002"]  // References
}

// Comments collection
{
  "_id": "comment_001",
  "post_id": "post_001",
  "user_id": "user_456",
  "text": "Great post!"
}
  • Pros: No duplication. Smaller documents. Easy updates.
  • Cons: Slower reads. May require multiple queries or a supported server-side join such as MongoDB $lookup.
  • Best For: One-to-Many (Unbounded) relationships (e.g., a Viral Post with 1M comments - don't embed 1M comments!).

Advanced Patterns

Hybrid: Embed First N, Reference the Rest

javascript
{
  "_id": "post_viral",
  "title": "Going Viral",
  "comments_preview": [  // Embed first 10
    { "user": "Alice", "text": "First!" },
    // ... 9 more
  ],
  "total_comments": 50000,
  "comments_overflow_ref": "comments_post_viral"  // Reference to overflow collection
}

Bucketing Pattern

For time-series or unbounded arrays:

javascript
// Instead of one giant document with 100k events
{
  "user_id": "user_123",
  "year": 2024,
  "month": 1,
  "events": [
    { "type": "login", "timestamp": "2024-01-01T10:00:00Z" },
    // ... ~1000 events per month
  ]
}

Why? MongoDB has a 16MB document size limit.

Indexing for Performance

Single Field Index

javascript
db.users.createIndex({ email: 1 })  // 1 = ascending

// Query uses index
db.users.find({ email: "alice@example.com" })  // Fast!

Compound Index

javascript
db.products.createIndex({ category: 1, price: -1 })

// This query uses the index efficiently
db.products.find({ category: "Electronics" }).sort({ price: -1 })

Multikey Index (Arrays)

When you create an index on an array field, MongoDB represents it as a multikey index:

javascript
db.posts.createIndex({ tags: 1 })

// Works on arrays!
db.posts.find({ tags: "system-design" })
javascript
db.posts.createIndex({ title: "text", content: "text" })

db.posts.find({ $text: { $search: "distributed systems" } })

Aggregation Pipeline

MongoDB's killer feature for analytics:

javascript
db.orders.aggregate([
  // Stage 1: Filter orders from 2024
  { $match: { date: { $gte: new Date("2024-01-01"), $lt: new Date("2025-01-01") } } },
  
  // Stage 2: Group by customer
  {
    $group: {
      _id: "$customer_id",
      total_spent: { $sum: "$amount" },
      order_count: { $sum: 1 }
    }
  },
  
  // Stage 3: Sort by total spent
  { $sort: { total_spent: -1 } },
  
  // Stage 4: Top 10 customers
  { $limit: 10 }
])

Illustrative output (depends on the stored data):

javascript
[
  { "_id": "cust_456", "total_spent": 15000, "order_count": 25 },
  { "_id": "cust_789", "total_spent": 12000, "order_count": 18 }
]

Transactions (Modern MongoDB)

MongoDB 4.0+ supports multi-document ACID transactions:

javascript
const session = client.startSession();
session.startTransaction();

try {
  // Transfer inventory
  await db.inventory.updateOne(
    { warehouse: "WH1", product: "laptop" },
    { $inc: { quantity: -1 } },
    { session }
  );
  
  await db.inventory.updateOne(
    { warehouse: "WH2", product: "laptop" },
    { $inc: { quantity: 1 } },
    { session }
  );
  
  await session.commitTransaction();
} catch (error) {
  await session.abortTransaction();
} finally {
  session.endSession();
}

Note: Transactions are slower than single-document atomicity. Use sparingly.

Real-World Use Cases

1. Content Management Systems

Why Document DB:

  • Articles have varying fields (some have videos, some have galleries)
  • Easy schema evolution
javascript
{
  "title": "System Design Guide",
  "author": "Alice",
  "sections": [
    { "type": "text", "content": "..." },
    { "type": "code", "language": "python", "code": "..." },
    { "type": "image", "url": "...", "caption": "..." }
  ]
}

2. Product Catalogs

Why Document DB:

  • Different product types have different attributes
  • A document model keeps varying attributes together; relational JSON columns are another option

3. User Profiles (Polymorphic Data)

javascript
// Free user
{ "type": "free", "name": "Bob", "email": "..." }

// Premium user - different schema!
{
  "type": "premium",
  "name": "Alice",
  "email": "...",
  "subscription": {
    "plan": "pro",
    "billing_cycle": "annual",
    "features": ["unlimited_storage", "priority_support"]
  }
}

Performance Best Practices

1. Projection (Fetch Only What You Need)

javascript
// Bad: Fetch entire document
db.users.find({ email: "alice@example.com" })

// Good: Project only needed fields
db.users.find(
  { email: "alice@example.com" },
  { name: 1, email: 1, _id: 0 }  // Only name and email
)

2. Avoid Large Arrays in Documents

javascript
// Bad: Unbounded array (will hit 16MB limit)
{
  "user_id": "user_123",
  "activity_log": [
    // ... 1 million entries (100MB!)
  ]
}

// Good: Use bucketing or separate collection

3. Covered Queries

If index contains all queried fields, MongoDB never touches the document:

javascript
db.users.createIndex({ email: 1, name: 1 })

// Covered! Only uses index
db.users.find(
  { email: "alice@example.com" },
  { name: 1, _id: 0 }
)

Common Pitfalls

⚠️ Over-Embedding: Embedding a product in 1M orders makes updates expensive.

⚠️ Under-Referencing: Not using references for large related datasets causes document bloat.

⚠️ Missing Indexes: Queries on unindexed fields scan entire collection (slow!).

⚠️ Ignoring Document Size Limit: 16MB max per document.

Interview Tips 💡

  1. Explain schema flexibility: "Product catalog with variable attributes fits document model naturally"
  2. Embed vs Reference decision: "Embed addresses (1-to-few), reference orders (1-to-many)"
  3. Modeling example: "A product catalog can embed bounded attributes while referencing an unbounded review collection."
  4. Acknowledge limitations: "Document DBs aren't ideal for complex multi-collection joins"
  5. Mention aggregation: "MongoDB aggregation pipeline is like SQL GROUP BY on steroids"

MongoDB vs SQL Decision Matrix

Use SQL WhenUse Document DB When
Complex multi-table joinsHierarchical/nested data
Fixed schema, rare changesSchema changes frequently
Strong referential integrityFlexible polymorphic data
Relational constraints and joins fit the workloadDocuments align with read/update boundaries

Sources and a design check

MongoDB transactions documents multi-document support and operational considerations. For this article's inventory example, enforce a positive available quantity in the update filter, check the matched result, and use the driver's transaction retry pattern; a transaction alone does not prevent a negative inventory balance.