Skip to main content
โšก Calmops

MongoDB & NoSQL: A Practical Introduction

MongoDB is one of the most popular NoSQL databases and a great choice for applications that need a flexible, document-oriented data model, high write throughput, and easy horizontal scaling. This article explains NoSQL concepts, introduces MongoDB’s fundamentals, provides concrete examples (shell and Node.js), and helps you decide when MongoDB is the right tool for your project.

Table of contents

  • Introduction: What is NoSQL and how does it differ from SQL?
  • Key terms & abbreviations
  • MongoDB basics: core features & architecture
  • Document structure: BSON / JSON-like documents and examples
  • CRUD examples: insert, query, update, delete (shell & Node.js)
  • Aggregation & indexing examples
  • When to use MongoDB: use cases and decision guidance
  • Deployment architecture (text graph)
  • Common pitfalls & best practices
  • Pros/cons vs alternatives
  • Resources & further reading

Introduction: What is NoSQL and how does it differ from SQL?

NoSQL databases (“not only SQL”) are a family of data stores built for flexible schemas, horizontal scaling, and varied data models. Unlike relational databases, NoSQL databases often:

  • Use flexible schemas (schema-on-read vs schema-on-write)
  • Store data as documents (JSON-like), key-value pairs, wide-column, or graphs
  • Prioritize horizontal scaling and partitioning
  • Sacrifice some relational features or strong consistency in favor of performance and scale (depending on the store)

MongoDB is a document database: it stores data as documents (binary JSON, or BSON) inside collections. A document can contain nested objects and arrays, making it natural for modeling JSON-based application data.

High-level differences

  • Schema: SQL = rigid schema (tables, columns); MongoDB = flexible documents
  • Joins: SQL uses joins; MongoDB encourages embedding or referencing and supports limited lookups and aggregation pipeline joins
  • Scaling: SQL often scales vertically; MongoDB is designed to scale horizontally via sharding
  • Transactions: SQL historically had mature multi-row transactions; MongoDB added multi-document transactions (since 4.0) but patterns differ

Key terms & abbreviations

  • NoSQL โ€” Not Only SQL, family of non-relational databases
  • BSON โ€” Binary JSON, the internal storage format used by MongoDB (adds types like Date, ObjectId)
  • CRUD โ€” Create, Read, Update, Delete
  • Replica Set โ€” MongoDB’s primary/secondary architecture for high availability
  • Sharding โ€” Horizontal partitioning of data across multiple servers for scale
  • Collection โ€” Similar to a table; a group of MongoDB documents
  • Document โ€” A JSON-like object stored in MongoDB (flexible schema)
  • Aggregation Pipeline โ€” MongoDB’s framework for data aggregation, filtering, grouping, and transformation
  • Index โ€” Data structure to speed up queries (single-field, compound, text, TTL, etc.)

