MongoDB — Overview

6 interview angles 4 min read source

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:

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 with await. 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).