"Schemaless" is the word that gets MongoDB adopted, and the word that gets it blamed for a mess two years later. MongoDB doesn't remove the need for schema design — it moves the decision from "what does the constraint allow" to "what does my application actually query for," a different design process, not an absence of one.

Model around access patterns, not entities

The relational instinct is to model entities and relationships first, then write queries against that structure. MongoDB design works backward from the query: what does the application actually fetch together, how often, in what shape. A blog post and its comments are a textbook example of where this changes the answer. In a relational schema, comments are naturally a separate table with a foreign key. In MongoDB, if comments are always fetched and rendered together with their post, and there aren't so many that the document becomes unwieldy, embedding them avoids a second round trip entirely.

javascript
// Embedded: one read gets the post and its comments together
db.posts.insertOne({
  title: "Understanding Replication Lag",
  body: "...",
  comments: [
    { author: "ava", text: "Great writeup", createdAt: ISODate("2025-06-01") },
    { author: "raj", text: "Clarified a lot, thanks", createdAt: ISODate("2025-06-02") }
  ]
});

The relational equivalent of that same read is a join the application has to issue and stitch together itself:

sql
-- What the same "post with its comments" read looks like relationally —
-- correct, and an extra round trip MongoDB's embedding avoids
SELECT p.title, p.body, c.author, c.text, c.created_at
FROM posts p
LEFT JOIN comments c ON c.post_id = p.id
WHERE p.id = 42
ORDER BY c.created_at;

Embed vs. reference: the actual decision criteria

The embed-versus-reference choice comes down to three questions: how large can the embedded array grow, is the embedded data ever queried or updated independently of its parent, and does more than one parent document need the same data. A product's reviews can run into the thousands, pushing toward MongoDB's 16MB document limit and slow updates as the array grows — a signal to reference instead. A shipping address embedded into every order is different: updating it on one order shouldn't change past orders, since a historical order should reflect the address it actually shipped to, not the customer's current one.

javascript
// Referenced: comments live in their own collection once the post
// has enough of them that embedding would bloat every post read
db.comments.insertOne({
  postId: ObjectId("665f1a2b3c4d5e6f7a8b9c0d"),
  author: "ava",
  text: "Great writeup",
  createdAt: ISODate("2025-06-01")
});
db.comments.createIndex({ postId: 1 });

The mistake that looks like a feature: unbounded arrays

An array field that grows without bound — "add every event a user triggers to their user document" — is the single most common MongoDB design mistake. It looks convenient early (one document, one read) and becomes a real problem later: documents nearing the 16MB limit, every update rewriting the whole document on disk, index performance degrading as arrays grow. The fix is the one relational design already knows: once a collection of related items is unbounded, it belongs in its own collection with a reference back to its parent, not an ever-growing array.

Indexing still matters exactly as much as in SQL

The absence of a schema doesn't mean the absence of query planning — MongoDB has its own explain(), its own equivalent of a B-tree index, and the same fundamental rule that a query without a matching index means a collection scan.

javascript
db.orders.createIndex({ customerId: 1, createdAt: -1 });
db.orders.find({ customerId: 42 }).sort({ createdAt: -1 }).explain("executionStats");

Compound index field order matters here exactly the way it does in a relational B-tree: this index serves {customerId: 42} alone and {customerId: 42, createdAt: ...} together, but not an isolated query on createdAt.

The actual principle

Treat the flexibility MongoDB gives you as a tool for matching document shape to read pattern, not as permission to skip modeling. Teams that get burned let the schema emerge accidentally from whatever got inserted first; teams that do well design the document shape as deliberately as a relational schema, just optimizing for a different variable — access pattern instead of normalization.