By now you know what MongoDB does. This lesson is about where to point it. The honest answer has two halves, and the second half is the useful one — being able to say “not this one” is worth more than another list of strengths.
One framing to carry through: this is a decision per workload, not per company. Plenty of good systems run a relational database for orders and MongoDB for the content catalogue. That is not indecision, it is using each tool for what it is.
Five shapes that fit well
1. Content and catalogues
Products, listings, articles, media libraries. The defining feature is that every item has a different set of attributes: a laptop has RAM and a screen size, a t-shirt has a size and a colour. Relationally that becomes two hundred nullable columns, an entity-attribute-value table nobody can query, or a JSON column — and if the answer is a JSON column, a document store is the honest version of the same idea.
db.products.find({ category: "laptop", "specs.ram_gb": { $gte: 16 } })2. User profiles, sessions and preferences
Anything read as a whole object, keyed by one id. The access pattern is “give me everything about user X”, which is one document and one read rather than a six-way join. Preferences and feature flags change shape constantly as a product evolves, and adding a field needs no migration. A TTL index expires sessions for free.
3. Events, telemetry and audit logs
High write volume, append-only, rarely updated, queried by time range. Time-series collections and TTL indexes exist for exactly this, and the aggregation pipeline does the roll-ups.
ReelCMS records one document per playback, and the shape is the giveaway — everything queried alongside the timestamp lives in the metadata:
public static class ViewMetadata {
@Field(targetType = FieldType.OBJECT_ID)
private String reelId;
@Field(targetType = FieldType.OBJECT_ID)
private String creatorId;
private String country;
private String device;
}4. Real-time dashboards
Aggregation is strong, and change streams let a dashboard update without
polling. The pattern that scales is to materialise an expensive result rather than recomputing it
per request — $merge writes a pipeline’s output into a collection you can
then read in one query:
db.view_events.aggregate([
{ $match: { ts: { $gte: since } } },
{ $group: { _id: { $dateTrunc: { date: "$ts", unit: "day" } }, views: { $sum: 1 } } },
{ $merge: { into: "daily_views", on: "_id", whenMatched: "replace" } }
])Run that on a schedule and the dashboard reads daily_views directly. The
alternative — aggregating thirty thousand events on every page load — works fine in
development and stops working at exactly the moment the product succeeds.
5. Mobile back ends and early-stage products
JSON in, JSON out — no impedance mismatch between the API layer and storage. The schema evolves as fast as the requirements do, which matters most in the first six months of a product when nobody knows what the questions are yet.
Four where it is the wrong answer
1. Money, ledgers and inventory
Anything where multi-record correctness is the normal case rather than the exception. MongoDB has had transactions since 4.0, but they are a bolt-on with a real cost, not the default mode of operation. A relational database was built for this and a decade of your accountants’ questions will be easier to answer there.
2. Deeply relational data
If the interesting questions are naturally five joins deep — which suppliers shipped parts
used in contracts signed last quarter — you will hand-write $lookup chains that
a query planner would do better and faster. Fighting the tool.
3. Reporting over unknown future queries
Document modelling rewards knowing your access patterns, because you shape documents around them. Ad-hoc analytics, where the next question is genuinely unknown, is where a normalised schema and a mature planner pay off.
4. A schema several teams must agree on
When the database is the contract between services, having it enforce that contract is a feature rather than friction. Schema validation narrows this gap but it is opt-in, per collection, and easy to skip.
A shorter way to decide
Six questions, and the honest answers usually settle it before you have finished the list:
| Question | MongoDB | Relational |
|---|---|---|
| Do records share one fixed shape? | No — they vary | Yes |
| Do you know your top three queries? | Yes | Not yet, or all of them |
| Is most reading “one whole thing by id”? | Yes | No — it joins |
| Must multi-record writes be exact? | Rarely | Routinely |
| Who owns the schema contract? | The application | Several teams |
| How does it grow? | Out, across machines | Up, bigger machine |
Mostly the left column? MongoDB, comfortably. Mostly the right? Use Postgres and be happier. Genuinely split down the middle usually means you have two workloads, not one — which is the per-workload answer from the top of this lesson arriving by a different route.
You are not locked in either way
One reason this decision feels heavier than it is: people treat it as permanent. It is not, in either direction, and knowing the escape routes makes it easier to commit.
Every relational database worth using now has a JSON column type, so “we need a flexible attribute bag” is not on its own a reason to move a whole system. Going the other way, a document store can hold normalised data with ids and joins — badly, but it works while you migrate.
What genuinely does not migrate cheaply is a shard key, and a schema whose denormalization has spread far enough that nobody can say which copy is authoritative. Both are worth avoiding until you are sure, and neither is a day-one decision.
Two signals from your own codebase
Two things are worth watching once you are running, because they tell you the model has drifted from the workload.
You are writing joins on a hot path. One $lookup in a nightly
report is fine. A $lookup on every page load means data that belongs together is
stored apart — either denormalize, or accept that the shape is relational.
You are reaching for transactions routinely. An occasional one is normal. If most writes need one, the documents are drawn along the wrong boundaries. ReelCMS embeds its counters inside the reel for precisely this reason, so incrementing them is atomic without a session:
public class ReelStats {
@Builder.Default
private long views = 0;
@Builder.Default
private long likes = 0;
@Builder.Default
private long comments = 0;
@Builder.Default
private long shares = 0;
}Both signals point at the schema rather than the database. That is usually where the fix is.
The test, one more time
Write down the three queries that will run on every page load. If you can, MongoDB will let you shape documents around them and turn your hottest read into a single lookup. If you cannot — if the honest answer is that you do not know yet, or that it is all of them — normalise now and denormalize later, once the questions have shown up.
The short version
- Good fits: varied catalogues, whole-object reads, event streams, live dashboards, fast-moving early products.
- Bad fits: money, deeply relational data, ad-hoc reporting, cross-team schema contracts.
- Choose per workload. Two databases in one system is a normal answer.
- Joins on a hot path and routine transactions are both schema smells, not database problems.