What Is a Document Database?

MongoDBCouchDBFerretDBDatabases

"Do I actually need MongoDB, or is PostgreSQL fine?" If you've asked this question while setting up a new project, you're not alone. Every developer hits this decision point eventually. The answer depends on how your data is shaped and how you need to query it, not on which database has more hype.

This post breaks down what a document database is, when it genuinely makes your life easier, when a relational database is the better pick, and where PostgreSQL's JSONB blurs the line between the two.

Contents

What Is a Document Database

A relational database stores data in tables with fixed columns. Every row in a users table has the same fields. If you need a new field, you run a migration. Every row gets that column whether it needs it or not.

A document database stores JSON-like documents instead of rows. Each document is a self-contained unit with its own structure. Documents in the same collection can have completely different fields. No migrations needed to add a field. Nested objects and arrays are first-class citizens.

Here's a product catalog entry as a document:

json
{
  "name": "ThinkPad X1 Carbon",
  "category": "laptop",
  "price": 1429.99,
  "specs": {
    "cpu": "Intel i7-1365U",
    "ram": "16GB",
    "storage": "512GB SSD",
    "screenSize": "14 inch"
  },
  "tags": ["ultrabook", "business", "lightweight"]
}

And a t-shirt in the same collection:

json
{
  "name": "Classic Crew Tee",
  "category": "clothing",
  "price": 24.99,
  "specs": {
    "material": "100% cotton",
    "fit": "regular",
    "sizes": ["S", "M", "L", "XL"]
  },
  "colors": ["black", "white", "navy"],
  "tags": ["casual", "basics"]
}

Same collection, completely different specs fields. The laptop has ram and screenSize. The t-shirt has material and sizes. In a relational database, you would need a generic attributes table with key-value pairs, a separate table per category, or a bunch of nullable columns. In a document database, each document carries exactly the fields it needs.

When Document Databases Make Sense

Document databases aren't universally better. They solve specific problems well.

Data with highly variable structure. The product catalog example above is the classic case. When every category of item has different attributes, forcing them all into the same table schema creates more problems than it solves. Content management systems have the same pattern: a blog post has body and tags, a video has duration and resolution, a podcast has episodes and rss_url.

Rapid prototyping. When you're iterating on a product and the data model changes every week, not needing to write migrations is a real advantage. Store whatever your application produces, adjust the structure as requirements solidify, and formalize the schema later (or don't). There's real velocity in skipping the ALTER TABLE step during early development.

Event and activity logs. Log entries vary in structure. An API request log has endpoint, statusCode, and responseTime. A user action log has action, target, and metadata. A payment event has amount, currency, and paymentMethod. All are "events," but no two types share the same fields. Document databases handle this naturally with high write throughput.

Application objects that are already documents. If your code works with nested objects and arrays, storing them directly eliminates the translation layer between your application and your database. No ORM mapping nested objects into junction tables. No reassembling flattened rows into objects on read. The document is the object.

When PostgreSQL Is Better

Here's the honest part: for many projects, you don't need a document database.

Your data is relational. Users have orders. Orders have line items. Line items reference products. Products belong to categories. This is a graph of relationships, and relational databases are literally named after this pattern. Foreign keys, joins, and referential integrity constraints exist because this kind of data is common and important. Trying to model deeply relational data in a document database leads to either massive data duplication or complex application-side joins.

You need JOINs across entities. "Show me all orders placed by users who signed up this month, grouped by product category, with the average order value." That's one SQL query with a few joins. In a document database, it's multiple queries stitched together in application code.

You need transactions across multiple records. Transferring money between accounts, decrementing inventory while creating an order, updating a parent and child record together. Relational databases give you multi-row, multi-table ACID transactions by default. MongoDB supports multi-document transactions, but they're opt-in, have performance overhead, and the document model encourages designs that avoid them.

Your schema is well-defined and stable. If you know the shape of your data and it doesn't change much, a fixed schema with migrations is an advantage, not a burden. The schema documents your data model, validates inserts, and catches bugs before they reach production.

The PostgreSQL JSONB Middle Ground

Before reaching for a separate document database, consider that PostgreSQL can do both.

PostgreSQL's jsonb type stores binary JSON with full indexing support. You can query into nested fields, use GIN indexes for fast lookups, and mix structured columns with flexible JSON in the same table:

sql
CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  category VARCHAR(100) NOT NULL,
  price NUMERIC(10,2) NOT NULL,
  specs JSONB,
  tags JSONB DEFAULT '[]'
);

INSERT INTO products (name, category, price, specs, tags) VALUES
  ('ThinkPad X1', 'laptop', 1429.99,
   '{"cpu": "i7", "ram": "16GB", "screenSize": "14 inch"}',
   '["ultrabook", "business"]'),
  ('Classic Tee', 'clothing', 24.99,
   '{"material": "cotton", "fit": "regular", "sizes": ["S","M","L"]}',
   '["casual", "basics"]');

Query into the JSON:

sql
-- Find laptops with 16GB RAM
SELECT name, price, specs->>'ram' AS ram
FROM products
WHERE category = 'laptop'
  AND specs->>'ram' = '16GB';

-- Find products tagged "business"
SELECT name, price
FROM products
WHERE tags @> '"business"';

For many projects, this is enough. You get document flexibility for the fields that need it, relational structure for the fields that don't, and everything lives in one database with one backup strategy, one connection pool, and one set of operational knowledge.

