← Files MongoDB AtlasARCHIVED FILE
references/pattern-extended-reference.md
3.32 KB · Sep 30, 2026 · 22:47 UTC
---
title: Use Extended Reference Pattern
impact: MEDIUM
impactDescription: "Reduces repeated `$lookup` on hot paths by caching selected referenced fields"
tags: schema, patterns, extended-reference, denormalization, caching
---
## Use Extended Reference Pattern
**Copy frequently-accessed fields from referenced documents into the parent.** If you always display author name with articles, embed it. This eliminates $lookup for common queries while keeping the full data normalized—best of both worlds.
**Incorrect (always $lookup for display data):**
```javascript
// Order references customer by ID only
{
_id: "order123",
customerId: "cust456", // Customer reference by ID only
items: [...],
total: 299.99
}
// Every order list/display requires $lookup
db.orders.aggregate([
{ $match: { status: "pending" } },
{ $lookup: {
from: "customers",
localField: "customerId",
foreignField: "_id",
as: "customer"
}},
{ $unwind: "$customer" }
])
// Repeated joins add avoidable work for a common list view
```
**Correct (extended reference):**
Embed frequently-needed customer fields directly in the order document: include a `customer` subdocument with `_id` (kept as a reference for full lookups), `name`, and `email`. The order list query returns customer display data without `$lookup`. Full customer data is still available via a targeted read to the `customers` collection when needed.
**Keeping cached data in sync:**
When the source field changes (e.g. customer name), update the source collection first, then update cached copies in the orders collection using `updateMany` on the embedded reference `_id`. This can be done synchronously or asynchronously via Change Streams / background jobs. For data that changes more often, add a `cachedAt` timestamp to the embedded subdocument so the application can refresh on read when the cache exceeds a staleness threshold.
**What to cache (extend):**
| Cache | Don't Cache |
|-------|-------------|
| Display name, avatar | Full bio, description |
| Status, type | Sensitive PII |
| Slowly-changing data | Real-time values (balance, inventory) |
| Fields used in sorting/filtering | Large binary data |
**Alternative: Hybrid pattern with cache expiry:**
Keep both a bare reference (`customerId`) and an optional cache subdocument (`customerCache`) with `name`, `email`, and `cachedAt`. On read, if the cache is missing or older than a threshold (e.g. one day), refresh it from the `customers` collection and write the updated cache back to the order.
**When NOT to use this pattern:**
- **Frequently-changing data**: If customer name changes daily, update overhead exceeds $lookup cost.
- **Large cached payloads**: Don't embed 50KB of author bio in every article.
- **Sensitive data segregation**: Don't copy PII into collections with different access controls.
- **Writes >> Reads**: If writes greatly outnumber reads, caching adds overhead.
## Verify with
Find lookup-heavy aggregations. See how often lookups hit the same collection. High count = candidate for extended reference
For Atlas M10+ use $queryStats. See [Query Stats](references/source-query-stats.md) and [Slow query logs](references/source-slow-query-logs.md)
Use codebase if available, ask the user.
Reference: [Reduce $lookup Operations](https://mongodb.com/docs/manual/data-modeling/design-antipatterns/reduce-lookup-operations/)
SHA-256: 8c0b90574216e56b359e882b6174ceae1d50d8133c334684185cf2b416aab31c