MongoDB gets picked by default far more often than its actual data model justifies, and it gets criticized far more harshly than its actual data model deserves — the truth for most projects is somewhere in between.
MongoDB is a document database that stores data as BSON (binary JSON) documents grouped into collections, without a fixed schema enforced by the database itself. That flexibility is genuinely valuable for data that's naturally nested and doesn't need cross-document joins — but it's also the source of most "MongoDB doesn't scale" complaints, which are usually actually "this data was modeled relationally in a document database" complaints.
Why MongoDB Matters (and When to Skip It)
MongoDB shines when your data's natural shape is a self-contained document — a blog post with embedded comments, a user profile with nested preferences, an event log entry with a variable-shape payload. Reading one document gets you everything you need in one query, no joins required, which is both simpler to reason about and often faster for that specific access pattern.
Skip MongoDB when your data has genuine relational structure with frequent cross-entity queries — an e-commerce system with orders, inventory, and users that constantly need to be joined and kept consistent is usually a worse fit than Postgres, despite MongoDB's own relational features ($lookup, transactions) having improved significantly.
Getting Started with MongoDB
A document model that embraces embedding for naturally nested data:
interface BlogPost {
_id: ObjectId;
title: string;
authorId: ObjectId;
comments: {
text: string;
userId: ObjectId;
createdAt: Date;
}[];
tags: string[];
createdAt: Date;
}
const posts = db.collection<BlogPost>("posts");
await posts.insertOne({
title: "Understanding Document Databases",
authorId: userId,
comments: [],
tags: ["mongodb", "database"],
createdAt: new Date(),
});
Querying with the native driver:
const recentPosts = await posts
.find({ tags: "mongodb" })
.sort({ createdAt: -1 })
.limit(10)
.toArray();
Core MongoDB Concepts Every Developer Should Know
Embedding vs. referencing is the central modeling decision. Embed data that's always accessed together and doesn't grow unbounded (comments on a post, up to a reasonable limit); reference data via ObjectId when it's shared across many documents or grows without bound (a user referenced by thousands of posts shouldn't be embedded in each one).
// embed: read together, bounded growth
{ post: { comments: [...] } }
// reference: shared, unbounded, or independently queried
{ post: { authorId: ObjectId("...") } }
Indexes matter exactly as much as in a relational database. A query filtering on an unindexed field triggers a full collection scan — explain() shows you whether an index is actually being used:
await posts.createIndex({ tags: 1 });
await posts.find({ tags: "mongodb" }).explain("executionStats");
The aggregation pipeline is MongoDB's answer to complex queries and joins. $lookup performs a join-like operation across collections when you do need cross-document data:
const results = await posts.aggregate([
{ $match: { tags: "mongodb" } },
{ $lookup: { from: "users", localField: "authorId", foreignField: "_id", as: "author" } },
{ $unwind: "$author" },
{ $project: { title: 1, "author.name": 1 } },
]).toArray();
Multi-document transactions exist and work, contrary to older assumptions about MongoDB — for operations that genuinely need atomicity across multiple documents/collections, session.withTransaction() provides real ACID guarantees, though reaching for them frequently is often a sign the data model itself should be reconsidered.
Common MongoDB Mistakes and How to Fix Them
Mistake 1: modeling relational data with heavy $lookup usage everywhere. If most queries need to join three or four collections, you're likely fighting the document model rather than using it. Fix: either embed more aggressively where access patterns allow, or reconsider whether Postgres fits this specific data better.
Mistake 2: unbounded array growth inside a document. Embedding comments directly in a post document works until a viral post accumulates thousands of comments, hitting MongoDB's 16MB document size limit. Fix: cap embedded arrays with pagination logic, or move to a referenced collection once growth is genuinely unbounded.
Mistake 3: no schema validation at all. Skipping MongoDB's optional schema validation ($jsonSchema) means a typo or bug can silently insert malformed documents that break assumptions elsewhere in the app. Fix: define validation rules at the collection level even though MongoDB doesn't require it — it catches bugs early instead of at read time.
When Should You Use MongoDB Instead of PostgreSQL?
Use MongoDB when your data is naturally document-shaped, access patterns favor reading whole documents at once, and you value schema flexibility for evolving data shapes (like variable event payloads or user-generated content with unpredictable structure). Use PostgreSQL when relational integrity, complex joins, and strict consistency across entities matter more than schema flexibility.
MongoDB in Production
Use MongoDB Atlas (the managed service) for production unless you have a specific reason to self-host — it handles replication, backups, and scaling operations that are meaningfully more work to manage yourself. Also set up schema validation and indexes deliberately from the start; MongoDB's flexibility is a feature during prototyping and a liability if it's still the production posture with real user data and no validation guardrails.
Before defaulting to MongoDB for a new project, sketch your top five queries first — if most of them need data from multiple collections, that's a strong signal Postgres fits the actual access pattern better.