MongoDB Best Practices

Essential guidelines for efficient and scalable MongoDB applications

✨ MongoDB Best Practices

Following MongoDB best practices ensures your database performs well, scales efficiently, and remains maintainable. These proven patterns cover schema design, indexing strategies, query optimization, and security measures that help you build robust, production-ready applications with MongoDB.


// Good practice: Create indexes for frequent queries
db.users.createIndex({ email: 1 })
db.users.find({ email: "[email protected]" })
                                    

Best Practice Categories

🏗️

Schema Design

Efficient data modeling

Embedding References Denormalization

Performance

Query optimization

Indexes Projections Limits
🔒

Security

Data protection

Authentication Validation Encryption
📊

Monitoring

Database health

Metrics Logs Alerts

🔹 Schema Design: Embed vs Reference

Choose between embedding and referencing based on your data relationships and access patterns. Embed related data that's always accessed together for better read performance. Use references for data that's accessed independently or when documents would exceed the 16MB limit with embedded data.

✅ When to Embed:

// ✅ GOOD - Embed one-to-few relationships
db.users.insertOne({
  name: "John Doe",
  email: "[email protected]",
  address: {
    street: "123 Main St",
    city: "Boston",
    zip: "02101"
  },
  recentOrders: [
    { id: 1, total: 50, date: "2024-01-15" },
    { id: 2, total: 75, date: "2024-01-20" }
  ]
})

// Single query gets all user data
const user = db.users.findOne({ email: "[email protected]" })

✅ When to Reference:

// ✅ GOOD - Reference one-to-many relationships
db.users.insertOne({
  _id: "user123",
  name: "John Doe",
  email: "[email protected]"
})

db.orders.insertMany([
  { userId: "user123", total: 50, items: [...] },
  { userId: "user123", total: 75, items: [...] },
  // ... hundreds more orders
])

// Query user and orders separately
const user = db.users.findOne({ _id: "user123" })
const orders = db.orders.find({ userId: "user123" })

Result:

✅ Optimal data structure for access patterns

✅ Documents stay under size limits

✅ Efficient queries

🔹 Create Indexes for Common Queries

Indexes dramatically improve query performance by allowing MongoDB to quickly locate documents without scanning the entire collection. Create indexes on fields you frequently query, sort, or use in joins. Monitor slow queries and add indexes strategically to optimize your most common operations.

// ❌ BAD - No index, slow query
db.users.find({ email: "[email protected]" })
// Scans all documents (COLLSCAN)

// ✅ GOOD - Create index first
db.users.createIndex({ email: 1 })
db.users.find({ email: "[email protected]" })
// Uses index (IXSCAN) - much faster!

// ✅ Compound index for multiple fields
db.products.createIndex({ category: 1, price: -1 })
db.products.find({ category: "Electronics" }).sort({ price: -1 })

// ✅ Text index for search
db.articles.createIndex({ title: "text", content: "text" })
db.articles.find({ $text: { $search: "mongodb tutorial" } })

// ✅ Unique index for constraints
db.users.createIndex({ email: 1 }, { unique: true })

// Check query performance
db.users.find({ email: "[email protected]" }).explain("executionStats")

Result:

✅ Queries execute 10-100x faster

✅ Reduced server load

✅ Better user experience

🔹 Use Projections to Limit Returned Fields

Projections reduce network bandwidth and improve performance by returning only the fields you need instead of entire documents. This is especially important for documents with large fields or when you only need a few specific fields for your application logic or display.

// ❌ BAD - Returns entire document (wasteful)
const users = db.users.find({})
// Returns: { _id, name, email, address, orders, 
//            preferences, history, ... }

// ✅ GOOD - Return only needed fields
const users = db.users.find(
  {},
  { name: 1, email: 1, _id: 0 }
)
// Returns: { name: "John", email: "[email protected]" }

// ✅ Exclude large fields
const products = db.products.find(
  { category: "Electronics" },
  { reviews: 0, fullDescription: 0 }
)
// Returns all fields except reviews and fullDescription

// ✅ Projection in aggregation
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $project: { 
      orderId: 1, 
      total: 1, 
      date: 1,
      _id: 0 
  }}
])

Result:

✅ Reduced network transfer

✅ Faster query response

✅ Lower memory usage

🔹 Implement Schema Validation

Schema validation ensures data consistency by enforcing rules on document structure and field types. Define validation rules when creating collections to prevent invalid data from being inserted. This catches errors early and maintains data quality across your application.