Additional core terms & abbreviations:

  • ACID โ€” Atomicity, Consistency, Isolation, Durability: transactional guarantees for reliability.
  • BASE โ€” Basically Available, Soft state, Eventual consistency: describes relaxed consistency models common in some distributed NoSQL systems.
  • CAP theorem โ€” Consistency, Availability, Partition tolerance: trade-offs in distributed systems (see: https://en.wikipedia.org/wiki/CAP_theorem). MongoDB provides configuration knobs that affect availability vs. consistency during partitions.
  • Oplog โ€” Operation log used by replica set secondaries to replicate operations from the primary (used for replication and point-in-time recovery).
  • mongosh โ€” The official MongoDB shell for interactive queries and commands.
  • TTL โ€” Time To Live (index type) used to automatically expire documents after a specified time.
  • 2dsphere / 2d โ€” Geospatial index types for location-based queries.

MongoDB basics: core features & architecture

Core features:

  • Document model (BSON) with nested structures and arrays
  • Flexible schema (documents in one collection can differ)
  • High availability through replica sets (automatic failover)
  • Horizontal scaling with sharding (distribute data by shard key)
  • Powerful aggregation pipeline for analytics
  • Variety of index types (single, compound, text, hashed, TTL)
  • Drivers for many languages (Node.js, Python, Java, Go, etc.) and an interactive shell (mongosh)

Architecture overview:

  • Client (application) uses a MongoDB driver to communicate with the cluster.
  • Replica set consists of one Primary node (writes) and multiple Secondaries (replica of data, can serve reads with appropriate read preferences).
  • Sharded cluster: multiple shard replica sets and a Config Server that stores metadata; mongos process routes queries to the correct shards.

Example: create a collection and insert a document (mongosh)

// mongosh
use shop;
db.products.insertOne({
  _id: ObjectId(),
  name: 'Wireless Mouse',
  price: 29.99,
  categories: ['electronics', 'accessories'],
  in_stock: true,
  specs: { weight: '85g', color: 'black' },
  created_at: new Date()
});

### Atomic single-document operations

MongoDB guarantees atomicity for single-document operations. This means updates that modify a single document (including nested fields and arrays) are atomic โ€” they either complete fully or have no effect. Prefer single-document designs where possible because they're simple and efficient.

Example: increment an order counter atomically

```js
db.counters.updateOne({ _id: 'order_seq' }, { $inc: { seq: 1 } }, { upsert: true });

The upsert: true option creates the document if it doesn’t exist, which is a common pattern for counters and sequence-like behavior.


---

## Document structure: BSON / JSON-like examples

Documents in MongoDB look like JSON but are stored in BSON. They support nested objects and arrays.

Example of a `user` document:

```json
{
  "_id": {"$oid": "656a3f0e5f1b2a3c4d5e6f78"},
  "username": "alice",
  "email": "[email protected]",
  "profile": {
    "first_name": "Alice",
    "last_name": "Johnson",
    "bio": "Developer and gardener"
  },
  "roles": ["user", "beta-tester"],
  "created_at": {"$date": "2025-10-01T12:00:00Z"}
}

Key ideas:

  • Documents are self-describing and can contain nested arrays or objects (good for representing hierarchical or related data).
  • No fixed schema: documents in the same collection can have different fields.
  • Use _id as the primary identifier (MongoDB uses ObjectId by default).

ObjectId and data types

  • MongoDB’s default _id type is ObjectId, a 12-byte value that encodes a timestamp, machine id, process id and counter โ€” convenient for generation and sorting by creation time.
  • BSON supports types not present in plain JSON (Date, Binary, Decimal128), which helps store more accurate types.

Embedding vs referencing (data modeling patterns)

One of the most important modeling decisions in MongoDB is whether to embed (nest related data inside a document) or reference (store related data in separate collections). The rule of thumb: embed for “contains” relationships and when the sub-document is frequently accessed with the parent; reference when the sub-document grows without bound or is shared across many parents.

Embedding example (user with addresses embedded):

{
  "_id": {"$oid": "..."},
  "username": "jane",
  "addresses": [
    { "line1": "1 Main St", "city": "Portland", "zip": "97205" },
    { "line1": "20 2nd Ave", "city": "Seattle", "zip": "98101" }
  ]
}

Referencing example (users referencing orders):

// users collection
{ "_id": 1, "username": "bob" }

// orders collection
{ "_id": 101, "user_id": 1, "total": 29.99 }

Trade-offs:

  • Embedding reduces the need for server-side joins and is fast for common reads.
  • Referencing maintains normalized data and avoids large documents; it requires separate queries or $lookup in the aggregation pipeline.


CRUD examples (mongosh and Node.js)

Insert documents (mongosh)

use shop;

// Insert single
db.products.insertOne({ name: 'Keyboard', price: 49.99, stock: 150 });

// Insert multiple
db.products.insertMany([
  { name: 'Mouse', price: 29.99, stock: 300 },
  { name: 'Monitor', price: 199.99, stock: 50 }
]);

Query documents (mongosh)

// Find all products
db.products.find({});

// Find with filter
db.products.find({ price: { $gte: 50 } }).sort({ price: -1 }).limit(10);

// Find one
db.products.findOne({ name: 'Mouse' });

Update documents (mongosh)

// Update one
db.products.updateOne({ name: 'Keyboard' }, { $set: { price: 44.99 } });

// Increment stock
db.products.updateOne({ name: 'Keyboard' }, { $inc: { stock: 10 } });

// Update many
db.products.updateMany({ price: { $lt: 20 } }, { $set: { on_sale: true } });

// Upsert (update or insert)
db.products.updateOne({ sku: 'SKU-123' }, { $set: { name: 'New Item', price: 9.99 } }, { upsert: true });

// Update an array element using the positional operator ($)
db.users.updateOne({ username: 'jane', 'addresses.city': 'Portland' }, { $set: { 'addresses.$.zip': '97205' } });

// Push into an array and ensure uniqueness with $addToSet
db.posts.updateOne({ _id: ObjectId('...') }, { $push: { comments: { $each: [{ user: 'bob', text: 'Nice!' }] } } });
db.users.updateOne({ _id: 1 }, { $addToSet: { roles: 'admin' } });

// findOneAndUpdate: return the updated document
db.products.findOneAndUpdate({ name: 'Keyboard' }, { $set: { price: 39.99 } }, { returnDocument: 'after' });

// Bulk operations (efficient for many writes)
db.products.bulkWrite([
  { insertOne: { document: { name: 'Adapter', price: 12.99 } } },
  { updateOne: { filter: { name: 'Mouse' }, update: { $inc: { stock: -1 } } } },
  { deleteOne: { filter: { discontinued: true } } }
]);

Delete documents (mongosh)

// Delete one
db.products.deleteOne({ name: 'Broken Item' });

// Delete many
db.products.deleteMany({ discontinued: true });

Node.js examples (using official driver)

// Install: npm install mongodb
const { MongoClient, ObjectId } = require('mongodb');
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017';
const client = new MongoClient(uri);

async function run() {
  try {
    await client.connect();
    const db = client.db('shop');
    const products = db.collection('products');

    // Create
    const res = await products.insertOne({ name: 'Webcam', price: 79.99, stock: 40 });

    // Read
    const item = await products.findOne({ _id: res.insertedId });

    // Update
    await products.updateOne({ _id: res.insertedId }, { $set: { price: 69.99 } });

    // Delete
    await products.deleteOne({ _id: res.insertedId });
  } finally {
    await client.close();
  }
}
run().catch(console.error);

// Projection example: return only specific fields const cursor = products.find({ price: { $gte: 50 } }, { projection: { name: 1, price: 1 } }); for await (const doc of cursor) { console.log(doc); }

// Change stream example (watching new orders) async function watchOrders() { const orders = client.db(‘shop’).collection(‘orders’); const stream = orders.watch(); for await (const change of stream) { console.log(‘Order change:’, change); } } // call watchOrders() in a background worker or service


Aggregation & indexing examples

Aggregation pipeline (grouping and summing)

// Total sales per product_id (order_items collection)
db.order_items.aggregate([
  { $group: { _id: '$product_id', units_sold: { $sum: '$quantity' }, revenue: { $sum: { $multiply: ['$quantity', '$unit_price'] } } } },
  { $sort: { revenue: -1 } },
  { $limit: 10 }
]);

// $lookup example: join orders with customers (like SQL JOIN) db.orders.aggregate([ { $lookup: { from: ‘customers’, localField: ‘customer_id’, foreignField: ‘customer_id’, as: ‘customer’ } }, { $unwind: '$customer’ }, { $project: { order_id: 1, total_amount: 1, ‘customer.first_name’: 1, ‘customer.email’: 1 } } ]);

Indexing examples

// Create a single-field index
db.products.createIndex({ product_name: 1 });

// Compound index
db.orders.createIndex({ customer_id: 1, order_date: -1 });

// Text index for full-text search
db.articles.createIndex({ title: 'text', content: 'text' });

// TTL index for expiring documents
// Automatically removes documents after `expireAt` date
db.sessions.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 });

