Draft / Scheduled Content
This article is a draft or scheduled for future publication. The content is subject to change.
Why MongoDB is Still the Wrong Choice for 99% of Projects
The “Web Scale” Delusion
In the early 2010s, MongoDB took the tech world by storm.
Supported by the infamous “MongoDB is Web Scale” meme, developers flocked to document databases. The pitch was incredibly appealing: “Ditch the constraints of SQL tables, schemas, migrations, and joins! Store your data as flexible, nested JSON documents. If you want to add a field to a record, just save it! Let your database evolve as fast as your code.”
It felt like developer liberation. No more running ALTER TABLE migrations. No more arguing about foreign keys. Just throw JSON at the database and get on with your day.
But as these applications matured, developers made a painful discovery: your data always has a schema.
By choosing a “schemaless” database, you didn’t eliminate the schema. You just moved the responsibility of enforcing that schema from the database engine (which is optimized for it) to your application code (which is terrible at it).
MongoDB remains the default database choice for many bootcamps and starters. It is almost always the wrong choice.
The Lie of “Schemaless”
The core selling point of MongoDB is that it is “schemaless.” You can write any document to any collection, regardless of its structure:
// Document 1 in "users" collection
{ "id": 1, "name": "Alice", "email": "[email protected]" }
// Document 2 in "users" collection
{ "id": 2, "name": "Bob", "contact_info": { "work_email": "[email protected]" } }
This looks flexible. But think about what happens when your application needs to read this data:
// Application code must handle every historical variation
const email = user.email || (user.contact_info && user.contact_info.work_email);
As your product evolves and your data model changes, your database ends up containing a geological record of every schema version you have ever deployed.
Because the database didn’t enforce a schema at write time, your application code must handle every single historical variation at read time.
Your codebase becomes bloated with safety checks, try-catch blocks, and type guards. If you miss a single edge case, your application crashes or displays corrupted data to the user.
A relational database solves this at the gate. If a field is required, the database rejects the write if the field is missing. If you rename a column, you run a migration that updates all existing records instantly.
The database guarantees data integrity. You can write your application code assuming the data is clean and consistent.
The Death of Relational Integrity
Most business data is relational.
- An order belongs to a user.
- A product has many categories.
- A comment references a post.
In a relational database, you enforce these relationships using Foreign Keys. If you try to delete a user who has active orders, the database prevents it. If you try to create an order for a user ID that doesn’t exist, the database rejects it.
In MongoDB, there are no foreign keys. Relationships are just strings holding IDs (userId: "123").
If a user account is deleted, their orders become orphaned records in the database. If your application code has a bug or network failure during the delete process, the database is corrupted.
You end up writing background scripts to clean up orphaned records and verify data integrity—rebuilding the basic features of a relational database database engine in your own application space.
Relational Database (Guaranteed Consistency)
+-------------------------------------------------+
| Users Table <--- [Foreign Key Constraint] |
| | |
| Orders Table <------+ |
+-------------------------------------------------+
Database engine prevents orphaned records.
NoSQL Database (No Constraints)
+-------------------------------------------------+
| Users Collection Orders Collection |
| { id: 1 } { user_id: 1 } |
| |
| *Delete user 1* ---> Order 1 is now orphaned! |
+-------------------------------------------------+
The Joins Performance Tax
MongoDB advocates argue that you should nest your relations inside the document (e.g., nesting orders inside the user document).
This works for tiny, static relationships (like a user’s addresses). But it fails for unbound relationships (like a user’s orders or a product’s reviews). A document in MongoDB has a hard size limit of 16MB. If a user buys hundreds of items, you cannot nest them.
So, you must reference them by ID. And when you need to query them together, you must run MongoDB’s $lookup operator (a join).
MongoDB’s lookup is notoriously slow and resource-intensive compared to SQL’s native, index-optimized joins. Relational databases have spent 40 years optimizing join algorithms (Nested Loop, Hash Join, Merge Join). MongoDB’s lookup is a secondary, bolted-on feature that doesn’t scale well under high concurrency.
The True Web Scale Solution: PostgreSQL
If you genuinely need document flexibility—if you have dynamic, user-defined fields or unstructured payloads—you still don’t need MongoDB.
PostgreSQL has built-in JSONB support.
You can define a table with structured columns for your core, relational data (like IDs, emails, and timestamps) and a JSONB column for your dynamic data:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
metadata JSONB -- Dynamic, schemaless fields
);
You can query, index, and modify keys inside the JSONB column using SQL:
-- Query users where metadata ->> 'role' is 'admin'
SELECT * FROM users WHERE metadata->>'role' = 'admin';
With PostgreSQL, you get the best of both worlds:
- Statically typed, relational integrity for your core database.
- High-performance, indexed JSON documents for your dynamic data.
- Full ACID transactional guarantees.
Default to SQL
Unless you are building a telemetry logging system that processes gigabytes of write-only events per second (where Cassandra or a time-series DB makes sense), or you have a highly specialized graph query requirement, choose a relational database.
Specifically, choose PostgreSQL.
It is the most reliable, feature-rich, and optimized general-purpose database in existence. It will keep your data clean, your queries fast, and your application simple.
Related Content
Why ORMs are the Worst Anti-Pattern in Modern Backend Development
Object-Relational Mapping (ORM) tools promise to free developers from SQL. In exchange, they introduce the N+1 query problem, hide database performance realities behind black-box abstractions, and create models that couple application state to database schemas. It's time to write SQL again.
Your API Doesn't Need Versioning
The default approach to API design is to prefix routes with version numbers like /api/v1/ and /api/v2/. In practice, route versioning leads to massive code duplication, client migration paralysis, and complex routing tables. Designing backward-compatible APIs is a much better operational model.
Rust is Great, but You're Probably Using it for the Wrong Reasons
Rust is the darling of the systems programming world, boasting safety and performance without a garbage collector. But rewriting your company's simple HTTP CRUD API in Rust is likely a waste of time and money. Here is why the productivity hit of Rust is rarely worth it for typical web applications.