MongoDB — Overview
Document database. Schemaless-ish (no fixed schema at the DB level, but you should treat documents as having a logical schema). Single-document atomicity by default; multi-document transactions since 4.0.
The model
// A collection (~ table) of documents (~ rows)
{
"_id": ObjectId("..."),
"name": "Alice",
"email": "alice@example.com",
"addresses": [
{ "type": "home", "city": "Berlin" },
{ "type": "work", "city": "Amsterdam" }
],
"preferences": { "lang": "en", "newsletter": true }
}
Nested objects, arrays — first-class. No JOINs traditionally; either embed related data or use $lookup (modern aggregation).
Embedding vs referencing
The most important MongoDB design decision.
Embed when
- Data is accessed together >80% of the time.
- The embedded data is owned by the parent (orders’ line items, post’s comments, user’s addresses).
- Lifetime ≤ parent’s lifetime.
- Size is bounded (no unbounded arrays).
// Order with embedded line items
{
"_id": "ord_42",
"user_id": "user_7",
"items": [
{"sku": "X1", "qty": 2, "price": 9.99},
{"sku": "Y2", "qty": 1, "price": 19.99}
],
"total": 39.97
}
Reference when
- Related data is shared across many parents.
- Updates to related data should be one-write.
- Size is unbounded.
{
"_id": "ord_42",
"user_id": "user_7" // reference
}
// Followed by:
{
"_id": "user_7",
"name": "Alice",
"email": "..."
}
Two reads instead of one — but updates to the user are one document.
Hybrid: extended reference
Embed a few frequently-accessed fields from the referenced entity to skip the join on hot paths:
{
"_id": "ord_42",
"user_id": "user_7",
"user_name": "Alice", // denormalized for display
"user_email_hash": "..."
}
Refresh when the user changes — eventual consistency on the embedded copy.
CRUD
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017")
db = client.myapp
# Insert
db.users.insert_one({"name": "Alice", "email": "alice@example.com"})
# Find
user = db.users.find_one({"email": "alice@example.com"})
for u in db.users.find({"active": True}).sort("created_at", -1).limit(20):
print(u)
# Update — atomic operators
db.users.update_one(
{"_id": user_id},
{"$set": {"name": "New Name"}, "$inc": {"login_count": 1}}
)
# Delete
db.users.delete_one({"_id": user_id})
Update operators are atomic at the document level: $set, $inc, $push, $pull, $addToSet, $unset. Multiple operators in one update are all applied atomically.
Async with Motor
from motor.motor_asyncio import AsyncIOMotorClient
client = AsyncIOMotorClient("mongodb://localhost:27017")
db = client.myapp
async def get_user(user_id):
return await db.users.find_one({"_id": user_id})
async def list_users():
async for user in db.users.find({"active": True}):
yield user
Same API as pymongo with await. Use Motor in FastAPI / async Django.
Schema design patterns
Bucket pattern (time-series)
Group time-series points into per-hour or per-day buckets:
{
"_id": ObjectId("..."),
"sensor_id": "s42",
"hour": "2026-05-12T18:00:00Z",
"samples": [
{"t": "2026-05-12T18:00:01Z", "value": 23.4},
{"t": "2026-05-12T18:00:02Z", "value": 23.5}
],
"sample_count": 2,
"min": 23.4,
"max": 23.5
}
One write per sample (push to the current bucket), one read per hour. Beats one-document-per-sample at scale.
Subset pattern
Heavy fields (full history, all reviews) stored separately; the parent embeds only the most recent N:
{
"_id": "product_42",
"name": "Widget",
"latest_reviews": [...] // most recent 10
}
Full review history in a separate reviews collection. Read for product page is cheap; “see all reviews” pages do a separate query.
Computed pattern
Pre-aggregate values at write time:
{
"_id": "product_42",
"review_count": 142,
"avg_rating": 4.3,
"ratings_breakdown": {"5": 80, "4": 40, "3": 15, "2": 5, "1": 2}
}
Avoid expensive aggregations on every read. Update on each new review (with $inc).
Polymorphic pattern
Different document shapes in one collection, distinguished by a type field:
{ "_id": ..., "type": "photo", "url": "...", "width": 1024, "height": 768 }
{ "_id": ..., "type": "video", "url": "...", "duration": 120, "codec": "h264" }
Single collection, single query. Application code branches on type.
Other files in this folder
Deep dives on specific topics:
- 02_indexes_compound_multikey.md — index types and design.
- 03_aggregation_pipeline.md —
$match,$group,$lookup, etc. - 04_transactions_concerns.md — multi-doc transactions, read/write concern.
- 05_replica_sharding.md — replica sets, sharding, shard key selection.
- 06_pitfalls_performance.md — 16 MB limit, unbounded arrays, common gotchas.
When NOT to use MongoDB
- Strong relational requirements (heavy JOINs, complex transactions across many tables).
- Strict schema validation as a primary need (Postgres’ JSONB + check constraints can match flexibility with stronger guarantees).
- Analytics workloads (Postgres / Snowflake / BigQuery are better at aggregations on huge datasets).
- Small-data CRUD where you’d benefit from Postgres’ richer ecosystem (advisory locks, listen/notify, foreign keys).
When MongoDB shines:
- Document-shaped data with deeply nested structure.
- High write throughput on a single document type.
- Horizontal scaling needs (sharding is first-class).
- Rapid schema iteration during product development.
Interview angle
- “Embedding vs referencing — decision criteria?” — embed when data is accessed together, owned by parent, lifetime ≤ parent, and bounded in size. Reference when data is shared, frequently updated independently, or unbounded. Hybrid: extended reference (embed a few hot fields, reference the rest).
- “How does MongoDB handle atomicity?” — atomic at the single document level by default. Multi-document transactions added in 4.0 (replica sets) and 4.2 (sharded); slower; use sparingly.
- “What’s a good use case for the bucket pattern?” — time-series data (IoT sensors, metrics). Group per-hour or per-day to reduce document count, improve compression, fit more in memory.
- “When would you NOT use MongoDB?” — heavy relational JOINs, strict normalized schema, complex analytics on huge datasets. Postgres with JSONB often matches MongoDB’s flexibility with stronger guarantees.
- “Async Python driver?” — Motor (
motor.motor_asyncio.AsyncIOMotorClient). Same API as pymongo withawait. Use in FastAPI / async Django. - “What’s the 16 MB document limit and how do you handle larger data?” — hard limit per document. Solutions: split (bucket / subset patterns), reference (separate collection), or for true binary blobs, GridFS (which splits files into chunks).