Indexing tips:

  • Index fields used frequently in queries, sorting, and joins (lookup).
  • Avoid over-indexing: each index adds write overhead and storage cost.
  • Use explain plans (db.collection.explain()) to diagnose slow queries.

Using explain:

// Shows how the query executed and whether it used an index
db.products.find({ price: { $gte: 50 } }).sort({ price: -1 }).limit(10).explain('executionStats');

Look for totalKeysExamined vs totalDocsExamined to see if the index reduced document scanning.

Geospatial example

// Create 2dsphere index for location
db.places.createIndex({ location: '2dsphere' });

// Find places within 5 kilometers of a point
db.places.find({
  location: {
    $near: {
      $geometry: { type: 'Point', coordinates: [ -122.4194, 37.7749 ] },
      $maxDistance: 5000
    }
  }
});

Time-series example

// Create a time-series collection (Postgres-style time-series optimization)
db.createCollection('sensor_readings', { timeseries: { timeField: 'timestamp', metaField: 'deviceId' } });

// Insert a reading
db.sensor_readings.insertOne({ deviceId: 'dev-1', timestamp: new Date(), temperature: 22.5 });

Facet example (multiple aggregations in one pass)

db.orders.aggregate([
  { $match: { order_date: { $gte: ISODate('2025-01-01') } } },
  { $facet: {
      revenueByMonth: [ { $group: { _id: { $month: '$order_date' }, revenue: { $sum: '$total_amount' } } } ],
      topProducts: [ { $unwind: '$items' }, { $group: { _id: '$items.product_id', sold: { $sum: '$items.quantity' } } }, { $sort: { sold: -1 } }, { $limit: 10 } ]
    }
  }
]);

