MongoDB powers a wide range of applications, from request-response APIs to event-driven systems. This guide explains MongoDB architecture with a focus on how data moves through the system, the control levers you have for durability and latency, common failure points, and hands-on code you can run today. You will get a clear workflow for production readiness and a local pilot plan that is quick to set up and simple to evaluate.
MongoDB at a glance: core components
Key building blocks:
- mongod: the database server process. Each instance stores data and participates in replication or sharding.
- Storage engine (WiredTiger): manages on-disk data, compression, journaling, and caches.
- Replica set: a group of mongod processes providing redundancy and automatic failover. One primary receives writes; secondaries replicate via the oplog.
- Oplog: an ordered, capped collection of operations that secondaries apply to reach consistency.
- Sharding: horizontal partitioning. Shards are replica sets; mongos is the stateless query router; config servers store cluster metadata.
- Indexes: btree or special types (hashed, text, TTL); critical for predictable read latency and efficient writes.
- Write and read concerns: client-side controls for durability and consistency levels.
Data and control flow
Write path (replica set):
- Client -> driver -> primary.
- Primary writes to WiredTiger, journaled on disk.
- Operation recorded in oplog.
- Secondaries pull and apply oplog entries.
- Acknowledgement back to client depends on writeConcern.
ASCII sketch:
Client -> Driver -> Primary -> Journal -> Oplog -> Secondaries
^ |
|_________________________|
Read path:
- Reads can go to primary or secondaries depending on readPreference.
- Query planner chooses an index; without a useful index, collection scans increase latency.
Control levers:
- writeConcern: 1, majority, or custom numbers. Higher values increase durability and latency.
- readConcern: local, majority, linearizable. Higher guarantees may reduce availability or increase latency.
- readPreference: primary, primaryPreferred, secondary, etc.
- Transactions: multi-document ACID on a replica set or sharded cluster; increase coordination cost, so use only where needed.
- Retryable writes and timeouts: retryWrites=true and maxTimeMS to handle transient failures safely.
Failure points and tradeoffs
Common failure modes and mitigations:
- Primary crash or network partition: leader election promotes a secondary. Mitigate with odd number of voting members and majority writeConcern.
- Disk or filesystem stalls: journaling and fsync can pause writes. Use fast storage and monitor iowait.
- Oplog too small: secondaries fall behind and need full resync. Size the oplog to cover replication lag during peaks and maintenance.
- Hot shard or uneven distribution: poor shard key choice causes hotspots. Choose high-cardinality, well-distributed, and preferably immutable keys.
- Index misses: full scans under load. Add and right-size indexes; avoid over-indexing that slows writes and consumes RAM.
- Large documents: 16 MB document limit. Normalize selectively or split large payloads.
- Cache pressure: working set larger than RAM increases page faults. Add RAM, tune indexes, or archive cold data.
Tradeoffs to weigh:
- Durability vs latency: majority writes cost more latency but protect against rollback.
- Consistency vs availability: strict readConcern and readPreference can reduce availability during failover.
- Simplicity vs scale: a single replica set is simpler; sharding adds complexity but scales writes and storage.
Workflow Overview
Step-by-step path to production:
- Requirements: data model, access patterns, durability targets, RPO/RTO.
- Topology: start with a 3-member replica set. Add sharding only when write throughput or dataset size requires it.
- Schema and indexes: design around queries; write tests that assert index coverage.
- Read/write controls: choose writeConcern and readConcern based on failure tolerance.
- Resilience: enable retryable writes, set socket and server selection timeouts, and connection pooling.
- Observability: define SLOs; collect metrics and logs; set alerts for replication lag, cache pressure, and slow queries.
- Backup and restore: schedule dumps or snapshots; rehearse restores regularly.
- Security: enable authentication and TLS; use least-privilege roles; rotate credentials and keys.
- Capacity plan: size RAM for working set; provision storage IOPS for peak write rates.
- Game days: rehearse failover, node replacement, and restore on non-production.
Practical examples: Node.js and Express API
Below is a minimal but production-minded API using the official MongoDB Node.js driver.
Connection string example (replica set, retryable writes, majority ack):
mongodb://localhost:27017, localhost:27018, localhost:27019/appdb?replicaSet=rs0&retryWrites=true&w=majority
Express API with indexes and a transaction:
// package.json deps: express, mongodb
const express = require('express');
const { MongoClient } = require('mongodb');
const uri = 'mongodb://localhost:27017, localhost:27018, localhost:27019/appdb?replicaSet=rs0&retryWrites=true&w=majority';
const client = new MongoClient(uri, {
maxPoolSize: 50,
serverSelectionTimeoutMS: 5000
});
async function main() {
await client.connect();
const db = client.db('appdb');
const products = db.collection('products');
const inventory = db.collection('inventory');
// Indexes: unique SKU, query-friendly compound index
await products.createIndex({ sku: 1 }, { unique: true });
await products.createIndex({ category: 1, price: 1 });
const app = express();
app.use(express.json());
app.post('/products', async (req, res) => {
try {
const doc = req.body; // { sku, name, category, price }
const r = await products.insertOne(doc, { writeConcern: { w: 'majority' } });
res.status(201).json({ id: r.insertedId });
} catch (e) {
if (e.code === 11000) return res.status(409).json({ error: 'duplicate sku' });
res.status(500).json({ error: 'server error' });
}
});
app.get('/products', async (req, res) => {
const { category, maxPrice } = req.query;
const q = {};
if (category) q.category = category;
if (maxPrice) q.price = { $lte: Number(maxPrice) };
const docs = await products.find(q).hint({ category: 1, price: 1 }).limit(100).toArray();
res.json(docs);
});
// Atomic inventory transfer using a transaction
app.post('/inventory/transfer', async (req, res) => {
const { sku, fromLoc, toLoc, qty } = req.body;
const session = client.startSession();
try {
await session.withTransaction(async () => {
const dec = await inventory.updateOne(
{ sku, location: fromLoc, qty: { $gte: qty } },
{ $inc: { qty: -qty } },
{ session }
);
if (dec.matchedCount !== 1) throw new Error('insufficient stock');
await inventory.updateOne(
{ sku, location: toLoc },
{ $inc: { qty } },
{ upsert: true, session }
);
}, {
readConcern: { level: 'local' },
writeConcern: { w: 'majority' }
});
res.json({ ok: 1 });
} catch (e) {
res.status(400).json({ error: e.message });
} finally {
await session.endSession();
}
});
// Change stream to log price updates (primary-only read)
products.watch([{ $match: { 'updateDescription.updatedFields.price': { $exists: true } } }])
.on('change', ev => console.log('price change', ev.documentKey, ev.updateDescription));
app.listen(3000, () => console.log('API on :3000'));
}
main().catch(err => {
console.error(err);
process.exit(1);
});
Replica set for local dev (3 nodes):
# Three mongod instances on different ports and dbpaths
mkdir -p ~/data/mongo/{rs0-1, rs0-2, rs0-3}
mongod --replSet rs0 --port 27017 --dbpath ~/data/mongo/rs0-1 --bind_ip localhost --oplogSizeMB 512 --fork --logpath ~/data/mongo/rs0-1/mongod.log
mongod --replSet rs0 --port 27018 --dbpath ~/data/mongo/rs0-2 --bind_ip localhost --oplogSizeMB 512 --fork --logpath ~/data/mongo/rs0-2/mongod.log
mongod --replSet rs0 --port 27019 --dbpath ~/data/mongo/rs0-3 --bind_ip localhost --oplogSizeMB 512 --fork --logpath ~/data/mongo/rs0-3/mongod.log
mongosh --port 27017 --eval 'rs.initiate({ _id: "rs0", members: [ {_id:0, host:"localhost:27017"}, {_id:1, host:"localhost:27018"}, {_id:2, host:"localhost:27019"} ] })'
Backup and restore
Backups are only useful if restores are rehearsed. Use dumps for small to medium datasets and filesystem snapshots for large ones. For replica sets, take logical backups from a secondary to reduce primary load.
Consistent dump with oplog for point-in-time window:
# From a secondary to reduce impact; include --oplog for consistency
mongodump --host localhost --port 27018 \
--out ./dump_$(date +%F_%H%M) --oplog
Restore rehearsal (drop and restore a test database):
mongorestore --nsFrom 'appdb.*' --nsTo 'appdb_restored.*' --drop ./dump_2024-01-01_1200
Checklist:
- Record MongoDB version and featureCompatibilityVersion with the dump.
- Verify user permissions exist or recreate them as part of restore.
- Time the restore to estimate RTO and compare with targets.
- After restore, run smoke tests: count collections, validate indexes, and run a small query suite.
Monitoring and security essentials
Monitoring:
- Quick view: mongostat --discover and mongotop for ops and collection-level activity.
- Server metrics: in mongosh, run db.serverStatus().metrics and db.currentOp().
- Logs: watch for slow query logs; set slowms appropriately.
- Key SLOs: p95 read/write latency, replication lag, cache hit ratio, page faults, connections.
Security basics:
- Enable authentication and create users with least privilege.
// In mongosh on the primary
use admin
// Create an admin user
db.createUser({
user: 'siteRootAdmin',
pwd: 'REPLACE_WITH_STRONG_PASSWORD',
roles: [ { role: 'root', db: 'admin' } ]
})
Restart mongod with authorization enabled, or set in config:
# mongod.conf
security:
authorization: enabled
Then create an app user restricted to the application database:
use appdb
db.createUser({
user: 'app_user',
pwd: 'REPLACE_WITH_STRONG_PASSWORD',
roles: [ { role: 'readWrite', db: 'appdb' } ]
})
TLS at rest and in transit:
- Generate server certs and enable TLS in mongod.conf:
net:
tls:
mode: requireTLS
certificateKeyFile: /path/to/server.pem
- Use TLS in the driver URI: add tls=true and verify certificates.
Auditing and hygiene:
- Rotate credentials, restrict network access, and disable unused interfaces.
- Review roles regularly and prefer built-in roles when possible.
Local Pilot Plan
Goal: a safe, narrow, and measurable pilot you can run on a laptop.
Scope:
- One 3-node replica set on localhost.
- A small Express API performing CRUD and one transaction.
- Basic durability: writeConcern=majority and retryable writes.
- A backup and restore rehearsal of the app database.
Steps:
- Stand up the replica set as shown earlier. Wait for primary election.
- Run the Express API. Seed 10k products and sample inventory.
- Traffic: run a simple loader for 5 minutes at 100 req/s mixed reads and writes.
- Failure drill: kill -9 the primary mongod process; observe automatic failover and API error rates. Ensure retryable writes and client timeouts avoid user-visible failures beyond a brief blip.
- Backup: run mongodump on a secondary with --oplog. Record duration and size.
- Restore: restore into appdb_restored and validate counts and indexes match.
Success criteria (measurable):
- p95 read < 30 ms and write < 50 ms under test load on a dev laptop (adjust to hardware).
- Failover recovery < 10 seconds and API resumes without manual intervention.
- Restore completes and sample checks pass.
Next pilot increment:
- Add auth and TLS, then repeat the same drills.
- Introduce a second collection and a second index to check regressions.
Conclusion
You now have a practical map of MongoDB: how components fit together, how data and control flow work, where failures happen, and how to steer durability, consistency, and latency. You also have runnable examples, a step-by-step operations workflow, and a local pilot you can measure on day one. Start with a simple replica set, set clear SLOs, prove your restore plan, and add complexity only when usage demands it.