Once you have the big decision down (embed what you read together, reference what is large or shared), MongoDB schema design becomes a set of named patterns for recurring problems. You do not have to invent these; the MongoDB community named them years ago. Here are the four you will actually reach for, each with the problem it solves.
The attribute pattern: many similar fields
The problem: a product catalog where every item has a different set of specs. A laptop has RAM and screen size; a shirt has size and color; a book has an ISBN. Model each as its own field and you get wide, sparse documents and a new index for every attribute anyone might filter on.
The pattern: store the variable fields as an array of key-value pairs, and index the array once:
{
name: "Arctic Laptop",
specs: [
{ k: "ram_gb", v: 32 },
{ k: "screen_in", v: 14 },
{ k: "weight_kg", v: 1.2 }
]
}
db.products.createIndex({ "specs.k": 1, "specs.v": 1 })
One index now serves a filter on any attribute. You traded a pile of single-purpose indexes for one, which is a strict win on write cost. The MongoDB pattern catalog has the full set if you want to go deeper.
The bucket pattern: high-volume, append-only data
The problem: a sensor writes a reading a second. One document per reading is millions of tiny documents and an index the size of a small country. And if you embed the readings in the device document, the array grows without bound toward the 16MB wall.
The pattern: bucket many readings into one document, usually by time window:
{
deviceId: "sensor-7",
hour: ISODate("2026-06-27T14:00:00Z"),
readings: [
{ t: 0, temp: 21.4 },
{ t: 1, temp: 21.5 }
// up to one hour of readings, then a new bucket
],
count: 3600
}
One document per device per hour instead of one per second. Far fewer documents, a far smaller index, and the array has a natural ceiling so it never becomes an unbounded-growth problem. This is how time-series data wants to be stored.
The computed pattern: precompute the expensive answer
The problem: every product page shows an average rating, and computing it means aggregating the reviews collection on every page load. Cheap once, expensive at scale.
The pattern: compute it when the data changes, store the result, and read it for free:
// on the product, kept current when a review lands
{ name: "Arctic Hoodie", ratingAvg: 4.6, ratingCount: 218 }
You do the math on write, which is rare, instead of on read, which is constant. The e-commerce schema post uses exactly this for product ratings.
The extended reference: copy a few fields to skip a join
The problem: an order references a customer by id, but every time you display the order you also want the customer's name and city, so you do a lookup.
The pattern: copy the handful of fields you always show onto the order, and keep the reference for everything else:
{
_id: ObjectId("..."),
customer: { id: 123, name: "Sam Rivers", city: "Portland" }, // extended reference
total: 6900
}
You denormalize a little, on purpose, to skip a join on the common read. The trade is keeping the copied fields in sync when they change, so use it for fields that rarely change (a name, a city) and not for volatile ones. The order's price snapshot in the e-commerce example is a close cousin of this.
Patterns drift too
Every one of these patterns denormalizes something, which means two copies that can disagree: a stale ratingAvg, an extended reference whose name went out of date, a bucket that stopped rolling over. That is schema drift wearing a design pattern, and it is the price of the read performance you bought. The foundations are in MongoDB schema design; these patterns sit on top.
mongoui's Schema doctor samples the collection and reports the real shapes, so a computed field that quietly stopped updating, or a bucket document that grew past its intended size, shows up as the anomaly it is. It runs on your machine, so your documents never leave your network. Reach for the pattern that fits the problem, and check now and then that the denormalized copy still agrees with the source.