// ✅ GOOD - Create collection with validation
db.createCollection("users", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["name", "email", "age"],
      properties: {
        name: {
          bsonType: "string",
          description: "must be a string and is required"
        },
        email: {
          bsonType: "string",
          pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
          description: "must be a valid email"
        },
        age: {
          bsonType: "int",
          minimum: 0,
          maximum: 120,
          description: "must be an integer between 0 and 120"
        },
        status: {
          enum: ["active", "inactive", "pending"],
          description: "must be one of the enum values"
        }
      }
    }
  }
})

// ✅ Valid insert
db.users.insertOne({
  name: "John Doe",
  email: "[email protected]",
  age: 30,
  status: "active"
})

// ❌ Invalid insert - validation error
db.users.insertOne({
  name: "Jane",
  email: "invalid-email",  // Invalid format
  age: 150  // Exceeds maximum
})

Result:

✅ Data consistency enforced

✅ Invalid data rejected

✅ Fewer application errors

🔹 Use Bulk Operations for Multiple Writes

Bulk operations combine multiple write operations into a single request, significantly improving performance when inserting, updating, or deleting many documents. Instead of making hundreds of individual database calls, send them all at once to reduce network overhead and increase throughput.

// ❌ BAD - Multiple individual inserts (slow)
for (let i = 0; i < 1000; i++) {
  db.products.insertOne({ 
    name: `Product ${i}`, 
    price: i * 10 
  })
}
// 1000 separate database calls!

// ✅ GOOD - Bulk insert (fast)
const products = []
for (let i = 0; i < 1000; i++) {
  products.push({ 
    name: `Product ${i}`, 
    price: i * 10 
  })
}
db.products.insertMany(products)
// Single database call!

// ✅ Bulk write with mixed operations
db.products.bulkWrite([
  { 
    insertOne: { 
      document: { name: "New Product", price: 100 } 
    } 
  },
  { 
    updateOne: { 
      filter: { name: "Product 1" },
      update: { $set: { price: 150 } }
    } 
  },
  { 
    deleteOne: { 
      filter: { name: "Product 2" } 
    } 
  }
])

Result:

✅ 10-100x faster than individual operations

✅ Reduced network overhead

✅ Better database performance

🔹 Enable Authentication and Authorization

Always enable authentication in production to prevent unauthorized access to your database. Create users with specific roles and permissions following the principle of least privilege. Never use the default admin account for applications, and always use strong passwords or certificate-based authentication.

// ✅ GOOD - Create admin user
use admin
db.createUser({
  user: "adminUser",
  pwd: "strongPassword123!",
  roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
})

// ✅ Create application user with limited permissions
use myAppDB
db.createUser({
  user: "appUser",
  pwd: "appPassword456!",
  roles: [
    { role: "readWrite", db: "myAppDB" }
  ]
})

// ✅ Connect with authentication
const { MongoClient } = require('mongodb')
const uri = "mongodb://appUser:appPassword456!@localhost:27017/myAppDB"
const client = new MongoClient(uri)

// ✅ Use environment variables for credentials
const uri = `mongodb://${process.env.DB_USER}:${process.env.DB_PASS}@localhost:27017/myAppDB`

// ❌ BAD - No authentication
// mongodb://localhost:27017  // Anyone can access!

Result:

✅ Database secured from unauthorized access

✅ Users have appropriate permissions

✅ Credentials protected

🔹 Monitor and Optimize Query Performance

Regularly monitor your database performance using MongoDB's built-in tools. Use explain() to analyze query execution plans, identify slow queries, and find missing indexes. Set up profiling to automatically log slow operations and use monitoring tools to track database metrics over time.

// ✅ Use explain() to analyze queries
db.users.find({ email: "[email protected]" })
  .explain("executionStats")
// Shows: execution time, documents scanned, index used

// ✅ Enable database profiling
db.setProfilingLevel(1, { slowms: 100 })
// Logs queries taking > 100ms

// View slow queries
db.system.profile.find().sort({ ts: -1 }).limit(5)

// ✅ Check index usage
db.users.aggregate([
  { $indexStats: {} }
])

// ✅ Monitor current operations
db.currentOp()

// ✅ Database statistics
db.stats()
db.users.stats()

// ✅ Create index for slow query
// If explain shows COLLSCAN, add index:
db.users.createIndex({ email: 1 })

Result:

✅ Identify performance bottlenecks

✅ Optimize slow queries

✅ Proactive performance management

🧠 Test Your Knowledge

What is the main benefit of using projections in MongoDB queries?