Model for access patterns
- Embed what you read together and what's bounded (an entry's small metadata).
- Reference what's unbounded or shared (assets, users, very large arrays).
- Useful patterns: Subset (hot fields in the main doc), Bucket (time-series), Computed (pre-aggregated counts), Extended reference (copy a few fields to avoid lookups), Schema versioning (a
_vfield and lazy migration).
Indexing: the ESR rule
Order compound index fields as Equality → Sort → Range.
// Query: tenant's published entries in a content type, newest first, updated in last 30 days
db.entries.find({ stack: s, branch: b, contentType: ct, updatedAt: { $gte: d } })
.sort({ updatedAt: -1 })
// Index: equality fields first, then sort/range
db.entries.createIndex({ stack: 1, branch: 1, contentType: 1, updatedAt: -1 })Always check explain("executionStats"): keysExamined ≈ nReturned is the goal.
Operational gotchas at scale
- Unbounded arrays and the 16 MB document limit.
- Large
$inorskip-based pagination: use range (keyset) pagination instead, e.g._id > lastId. - Index bloat: every index slows writes. Audit usage with
$indexStats. - Transactions: available on replica sets, but keep them short (under 1 s, few docs) and design aggregates to avoid them.
- Bulk writes: use
bulkWritewithordered: falsefor throughput, and handle per-item errors (which fits nicely with per-item job status). - Tenant isolation: every index should start with the tenant key.
Sources & further learning
Videos, courses, docs and books I recommend for this topic.
Related topics
Sharding & Partitioning
Split data across nodes so storage and throughput scale horizontally — choosing shard keys, range vs hash, hot spots and rebalancing.
Replication
Keep copies of data on multiple nodes for availability, durability and read scaling — single-leader, multi-leader and leaderless, sync vs async.
Multi-Tenant SaaS Architecture
Serve many customers from shared infrastructure while guaranteeing isolation of data, performance and configuration per tenant.
Transactional Outbox
Write the business change and the outgoing event in the same local transaction, then relay the event to the broker — no more "saved to DB but message lost".