The trade-off: querying deeply nested JSONB is more verbose than MongoDB's query language, and you still need migrations when your fixed columns change. But for a team already running PostgreSQL, adding a jsonb column is simpler than adding and operating a second database.

Your Options

If you do need a dedicated document database, here are the three main choices.

MongoDB

The standard. Largest ecosystem, most mature tooling, the strongest query language and aggregation pipeline of any document database. The mongodb npm package works with every major language and framework. MongoDB Atlas provides a fully managed cloud option with a free tier.

The licensing question: MongoDB uses SSPL, which prevents you from offering it as a managed service without open-sourcing your entire stack. For most application developers, this isn't a practical issue. For platform builders, it might be.

Read the full walkthrough: Getting Started with MongoDB

FerretDB

MongoDB's query API on PostgreSQL. Same mongodb:// connection string, same driver, same query syntax. Your documents get stored as JSONB in PostgreSQL tables. Apache 2.0 licensed, no SSPL concerns.

The catch: it doesn't support everything MongoDB supports yet. No $lookup, no change streams, no text indexes. For core CRUD and basic aggregation, it produces identical results to MongoDB. If you're already a PostgreSQL shop and want the document API without a second database system, FerretDB is compelling.

Read the full walkthrough: Getting Started with FerretDB

Deeper comparison: MongoDB vs FerretDB

CouchDB

Built for replication. Where MongoDB focuses on flexible queries, CouchDB tackles keeping data consistent across distributed systems. Every operation is an HTTP request. Built-in multi-master replication with automatic conflict resolution.

CouchDB is the right pick for offline-first applications, multi-site deployments, and edge computing where devices sync data when they come online. If your primary concern is query flexibility and aggregation, MongoDB is a better fit. If your primary concern is reliable data synchronization across unreliable networks, CouchDB was designed for that.

Read the full walkthrough: Getting Started with CouchDB

Try It Locally

SpinDB gets MongoDB running locally in about 30 seconds. No Docker, no manual setup. (What is SpinDB?)

Install SpinDB:

bash
npm i -g spindb    # npm
pnpm add -g spindb # pnpm

Create and start a MongoDB instance:

bash
spindb create mongo1 -e mongodb --start

Get the connection string:

bash
spindb url mongo1
text
mongodb://127.0.0.1:27017

Connect with the official driver:

bash
mkdir doc-db-test && cd doc-db-test
pnpm init
pnpm add mongodb
pnpm add -D tsx typescript

Create test.ts:

typescript
import { MongoClient } from 'mongodb'

const client = new MongoClient('mongodb://localhost:27017')
await client.connect()

const db = client.db('testdb')
const products = db.collection('products')

await products.drop().catch(() => {})

await products.insertMany([
  {
    name: 'ThinkPad X1',
    category: 'laptop',
    price: 1429.99,
    specs: { cpu: 'i7', ram: '16GB', screenSize: '14 inch' },
    tags: ['ultrabook', 'business'],
  },
  {
    name: 'Classic Tee',
    category: 'clothing',
    price: 24.99,
    specs: { material: 'cotton', fit: 'regular' },
    tags: ['casual', 'basics'],
  },
  {
    name: 'Mechanical Keyboard',
    category: 'accessory',
    price: 149.99,
    specs: { switches: 'Cherry MX Brown', layout: 'TKL', backlit: true },
    tags: ['peripherals', 'mechanical'],
  },
])

// Query: find all products under $100
const affordable = await products
  .find({ price: { $lt: 100 } })
  .toArray()

console.log('Products under $100:')
for (const p of affordable) {
  console.log(`  ${p.name} - $${p.price}`)
}

// Each document has different specs fields, queried the same way
const allSpecs = await products
  .find({}, { projection: { name: 1, specs: 1, _id: 0 } })
  .toArray()

console.log('\nProduct specs (all different shapes):')
for (const p of allSpecs) {
  console.log(`  ${p.name}: ${JSON.stringify(p.specs)}`)
}

await client.close()

Run it:

bash
npx tsx test.ts
text
Products under $100:
  Classic Tee - $24.99

Product specs (all different shapes):
  ThinkPad X1: {"cpu":"i7","ram":"16GB","screenSize":"14 inch"}
  Classic Tee: {"material":"cotton","fit":"regular"}
  Mechanical Keyboard: {"switches":"Cherry MX Brown","layout":"TKL","backlit":true}

Three products, three completely different specs shapes, all in the same collection, all queryable.

Wrapping Up

Document databases store flexible, self-describing JSON documents instead of fixed-schema rows. They work well for variable-structure data, rapid prototyping, event logs, and application objects that are already nested.

But they're not always the right tool. If your data is relational, if you need JOINs and multi-record transactions, or if your schema is stable, PostgreSQL is probably the better choice. And PostgreSQL's JSONB gives you document-like flexibility without a second database when you need it.

If you do want a document database: MongoDB is the default for most use cases, FerretDB gives you the MongoDB API on PostgreSQL, and CouchDB handles distributed replication better than anything else.

Manage your local instances:

bash
spindb stop mongo1    # Stop the server
spindb start mongo1   # Start it again
spindb list           # See all your database instances

SpinDB supports 20+ engines, so you can run MongoDB alongside PostgreSQL, Redis, or any other database from the same CLI. For a managed option, Layerbase Cloud provisions MongoDB, FerretDB, or CouchDB instances in seconds.

Something not working?