Document Databases: MongoDB, JSON Documents and When to Use Them

Document databases explained: the ORM problem, self-contained JSON documents, embedding vs referencing, where they fit, where they don't, and MongoDB basics.

Beginner 3 min read · Updated

A document database stores data as JSON-like documents: each record is a self-contained object with nested fields and arrays, and different documents in the same collection can have different fields. Key-value stores give simple lookups, but many applications need nested data (user profiles with addresses, blog posts with comments, products with specifications) and want to query inside it. Document databases are built for exactly that. MongoDB is the best-known example.

The ORM problem

Douglas Crockford published the JSON specification in 2001, and JSON became the language of the web: browsers send JSON, APIs answer in JSON, services talk JSON to each other. Relational databases, though, store tables and rows.

Storing JSON-shaped data in SQL creates friction. A user profile with nested addresses and several phone numbers needs three tables (users, addresses, phone_numbers): saving one profile takes three INSERTs, and reading it back joins three tables. Object-relational mappers (ORMs) translate between objects and tables, and they work until they don't: nested arrays need complex mapping, schema migrations break existing code, and performance suffers from the N+1 query problem.

JSONSQLBrowserWeb appORMJSON ⇄ tables and rowsRelational database
With a relational database, an ORM translates every request: JSON objects are mapped to rows on the way in and rows back to JSON on the way out.
# The N+1 problem with an ORM: 1 query for the users, then 1 more per user
users = session.query(User).limit(100).all()          # 1 query
for user in users:
    print(user.name, [a.city for a in user.addresses])  # +100 lazy-loaded queries

# The same data as documents: one query returns users with their addresses embedded
users = db.users.find({}, {"name": 1, "addresses.city": 1}).limit(100)

How document databases work

Document databases store JSON (or a binary form of it, such as MongoDB's BSON) directly: no translation layer and no ORM. The application sends JSON, the database stores JSON, and reads return JSON.

JSONJSONBrowserWeb appDocument database
With a document database, JSON flows end to end: browser, web app and database all speak the same shape.
  • Self-contained documents: a user profile includes its related data in one document, with addresses and phone numbers nested inside, so one read returns everything.
  • Flexible fields: one user has a verified badge and another doesn't, and the database accepts both. New documents can carry new fields while old documents stay unchanged, with no schema migration.
  • Queryable insides: unlike a key-value store, the database understands the structure, so you can filter, sort and index on nested fields.
{
  "_id": "u_456",
  "name": "Bob Johnson",
  "email": "bob.johnson@example.com",
  "verified": true,
  "addresses": [
    { "type": "home", "street": "123 Main St", "city": "Springfield" },
    { "type": "work", "street": "9 Market Sq", "city": "Shelbyville" }
  ],
  "phones": ["+1-555-0100", "+1-555-0199"],
  "rewards": []
}

Embedding vs referencing

The key modeling decision is whether related data lives inside a document or in its own document, linked by ID:

EmbedReference
HowNest the data inside the parent (addresses inside the user)Store an ID and fetch separately (an order stores user_id)
Choose whenRead together, owned by one parent, bounded in sizeShared by many parents, large or growing without limit, updated independently
ExampleA post's tags; a user's few addressesA post's comments once they number in the thousands; products referenced by orders
CostDuplication if the same data is embedded in many placesExtra queries (or $lookup) to join at read time

When to use a document database

  • User and entity profiles: self-contained content with no strong relationships. A Reddit profile page shows one user's data, needs no complex queries, and new features add fields (rewards: []) without migrations. Merchant profiles on Yelp, venue profiles on Ticketmaster and e-commerce listings follow the same pattern.
  • Content management: a blog post has a title, body and tags array; posts are mostly independent, reading one shouldn't need joins, and flexible schemas allow custom fields per post.
  • Product catalogs: an electronics product has screen size and battery life, clothing has size and material. Both live in the same collection with different fields.
{ "sku": "tv-55", "category": "electronics", "name": "55\" OLED TV",
  "specs": { "screen_in": 55, "resolution": "4K", "power_w": 120 } }

{ "sku": "tee-m-blue", "category": "clothing", "name": "Cotton tee",
  "specs": { "size": "M", "material": "100% cotton", "color": "blue" } }

When not to use one

Avoid document databases when the data needs strong consistency across many records, complex relationships and strict normalization. A banking application moving money between accounts needs multi-record ACID transactions; use a relational database. (MongoDB does support multi-document transactions, but if most operations need them, the relational model is usually the better fit.) Many-to-many relationships queried from every direction also fit tables better than documents. See normalization vs denormalization.

Try it: MongoDB basics

These commands run in mongosh against a local MongoDB (for example docker run -p 27017:27017 mongo:7), or in any online MongoDB playground:

// create a collection
db.createCollection("users");

// insert one document, then two more
db.users.insertOne({ name: "John Doe", age: 30, email: "john.doe@example.com" });
db.users.insertMany([
  { name: "Jane Smith", age: 28, email: "jane.smith@example.com" },
  { name: "Bob Johnson", age: 45, email: "bob.johnson@example.com" },
]);

// find all documents, then one specific document
db.users.find();
db.users.findOne({ name: "John Doe" });

// query on a field and index it
db.users.find({ age: { $gt: 29 } }).sort({ age: -1 });
db.users.createIndex({ email: 1 }, { unique: true });

// update a field, then add a brand-new field to one document (no migration)
db.users.updateOne({ name: "John Doe" }, { $set: { age: 31 } });
db.users.updateOne({ name: "John Doe" }, { $set: { address: "123 Main St, Springfield" } });

// delete a document
db.users.deleteOne({ name: "John Doe" });

Where it’s used

Profiles and listings

User, merchant, venue and product pages that load one self-contained document per view.

Content management

Articles and posts with flexible custom fields and embedded tags.

Product catalogs

Different attributes per category in one collection, indexed on the fields shoppers filter by.

Mobile and web backends

Firestore and similar document stores sync JSON-shaped app state directly to clients.

In an interview

  • Choose a document store when each screen reads one self-contained entity and the fields keep changing.
  • Say what you embed and what you reference, and why (read together vs shared or unbounded).
  • Name the indexes for the fields you query on.
  • Don't use it as the system of record for money or other cross-record invariants; that's a relational job.

Further reading