When to use MongoDB: use cases & decision guidance

MongoDB fits well when your application requires any of the following:

  • Flexible / evolving schema: APIs, MVPs, or apps with frequent schema changes
  • Hierarchical or nested data: product specs, user profiles, JSON documents
  • High write volumes and horizontal scale: logging, IoT ingestion, event streams
  • Rapid prototyping and developer productivity: JSON model maps naturally to application objects
  • Geospatial queries and time-series: built-in support for geospatial indexing and time-series collections
  • Content stores and catalogs: product catalogs, CMS content, user-generated content

Real-world examples:

  • A product catalog with varying attributes per category (electronics vs clothing)
  • An activity/event ingestion pipeline collecting telemetry from devices
  • A CMS storing articles with varied metadata and embedded author profiles
  • A social feed where posts contain arrays of comments, likes, and metadata

When MongoDB might not be the best choice:

  • Complex multi-row transactions with strict consistency requirements across many relational tables (traditional SQL shines here, though MongoDB has ACID support for multi-document transactions since v4.0)
  • Heavy ad-hoc joins across many normalized tables (RDBMS often performs better)
  • Applications requiring advanced SQL features like window functions (although MongoDB has aggregation capabilities, SQL has matured features)

Decision checklist:

  • Does your data fit naturally into documents? If yes, MongoDB is a good fit.
  • Do you need flexible schemas and fast iterations? MongoDB excels here.
  • Do you need strong relational integrity with many normalized tables and heavy joins? Consider a relational DB.

Quick decision matrix:

  • If you need: flexible JSON documents, rapid iteration, and moderate joins โ†’ MongoDB โœ…
  • If you need: strict relational integrity, complex joins, and mature reporting SQL features โ†’ PostgreSQL โœ…
  • If you need: write-heavy, append-only time-series at massive scale โ†’ Cassandra or TimescaleDB โœ…
  • If you need: fast key-value caching / ephemeral data โ†’ Redis โœ…

Deployment architecture (text graph)

Simple single-region deployment:

client -> app server -> mongodb replica set (primary -> secondaries)

Sharded, highly available architecture:

client -> mongos (query router) -> shards (each shard is a replica set) -> config servers

Notes:

  • Use replica sets for high availability and read scaling (with read preferences).
  • Use sharding for write scalability and very large datasets; choose an appropriate shard key carefully.

Transactions & consistency

MongoDB supports multi-document ACID transactions when using replica sets and sharded clusters (since 4.0+). Transactions allow you to group multiple writes into an atomic unit, but they have performance costs and complexity. Use them when you need cross-document atomicity (e.g., financial transfers), and prefer single-document atomic operations when possible.

Node.js transaction example (simplified):

const session = client.startSession();
try {
  await session.withTransaction(async () => {
    const users = db.collection('users');
    const accounts = db.collection('accounts');
    await users.updateOne({ _id: uid }, { $inc: { balance: -100 } }, { session });
    await accounts.updateOne({ _id: aid }, { $inc: { balance: 100 } }, { session });
  });
} finally {
  await session.endSession();
}

Choosing a shard key (brief guidance)

  • Pick a shard key that ensures even data distribution and supports your query patterns. A good shard key has high cardinality and avoids monotonic values (like creation timestamps) that cause hotspotting.
  • Analyze your query patterns and shard on fields used frequently in queries for targeted partitions.
  • Consider compound shard keys when a single field is not sufficient.

Common pitfalls & best practices

Pitfalls

  • Over-embedding: Embedding too much data in a single document can lead to very large documents (16MB BSON document size limit).
  • Poor shard key selection: Leads to uneven data distribution (hot shards).
  • Missing indexes: Slow queries and collection scans.
  • Treating MongoDB exactly like a relational DB and normalizing everythingโ€”leads to inefficient patterns.

