One-to-Many Relationships
One-to-many is the most common relationship in any schema — one order has many line items, one author has many posts, one department has many employees. MongoDB gives you four distinct ways to model it, and picking the right one depends on cardinality and how the data is accessed.
The Four Patterns
Embedded array — children live inside the parent document as an array of sub-documents.
Array of references — parent stores an array of child IDs; children live in their own collection.
Parent reference (child-referencing-parent) — each child document stores the parent's ID; no array on the parent at all.
Two-way reference — parent stores child IDs AND each child stores the parent ID (rare, more write overhead, used when both directions need to be indexed independently).
Pattern 1: Embedded Array
Best when the "many" side is small and bounded, and is always read alongside the parent. The classic example: an order and its line items.
Order with embedded line items
{
_id: ObjectId("order1"),
customerId: ObjectId("cust1"),
status: "shipped",
createdAt: ISODate("2026-01-15"),
items: [
{ productId: ObjectId("p1"), name: "Wireless Mouse", price: 24.99, qty: 2 },
{ productId: ObjectId("p2"), name: "USB-C Cable", price: 9.99, qty: 1 }
],
total: 59.97
}
// Reading the whole order — one query, no joins
db.orders.findOne({ _id: ObjectId("order1") })
// Adding a line item is a single atomic update
db.orders.updateOne(
{ _id: ObjectId("order1") },
{ $push: { items: { productId: ObjectId("p3"), name: "Keyboard", price: 49.99, qty: 1 } },
$inc: { total: 49.99 } }
)Pattern 2: Array of References
Use this when children are shared, queried independently, or too large/unbounded to embed, but you still want a fast way to list "this parent's children" without a full collection scan.
Author with an array of post IDs
// authors collection
{ _id: ObjectId("author1"), name: "Priya Shah", postIds: [ObjectId("post1"), ObjectId("post2")] }
// posts collection — the actual content lives here
{ _id: ObjectId("post1"), title: "Intro to Indexes", authorId: ObjectId("author1") }
{ _id: ObjectId("post2"), title: "Aggregation Basics", authorId: ObjectId("author1") }
// Fetch an author's posts via $lookup
db.authors.aggregate([
{ $match: { _id: ObjectId("author1") } },
{ $lookup: {
from: "posts",
localField: "postIds",
foreignField: "_id",
as: "posts"
} }
])Pattern 3: Parent Reference (the scalable default)
For anything that can grow without bound, put the reference on the "many" side instead. The parent document never changes size as children are added — this is the pattern used for comments, order history, sensor readings, log entries, and virtually every "one-to-squillions" relationship.
Order and order-items, worked example
// orders collection stays small and fixed-size
{ _id: ObjectId("order1"), customerId: ObjectId("cust1"), status: "shipped", total: 59.97 }
// order_items collection — one document per line item, referencing the order
{ _id: ObjectId("i1"), orderId: ObjectId("order1"), productId: ObjectId("p1"), name: "Wireless Mouse", price: 24.99, qty: 2 }
{ _id: ObjectId("i2"), orderId: ObjectId("order1"), productId: ObjectId("p2"), name: "USB-C Cable", price: 9.99, qty: 1 }
// Index the foreign key
db.order_items.createIndex({ orderId: 1 })
// Fetch all items for an order — fast, indexed
db.order_items.find({ orderId: ObjectId("order1") })
// Fetch order + items together via $lookup when a joined view is needed
db.orders.aggregate([
{ $match: { _id: ObjectId("order1") } },
{ $lookup: { from: "order_items", localField: "_id", foreignField: "orderId", as: "items" } }
])Choosing by Cardinality and Access Pattern
Situation | Recommended Pattern |
|---|---|
A handful of items, always shown with parent | Embedded array |
Dozens to low thousands, mostly read with parent | Embedded array (watch document size) |
Children queried/paginated independently of parent | Parent reference |
Children shared across multiple parents | Array of references or parent reference |
Unbounded growth (logs, events, readings) | Parent reference (child holds the FK) |
Need fast "list all children of X" without $lookup | Parent reference + index on the FK field |
Combining Patterns: Recent Items Embedded, Full History Referenced
// Common hybrid: embed the last few items for a fast dashboard view,
// keep the full history in a referenced collection for deep queries
{
_id: ObjectId("cust1"),
name: "Acme Corp",
recentOrders: [ // last 5, embedded — fast to render on the customer page
{ orderId: ObjectId("order9"), total: 120.00, date: ISODate("2026-06-01") }
]
}
// Full order history lives in the orders collection, queried by customerId
// when the user clicks "view all orders"Summary
Embedded array: simplest, atomic, best for small and bounded child sets read with the parent.
Array of references: good middle ground, but still grows with the parent — watch cardinality.
Parent reference: the scalable default for anything unbounded; index the foreign key field.
Hybrid (recent embedded + full referenced) balances fast reads with unlimited history.