Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
MongoDB is a document-oriented NoSQL database used for high volume data storage. Instead of using tables and rows as in traditional relational databases, MongoDB uses collections and documents with a JSON-like structure.
MongoDB, despite its flexibility and scalability, has several common anti-patterns that can lead to performance issues, maintenance problems, and data inconsistency. Here are the most important anti-patterns to avoid when working with MongoDB.
// Anti-pattern: Querying without indexes
db.users.find({ email: "user@example.com" });
// This query will perform a full collection scan if there's no index on the email field
// Better approach: Create appropriate indexes
db.users.createIndex({ email: 1 }, { unique: true });
// Now the query will use the index
db.users.find({ email: "user@example.com" });
// Anti-pattern: Not analyzing query performance
db.orders.find({ status: "processing", createdAt: { $gt: new Date('2023-01-01') } }).sort({ createdAt: -1 });
// Better approach: Use explain() to analyze query performance
db.orders.find({ status: "processing", createdAt: { $gt: new Date('2023-01-01') } }).sort({ createdAt: -1 }).explain("executionStats");
// Create a compound index based on the query pattern
db.orders.createIndex({ status: 1, createdAt: -1 });
Not using proper indexes leads to full collection scans and poor query performance. Create indexes based on your query patterns and use explain()
to verify that your queries are using indexes efficiently.
// Anti-pattern: Deeply nested documents
db.customers.insertOne({
name: "John Doe",
contact: {
email: "john@example.com",
phone: {
home: "123-456-7890",
work: "098-765-4321",
cell: {
primary: "555-123-4567",
secondary: "555-765-4321"
}
},
address: {
home: {
street: "123 Main St",
city: "Anytown",
state: "CA",
zip: "12345",
country: {
name: "United States",
code: "US"
}
},
work: { /* similarly nested */ }
}
},
// More deeply nested fields
});
// Better approach: Flatten the structure
db.customers.insertOne({
name: "John Doe",
email: "john@example.com",
phoneHome: "123-456-7890",
phoneWork: "098-765-4321",
phoneCellPrimary: "555-123-4567",
phoneCellSecondary: "555-765-4321",
homeStreet: "123 Main St",
homeCity: "Anytown",
homeState: "CA",
homeZip: "12345",
homeCountry: "United States",
homeCountryCode: "US",
// Work address fields similarly flattened
});
// Or use subdocuments with limited nesting
db.customers.insertOne({
name: "John Doe",
email: "john@example.com",
phones: {
home: "123-456-7890",
work: "098-765-4321",
cellPrimary: "555-123-4567",
cellSecondary: "555-765-4321"
},
homeAddress: {
street: "123 Main St",
city: "Anytown",
state: "CA",
zip: "12345",
country: "United States",
countryCode: "US"
},
workAddress: {
// Similar structure
}
});
Deeply nested documents are difficult to query and update. Limit nesting to 2-3 levels and consider flattening your document structure for better performance and maintainability.
// Anti-pattern: Document with a massive array
db.products.insertOne({
name: "Popular Product",
sku: "PP-123",
// Thousands of reviews in a single document
reviews: [
{ user: "user1", rating: 5, comment: "Great product!" },
{ user: "user2", rating: 4, comment: "Good product." },
// ... thousands more reviews
]
});
// Better approach: Use a separate collection for reviews
db.products.insertOne({
name: "Popular Product",
sku: "PP-123"
});
db.reviews.insertMany([
{ productSku: "PP-123", user: "user1", rating: 5, comment: "Great product!" },
{ productSku: "PP-123", user: "user2", rating: 4, comment: "Good product." },
// ... thousands more reviews as separate documents
]);
// Create an index on productSku for efficient queries
db.reviews.createIndex({ productSku: 1 });
// Query for a product's reviews
db.reviews.find({ productSku: "PP-123" }).sort({ rating: -1 });
Massive arrays in documents can lead to performance issues and hit the 16MB document size limit. Use separate collections for one-to-many relationships with high cardinality.
// Anti-pattern: Multiple queries and client-side processing
// Step 1: Get all orders for a customer
const orders = db.orders.find({ customerId: "12345" }).toArray();
// Step 2: Calculate total spent in application code
let totalSpent = 0;
for (const order of orders) {
totalSpent += order.total;
}
// Step 3: Get customer details
const customer = db.customers.findOne({ _id: "12345" });
// Step 4: Combine data in application code
const result = {
customer: customer.name,
email: customer.email,
totalOrders: orders.length,
totalSpent: totalSpent,
averageOrderValue: totalSpent / orders.length
};
// Better approach: Use the aggregation framework
const result = db.orders.aggregate([
{ $match: { customerId: "12345" } },
{ $group: {
_id: "$customerId",
totalOrders: { $sum: 1 },
totalSpent: { $sum: "$total" },
averageOrderValue: { $avg: "$total" }
}
},
{ $lookup: {
from: "customers",
localField: "_id",
foreignField: "_id",
as: "customerDetails"
}
},
{ $unwind: "$customerDetails" },
{ $project: {
_id: 0,
customer: "$customerDetails.name",
email: "$customerDetails.email",
totalOrders: 1,
totalSpent: 1,
averageOrderValue: 1
}
}
]).toArray();
Using multiple queries and processing data in application code is inefficient. Use MongoDB’s aggregation framework to process data on the server side, reducing network traffic and improving performance.
// Anti-pattern: No transactions for operations that should be atomic
// Transfer funds between accounts
db.accounts.updateOne(
{ _id: sourceAccountId },
{ $inc: { balance: -amount } }
);
// If an error occurs here, the system is in an inconsistent state
db.accounts.updateOne(
{ _id: destinationAccountId },
{ $inc: { balance: amount } }
);
// Better approach: Use transactions for atomicity
const session = client.startSession();
session.startTransaction();
try {
await db.accounts.updateOne(
{ _id: sourceAccountId, balance: { $gte: amount } },
{ $inc: { balance: -amount } },
{ session }
);
await db.accounts.updateOne(
{ _id: destinationAccountId },
{ $inc: { balance: amount } },
{ session }
);
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
Not using transactions for operations that need to be atomic can lead to data inconsistency. Use transactions when you need to ensure that multiple operations succeed or fail as a unit.
// Anti-pattern: No schema validation
db.users.insertOne({
name: "John Doe",
email: "not-an-email", // Invalid email format
age: "thirty" // Should be a number
});
// Better approach: Use schema 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 address and is required"
},
age: {
bsonType: "int",
minimum: 0,
maximum: 120,
description: "must be an integer between 0 and 120 and is required"
}
}
}
},
validationLevel: "strict",
validationAction: "error"
});
Not using schema validation can lead to inconsistent data. Use MongoDB’s schema validation to enforce data integrity and structure.
// Anti-pattern: Using $where queries
db.orders.find({
$where: function() {
return this.total > 100 && this.items.length > 5;
}
});
// Better approach: Use standard query operators
db.orders.find({
total: { $gt: 100 },
"items.5": { $exists: true }
});
Using $where
queries is slow and can be a security risk (potential for injection attacks). Use standard MongoDB query operators instead.
// Anti-pattern: Creating new connections for each operation
async function getUserById(id) {
const client = new MongoClient(uri);
try {
await client.connect();
const db = client.db("mydb");
return await db.collection("users").findOne({ _id: id });
} finally {
await client.close();
}
}
// Better approach: Use connection pooling
// Initialize once at application startup
const client = new MongoClient(uri, {
useUnifiedTopology: true,
maxPoolSize: 50
});
await client.connect();
const db = client.db("mydb");
// Reuse the connection for operations
async function getUserById(id) {
return await db.collection("users").findOne({ _id: id });
}
// Close the connection when the application shuts down
process.on("SIGINT", async () => {
await client.close();
process.exit();
});
Creating new connections for each operation is inefficient. Use connection pooling to reuse connections and improve performance.
// Anti-pattern: Using default write concern
db.orders.insertOne({
customer: "John Doe",
total: 99.99,
items: [/* ... */]
});
// Better approach: Specify appropriate write concern
db.orders.insertOne(
{
customer: "John Doe",
total: 99.99,
items: [/* ... */]
},
{ writeConcern: { w: "majority", wtimeout: 1000 } }
);
Not specifying write concerns can lead to data loss in case of failures. Use appropriate write concerns based on your application’s durability requirements.
// Anti-pattern: Retrieving entire documents when only a few fields are needed
const users = await db.users.find({ age: { $gt: 18 } }).toArray();
// Client only uses name and email
const result = users.map(user => ({
name: user.name,
email: user.email
}));
// Better approach: Use projection to retrieve only needed fields
const result = await db.users.find(
{ age: { $gt: 18 } },
{ projection: { name: 1, email: 1, _id: 0 } }
).toArray();
Retrieving entire documents when only a few fields are needed wastes bandwidth and memory. Use projection to retrieve only the fields you need.
// Anti-pattern: Polling for changes
async function pollForChanges() {
const lastCheckTime = new Date(Date.now() - 60000); // Last minute
const changes = await db.orders.find({
updatedAt: { $gt: lastCheckTime }
}).toArray();
// Process changes
// Poll again after delay
setTimeout(pollForChanges, 5000);
}
// Better approach: Use change streams
const changeStream = db.orders.watch();
changeStream.on("change", change => {
// Process change in real-time
console.log("Received change:", change);
if (change.operationType === "insert") {
// Handle new order
} else if (change.operationType === "update") {
// Handle order update
} else if (change.operationType === "delete") {
// Handle order deletion
}
});
// Handle errors
changeStream.on("error", error => {
console.error("Change stream error:", error);
// Reconnect logic
});
Polling for changes is inefficient and can miss updates. Use change streams for real-time notifications of database changes.
// Anti-pattern: Using strings for IDs, dates, and numeric values
db.orders.insertOne({
orderId: "12345", // Should be ObjectId or a numeric ID
customerId: "67890", // Should be ObjectId or a numeric ID
orderDate: "2023-05-15", // Should be a Date object
total: "99.99", // Should be a numeric type
items: [/* ... */]
});
// Better approach: Use appropriate data types
db.orders.insertOne({
_id: new ObjectId(), // MongoDB's ObjectId
customerId: new ObjectId("507f1f77bcf86cd799439011"),
orderDate: new Date("2023-05-15"),
total: 99.99, // Numeric value
items: [/* ... */]
});
Using inappropriate data types makes queries and aggregations more complex and less efficient. Use the appropriate BSON data types for your data.