All skills
Skillintermediate

pattern extended reference

**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.

Claude Code Knowledge Pack7/10/2026

Overview

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):

// 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):

CacheDon't Cache
Display name, avatarFull bio, description
Status, typeSensitive PII
Slowly-changing dataReal-time values (balance, inventory)
Fields used in sorting/filteringLarge 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 in profile
db.setProfilingLevel(1, { slowms: 20 }) // Disable afterwards
db.system.profile.find({
  "command.aggregate": { $exists: true },
  "command.pipeline.$lookup": {
    $exists: true
  }
}).sort({ millis: -1 }).limit(10)

// Check how often lookups hit same collections
db.system.profile.aggregate([
  { $match: { "command.pipeline.$lookup": { $exists: true } } },
  { $project: { pipeline: "$command.pipeline" } },
  { $unwind: "$pipeline" },
  { $project: { lookup: { $getField: { field: { $literal: '$lookup' }, input: '$pipeline' } } } },
  { $match: { "lookup": { $exists: true } } },
  { $group: { _id: "$lookup.from", count: { $sum: 1 } } }
])
// High count = candidate for extended reference

Reference: Reduce $lookup Operations