skip to content

In MongoDB's subset pattern, what stays embedded in the main document and how do you cap it?

level: juniorimportance: should knowfreq 45%

answer

  1. only the part the screen renders
  2. full set lives in its own collection
  3. embedded array must stay bounded
  4. $push modifiers do the truncation
  5. $each is required alongside $slice

basics

~20 s

The subset pattern embeds only the small, hot slice of a large related set — the ten newest reviews, say — and keeps the complete set in its own collection. A $push with the $slice modifier keeps the embedded slice capped.

solid answer

~50 s

You embed the part of a related set that the common screen actually renders, and nothing more. A product page shows the product plus its handful of newest reviews, so the product document carries `recentReviews` with ten entries while all 4,300 reviews live in a `reviews` collection keyed by `productId`. The common read becomes a single document fetch; the rare "see all reviews" click is a second, paginated query against the reviews collection. Maintaining the slice is a one-liner: `$push` with `$each`, `$sort` and `$slice` appends the new review, orders by date and truncates the array back to ten in the same atomic update. The point of the pattern is bounded documents — the embedded array can never grow with the data, so the document stays small, cache-friendly and far from the 16 MB BSON limit.

code

javascript · 8 lines
javascript
// keep the ten newest reviews embedded on the product
db.products.updateOne(
  { _id: "p-991" },
  { $push: { recentReviews: { $each: [review], $sort: { date: -1 }, $slice: 10 } } }
)

// the authoritative copy goes to its own collection
db.reviews.insertOne({ ...review, productId: "p-991" })

go deeper

for a junior

Know the shape: the parent carries a small fixed-size array of the newest children, the full set sits in its own collection, and the array is capped by $push with $slice.

for a middle

Be able to write the update correctly — $each is required with $sort and $slice, sorting happens before truncation — and to explain why the bounded array is what protects the document from unbounded growth.

for a senior

Show how you repair drift between the embedded slice and the authoritative collection, and recognize when a slice rewritten on every child write turns a hot parent document into a write hotspot.

for a principal

Frame it as a caching decision inside the schema: which screens justify a denormalized slice, what the repair and backfill story is, and how the choice holds when one parent becomes enormously more popular than the rest.

## What the pattern is The subset pattern says: embed a *bounded slice* of a large related set in the parent document, and keep the authoritative full set in a separate collection. It is a named answer to a specific tension — the screen you render a thousand times a minute needs only a few of the children, but the child set itself is unbounded. The canonical example is a product page. It renders the product plus the three-to-ten newest reviews. The product has 4,300 reviews in total. Embedding all of them makes every product read drag 4,300 subdocuments through cache and marches the document toward the 16 MB BSON limit. Embedding none of them makes the hottest page in the application a two-query page. ```json { "_id": "p-991", "name": "Mechanical keyboard", "price": 149.0, "reviewCount": 4300, "avgRating": 4.4, "recentReviews": [ { "_id": "r-77", "author": "kim", "rating": 5, "date": "2026-08-19T…", "text": "…" } ] } ``` `recentReviews` holds ten entries forever. `reviewCount` and `avgRating` are the computed pattern riding along, so the summary line needs no second query either. ## Keeping the slice capped The maintenance is a single atomic update using the `$push` modifiers: ```javascript db.products.updateOne( { _id: "p-991" }, { $push: { recentReviews: { $each: [review], $sort: { date: -1 }, $slice: 10 } } } ) ``` Three details matter. `$each` is mandatory whenever `$sort` or `$slice` is used — the modifiers are arguments to a `$each` push, not standalone operators. `$sort` runs before `$slice`, so the array is ordered newest-first and then truncated. A **positive** `$slice` keeps the first N elements of the sorted array; a **negative** `$slice` keeps the last N. With `$sort: { date: -1 }` and `$slice: 10` you keep the ten newest; without the sort, `$slice: -10` keeps the ten most recently appended, which is usually equivalent but not identical if writes arrive out of order. The full review is written to the `reviews` collection separately. Those are two writes to two documents, so they are not atomic together unless you wrap them in a transaction; in practice most teams accept that the embedded slice is a cache and repair it by re-deriving from the reviews collection if it drifts. ## Reading around it The product page issues one query. The "all reviews" view queries `db.reviews.find({ productId: "p-991" }).sort({ date: -1 })` with pagination and an index on `{ productId: 1, date: -1 }`. Notice that the embedded copy is never the source of truth for anything except the render — deletion of a review means removing it from the reviews collection and, if it happens to be in the slice, `$pull`-ing it from the array and topping the array back up from the collection. ## Subset versus extended reference These two get confused constantly, and interviewers ask the difference. **Subset** takes a *few of many related documents* and embeds them whole (or nearly whole). **Extended reference** takes *one referenced document* and embeds a *few of its fields* next to the reference — an order line keeping `{ customerId, customerName, city }` so the order list renders without a join. One bounds cardinality, the other bounds width. They compose freely: a subset of reviews where each embedded review carries an extended reference to its author. ## Choosing the slice Three questions decide it. What does the dominant screen actually display — that fixes N and the sort order. How wide is one embedded element — ten fat elements can be worse than fifty thin ones, and you can embed a trimmed projection rather than the whole child. How often does the slice change — a slice rewritten on every child write means the parent document is rewritten on every child write, which can turn a read optimization into a write hotspot on popular parents. ## How to answer it Name the pattern, state the invariant ("the embedded array is bounded and never grows with the data"), show the `$push`/`$sort`/`$slice` update, and say plainly what the second collection is for. Adding that the embedded slice is a derived cache — repairable, not authoritative — signals you have maintained one rather than only read about it.

  • How does the subset pattern differ from the extended reference pattern?
    Subset embeds a bounded number of whole related documents — the ten newest reviews out of thousands. Extended reference embeds a few *fields* of one referenced document — the customer's name and city next to `customerId` — so a list renders without a second lookup. One bounds how many children you carry, the other bounds how much of a single referenced document you copy. They compose.
  • What happens when a review inside the embedded slice is deleted?
    Delete it from the reviews collection, `$pull` it from the parent's array by its `_id`, and top the array back up from the collection so the slice still holds N entries. The embedded copy is a derived cache, so the repair path — re-deriving the slice from the authoritative collection — should exist as a routine, not an emergency script.

saying these in an interview costs you the question

  • Calls the embedded slice the source of truth
  • Uses $slice without $each and expects it to work
  • Lets the embedded array grow as reviews accumulate
  • Confuses subset with extended reference
  • Assumes the parent update and child insert are atomic together

context