Additional pitfalls to watch for:

  • Unbounded arrays: Documents with arrays that always grow (e.g., appending comments without bounds) can cause document growth and performance problems; consider referencing or capped collections for such data.
  • Ignoring working set size: If your total active data set doesn’t fit in RAM, you’ll see disk I/O and performance degradation โ€” monitor memory usage and indices.
  • Lack of schema validation: Flexible schemas are powerful, but without validation or versioning you can end up with inconsistent documents that complicate queries.
  • Improper TTL usage: TTL indexes are useful for session and ephemeral data, but if misconfigured they may delete data unexpectedly.
  • Operations without explain(): Deploying code with poorly performing queries without profiling can lead to production incidents.

Best practices

  • Model data around query patterns โ€” design documents for common reads and writes.
  • Use indexes wisely; benchmark and use explain().
  • Limit document size and avoid large arrays that grow without bounds.
  • Use replica sets for HA and set up proper backups (mongodump, filesystem snapshots, cloud provider backups).
  • For cross-document transactions, use multi-document transactions and understand their performance trade-offs.
  • Monitor with tools (MongoDB Cloud / Atlas, MMS, Datadog) and set alerts.

Additional best practices:

  • Schema validation & versioning: Use MongoDB’s JSON Schema validation ($jsonSchema) to enforce structural constraints where appropriate and keep a migration plan for schema changes.

Example: JSON Schema validation

db.createCollection('users', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['username', 'email'],
      properties: {
        username: { bsonType: 'string' },
        email: { bsonType: 'string', pattern: '^.+@.+$' },
        age: { bsonType: 'int', minimum: 0 }
      }
    }
  }
});

This imposes server-side validation while still preserving some of MongoDB’s flexibility.

  • Backups & PITR: Implement regular backups (mongodump, filesystem snapshots, cloud provider backups) and, where required, point-in-time recovery strategies. Test restores regularly.
  • Monitoring & alerting: Monitor replication lag, CPU, memory, disk I/O, index usage, and slow queries. Set SLOs and alerts for key metrics.
  • Operational procedures: Use rolling upgrades, maintenance windows, and have runbooks for primary elections, recovering from a failed primary, and rebalancing shards.
  • Use change streams for CDC: MongoDB’s change streams allow you to stream DB changes to other services reliably for caching or analytics pipelines.
  • Test with production-like data: Performance characteristics often surface only when data volume and distribution match production.

Pros & cons vs alternatives

Pros

  • Flexible schema and fast development
  • Good horizontal scalability (sharding)
  • Rich query language with aggregation pipeline
  • Strong ecosystem: Atlas (managed), drivers, tooling

More pros:

  • Excellent developer DX for JSON-first applications (maps well to JavaScript/JSON clients)
  • Strong managed offering (MongoDB Atlas) simplifies operations and backup
  • Good tooling for analytics via aggregation pipeline and connectors

Cons

  • Not always the best for highly normalized, relation-heavy workloads
  • Sharding complexity and careful operational considerations
  • Potential for inconsistent modeling if team lacks schema discipline

More cons:

  • Operational overhead for large sharded clusters (careful capacity planning required)
  • Multi-document transactions have performance costs compared with single-document operations
  • Incorrect modeling can lead to hot-spotting, memory pressure, or expensive aggregation queries

Alternatives to consider:

  • PostgreSQL: Strong relational features, rich SQL, indexing, JSONB for semi-structured data
  • Cassandra: Distributed wide-column store ideal for write-heavy, time-series workloads
  • Elasticsearch: Full-text search & analytics (not a primary database by itself)

Other alternatives and when to pick them:

  • DynamoDB: Fully managed key-value/NoSQL from AWS for hyper-scale workloads with single-digit millisecond performance guarantees and pay-per-request pricing model.
  • Redis: In-memory key-value store for ultra-fast caching, leaderboards, and ephemeral data; not a durable primary DB for large datasets unless using Redis on Flash/Redis Enterprise patterns.
  • CockroachDB / YugabyteDB: Distributed SQL databases that provide horizontal scaling with strong consistency (if you need SQL semantics with horizontal scale).

Resources & further reading

Additional resources & books:

Community & tutorials:


Conclusion

MongoDB is a versatile document database that excels at flexible schemas, nested data modeling, and horizontal scalability. If your application benefits from rapid iteration, JSON-native data, and the ability to scale writes across machines, MongoDB is a strong choice. However, balance the decision with considerations about transactions, join complexity, and operational trade-offs. Start small, model around queries, and iterate with real-world data and monitoring.

Happy building with documents! ๐Ÿงฉ

Comments