Referencing Between Documents
Complete the full lesson to earn 25 points — 50 with Pro
Work through each section, then tap “Mark as Complete” on the last one.
✦ Skip the page breaks, the wait, and see fewer ads — read each lesson on a single page with Pro
Lesson: Referencing Between Documents in Non-Relational Databases
Introduction: The Architecture of Relationships in NoSQL
In the world of relational databases, we are taught that normalization is the gold standard. We split data into multiple tables, link them with foreign keys, and perform expensive joins to reconstruct our entities. However, when moving to non-relational or document-oriented databases like MongoDB, CouchDB, or Firestore, the paradigm shifts. While these databases encourage embedding related data to keep reads fast and localized, real-world data is rarely flat. Sooner or later, you will encounter scenarios where data needs to be shared, updated in multiple places, or simply grows too large to nest within a single document.
This is where the concept of "referencing" comes into play. Referencing is the process of storing a reference (usually an ID) to another document instead of embedding the entire object. Understanding when to use referencing versus embedding is the single most important architectural decision you will make when designing a NoSQL data model. If you choose the wrong approach, you end up with either massive, unmanageable documents that hit size limits or a system that performs so many round-trips to the database that it loses the performance benefits of a non-relational store.
This lesson explores the mechanics, strategies, and trade-offs of referencing between documents. We will examine how to implement manual references, discuss the implications for application-level joins, and define the design patterns that keep your data consistent and your queries performant.
The Core Concept: Embedding vs. Referencing
Before we dive into the implementation of references, we must establish a clear mental model of the alternatives. Embedding is the "default" for many NoSQL developers because it aligns perfectly with the document-oriented nature of the data. If you have an Order and a list of LineItems, embedding those items directly inside the order document is usually the correct choice because an order's line items rarely exist independently of the order itself.
Referencing, conversely, is used when data has a "many-to-many" relationship, or when one piece of data is frequently updated and referenced by many other documents. If you have a User profile and a set of Product reviews, you wouldn't want to embed the User object inside every single review document. If the user changes their username or profile picture, you would have to perform a massive database-wide update to every review they ever wrote. By using a reference—storing only the user_id inside the review document—you ensure the user information is stored in one place.
Callout: The "Golden Rule" of NoSQL Modeling The decision between embedding and referencing is determined by access patterns. If you need to access the data together 99% of the time, embed it. If you need to access the data independently, or if the data is shared across multiple entities, use a reference.
Implementing Manual References
In most document databases, a manual reference is simply a field that holds the unique identifier of another document. Unlike relational databases, the engine does not enforce "referential integrity." There is no built-in mechanism to prevent you from deleting a document that is still being referenced by another. You, as the application developer, are responsible for managing these links.
Example: The E-commerce Product and Category Relationship
Imagine an e-commerce platform where products belong to categories. A category might contain thousands of products. If you embed the category inside the product, updating the category name would be a nightmare. Instead, we use a reference.
Category Document:
{
"_id": "cat_electronics_001",
"name": "Electronics",
"description": "Gadgets, phones, and computers"
}
Product Document:
{
"_id": "prod_phone_99",
"name": "Smartphone X",
"price": 699,
"category_id": "cat_electronics_001"
}
In this setup, when you display the product page, your application logic performs two steps:
- Fetch the product document using
prod_phone_99. - Extract
category_idand fetch the category document using that ID.
Handling Relationships at the Application Level
Because document databases do not perform "joins" in the traditional sense, your application code acts as the relational engine. This is known as "Application-Level Joins." While this might sound like a performance penalty, it is often faster than a complex SQL join because you are performing two simple, primary-key lookups rather than a complex table scan or nested loop join.
Note: Many modern document databases now support
$lookup(or similar aggregation framework operators) to perform joins on the server side. While convenient for reports or administrative tasks, avoid using these in your critical path for high-frequency application queries.
Advanced Referencing Patterns
Once you master the basic ID reference, you will find that certain patterns emerge to handle more complex scenarios. These patterns help balance the trade-off between performance (number of reads) and consistency (the difficulty of updating data).
1. The "Extended Reference" Pattern
Sometimes, you want the benefits of a reference (to keep the main document small) but you also want the speed of an embedded document (to avoid that second lookup for common fields). The Extended Reference pattern involves storing the ID of the referenced document, along with a few frequently accessed fields.
Product Document with Extended Reference:
{
"_id": "prod_phone_99",
"name": "Smartphone X",
"category": {
"id": "cat_electronics_001",
"name": "Electronics"
}
}
In this scenario, if you just need to list products with their category name, you don't need a second query. However, if you need the full category description or metadata, you can still use the id to fetch the complete category document.
2. The "Two-Way Referencing" Pattern
In some cases, you need to navigate the relationship in both directions. For example, a User might have many Groups, and a Group might have many Users. Storing an array of group_ids in the User document and an array of user_ids in the Group document allows you to query from either side.
User Document:
{
"_id": "user_123",
"username": "jdoe",
"groups": ["group_admin", "group_editors"]
}
Group Document:
{
"_id": "group_admin",
"members": ["user_123", "user_456"]
}
Warning: Two-way referencing requires strict transaction management. If you add a user to a group, you must update both the user document and the group document. If one update fails, your data will be in an inconsistent state. Always ensure your database supports multi-document transactions if you choose this path.
Step-by-Step: Designing a Referencing Strategy
When faced with a new data model, follow these steps to determine if referencing is the right choice for your specific entity relationship.
Step 1: Analyze the Cardinality
Determine if the relationship is one-to-one, one-to-many, or many-to-many.
- One-to-One: Usually better to embed.
- One-to-Many: If the "many" side is small and bounded (e.g., a user has 3 addresses), embed. If it is unbounded (e.g., a user has 10,000 activity logs), use a reference.
- Many-to-Many: Use a reference.
Step 2: Evaluate Update Frequency
Ask yourself: "How often does the child data change?" If the child data is static (like a product's SKU or a user's birthdate), embedding is safe. If the child data changes frequently or is shared (like a user's display name or a product's price), referencing is safer because you only update it in one location.
Step 3: Assess Read Requirements
Do you always need to display the child data when viewing the parent? If the answer is "yes, always," consider embedding or the Extended Reference pattern. If the child data is only needed when the user clicks a "Details" link, use a standard reference.
Step 4: Define Consistency Requirements
Can your application tolerate a momentary mismatch between the parent and the referenced data? If the answer is "no," you need to account for the overhead of updating multiple documents. If you have a high-read, low-write volume, this is usually acceptable.
Common Pitfalls and How to Avoid Them
Even experienced developers fall into traps when implementing document references. Here are the most common mistakes and strategies to avoid them.
Pitfall 1: The "N+1" Query Problem
The N+1 problem occurs when you fetch a list of parent documents (e.g., 20 products) and then execute a separate database query for every single one of those products to fetch their referenced category. This can kill your application performance.
Solution: Use "Batch Loading" or "In-Clause" lookups. Instead of querying for categories one by one, collect all the unique category IDs from your 20 products and perform a single query: db.categories.find({ _id: { $in: [cat1, cat2, cat3] } }).
Pitfall 2: Dangling References
In a relational database, a foreign key constraint prevents you from deleting a record if other records point to it. In document databases, if you delete a category but forget to remove the category_id from your products, you end up with "dangling references."
Solution: Implement a cleanup routine or use a "soft delete" strategy. Instead of deleting the category, set a deleted: true flag. This ensures that any product referencing that ID still has a valid document to point to, even if the user experience logic treats it as non-existent.
Pitfall 3: Over-Normalizing
Sometimes, developers coming from SQL backgrounds try to normalize their document database too much, creating a "collection-heavy" architecture that looks like a bunch of small tables linked together. This defeats the purpose of choosing a document database.
Solution: If you find yourself needing to join more than three collections to display a single page, you have likely over-normalized. Re-evaluate your document design and consider embedding some of that data.
Callout: When to Break the Rules Performance is rarely about theoretical purity. If your application is slow, the "correct" data model is the one that makes your primary queries fast. If that means duplicating data (denormalization) to avoid a reference lookup, do it. Modern storage is cheap; latency is expensive.
Comparison: Embedding vs. Referencing
| Feature | Embedding | Referencing |
|---|---|---|
| Data Locality | High (All in one document) | Low (Distributed) |
| Write Performance | High (Atomic update) | Lower (Multi-doc update) |
| Read Performance | High (Single read) | Lower (Requires multiple lookups) |
| Data Size | Can hit document limits | Efficient |
| Consistency | Easy (Atomic) | Harder (Requires transactions) |
| Complexity | Simple | Higher (Requires app logic) |
Best Practices for Production Systems
When you implement referencing in a production environment, you should adhere to a set of standards that ensure long-term maintainability.
1. Use Meaningful ID Naming
While many databases generate random UUIDs, using a consistent naming convention for your references helps in debugging. If you see a field called owner_id, it is immediately clear that this references a user. If you see parent_id, it is ambiguous.
2. Index Your References
This is the most common performance oversight. If you have a product document that references a category_id, you must have an index on the category_id field in the product collection. Without an index, every time you want to find all products in a category, the database must perform a full collection scan.
// Example index creation in a typical document DB
db.products.createIndex({ category_id: 1 });
3. Consider Eventual Consistency
If you have a high-traffic application, updating a referenced object across thousands of documents might be too slow for an atomic transaction. In these cases, use an asynchronous background job to update the referenced data. Accept that the data might be slightly stale for a few milliseconds, but ensure that your application handles this gracefully.
4. Document Your Schema
NoSQL databases are "schema-less," which is a double-edged sword. It means you don't have to define a schema, but it also means no one knows how the documents are related. Create a data dictionary or an ERD (Entity Relationship Diagram) that visually maps which collections reference others.
Frequently Asked Questions (FAQ)
Q: Can I use foreign keys in a NoSQL database? A: Most NoSQL databases do not support hard foreign key constraints. You are responsible for the integrity of your references via application logic or database-level triggers.
Q: How many levels of references should I have? A: Ideally, try to stick to one or two levels of referencing. If your application requires traversing three or more levels of references (e.g., User -> Order -> Item -> Supplier -> City), your data model is likely too complex and should be flattened or embedded.
Q: What if I need to perform a join for an analytical report? A: Use your database's aggregation framework or an ETL process to move data into a separate analytical store (like a data warehouse). Do not attempt to run complex multi-collection joins on your production transactional database.
Q: Is it ever okay to store the same data in two different places?
A: Yes, this is called denormalization. If you have a User name that is needed in a Comment document, storing the name in the Comment document (even if it's also in the User document) is a valid pattern if it saves you a lookup every time you render a comment section.
Summary of Key Takeaways
- Context is King: The choice between embedding and referencing is entirely dependent on your specific access patterns. There is no "perfect" schema; there is only the schema that works for your application's read and write requirements.
- The "Independent Entity" Rule: Use references when the related data needs to exist independently, is shared across multiple entities, or is too large to fit into a single document.
- Application-Level Joins: In the absence of SQL joins, your application logic must handle the task of fetching related documents. Use batching (the
INoperator) to avoid the N+1 query problem. - Indexing is Mandatory: Any field used to reference another document should have a database index. Without it, your application will face significant performance degradation as your dataset grows.
- Manage Your Integrity: Since NoSQL databases usually lack foreign key constraints, you must build mechanisms into your application to handle orphaned references or, at the very least, design your code to handle missing references gracefully.
- Prefer Simplicity: If you find yourself over-engineering your referencing strategy to the point where your code is difficult to follow, take a step back and reconsider if embedding or a hybrid approach (Extended Reference) might actually be cleaner and faster.
- Performance First: Always prioritize the performance of your most frequent queries. If duplicating data (denormalization) makes your main user flow faster, do not hesitate to do it, provided you have a strategy to handle the eventual consistency of that data.
By following these principles, you will be able to design non-relational data models that are not only performant and scalable but also maintainable as your application evolves. Remember that NoSQL is about flexibility; don't be afraid to iterate on your schema as your understanding of the data grows.
Reach the last section to complete this lesson and earn points — you're on section 1 of 9.
- Introduction to Cosmos DB Data Modeling
- Introduction to Cosmos DB Data Modeling Quiz5q
- Multiple Entity Types in Same Container
- Multiple Entity Types in Same Container Quiz5q
- Storing Related Entities in Same Document
- Storing Related Entities in Same Document Quiz5q
- Denormalizing Data Across Documents
- Denormalizing Data Across Documents Quiz5q
- Referencing Between Documents
- Referencing Between Documents Quiz5q
- Partition Keys and Document IDs
- Partition Keys and Document IDs Quiz5q
- Time to Live (TTL) Configuration
- Time to Live (TTL) Configuration Quiz5q
- Document Versioning Strategies
- Document Versioning Strategies Quiz5q
- Schema Versioning Patterns
- Schema Versioning Patterns Quiz5q
- Choosing Partition Strategies
- Choosing Partition Strategies Quiz5q
- Partition Key Selection Best Practices
- Partition Key Selection Best Practices Quiz5q
- Transactions and Partition Keys
- Transactions and Partition Keys Quiz5q
- Cross-Partition Query Costs
- Cross-Partition Query Costs Quiz5q
- Data Distribution Analysis
- Data Distribution Analysis Quiz5q
- Throughput Distribution Planning
- Throughput Distribution Planning Quiz5q
- Synthetic Partition Keys
- Synthetic Partition Keys Quiz5q
- Hierarchical Partition Keys
- Hierarchical Partition Keys Quiz5q
- Throughput and Storage Requirements
- Throughput and Storage Requirements Quiz5q
- Serverless vs Provisioned Throughput
- Serverless vs Provisioned Throughput Quiz5q
- Database-Level Provisioned Throughput
- Database-Level Provisioned Throughput Quiz5q
- Granular Scale Units
- Granular Scale Units Quiz5q
- Global Distribution Costs
- Global Distribution Costs Quiz5q
- Configuring Throughput in Portal
- Configuring Throughput in Portal Quiz5q
- Gateway vs Direct Connectivity Mode
- Gateway vs Direct Connectivity Mode Quiz5q
- Creating Database Connections
- Creating Database Connections Quiz5q
- Azure Cosmos DB Emulator
- Azure Cosmos DB Emulator Quiz5q
- Connection Error Handling
- Connection Error Handling Quiz5q
- Singleton Pattern for Clients
- Singleton Pattern for Clients Quiz5q
- Global Distribution Regions
- Global Distribution Regions Quiz5q
- Threading and Parallelism
- Threading and Parallelism Quiz5q
- Arrays and Nested Objects Queries
- Arrays and Nested Objects Queries Quiz5q
- Correlated Subqueries
- Correlated Subqueries Quiz5q
- Array and Type-Checking Functions
- Array and Type-Checking Functions Quiz5q
- Mathematical and String Functions
- Mathematical and String Functions Quiz5q
- Date Functions in Queries
- Date Functions in Queries Quiz5q
- Point Operations vs Query Operations
- Point Operations vs Query Operations Quiz5q
- CRUD Point Operations
- CRUD Point Operations Quiz5q
- Patch Operations for Updates
- Patch Operations for Updates Quiz5q
- Transactional Batch Operations
- Transactional Batch Operations Quiz5q
- Bulk Operations with SDK
- Bulk Operations with SDK Quiz5q
- Optimistic Concurrency with ETags
- Optimistic Concurrency with ETags Quiz5q
- Query Pagination and Continuation
- Query Pagination and Continuation Quiz5q
- Cosmos DB Mirroring for Fabric
- Cosmos DB Mirroring for Fabric Quiz5q
- Mirroring vs Spark Connector
- Mirroring vs Spark Connector Quiz5q
- Enabling Analytical Store
- Enabling Analytical Store Quiz5q
- Synapse Spark and SQL Queries
- Synapse Spark and SQL Queries Quiz5q
- Change Data Capture in Analytical Store
- Change Data Capture in Analytical Store Quiz5q
- Azure Functions and Event Hubs Integration
- Azure Functions and Event Hubs Integration Quiz5q
- Denormalization with Change Feed
- Denormalization with Change Feed Quiz5q
- Referential Integrity with Change Feed
- Referential Integrity with Change Feed Quiz5q
- Azure AI Search Integration
- Azure AI Search Integration Quiz5q
- Azure Functions Change Feed Trigger
- Azure Functions Change Feed Trigger Quiz5q
- Consuming Change Feed with SDK
- Consuming Change Feed with SDK Quiz5q
- Change Feed Estimator
- Change Feed Estimator Quiz5q
- Denormalization via Change Feed
- Denormalization via Change Feed Quiz5q
- Aggregation Persistence with Change Feed
- Aggregation Persistence with Change Feed Quiz5q
- Read-Heavy vs Write-Heavy Indexing
- Read-Heavy vs Write-Heavy Indexing Quiz5q
- Index Type Selection
- Index Type Selection Quiz5q
- Custom Indexing Policies
- Custom Indexing Policies Quiz5q
- Composite Index Implementation
- Composite Index Implementation Quiz5q
- Index Performance Optimization
- Index Performance Optimization Quiz5q
- Response Status Codes and Metrics
- Response Status Codes and Metrics Quiz5q
- Normalized RU Consumption Monitoring
- Normalized RU Consumption Monitoring Quiz5q
- Server-Side Latency Metrics
- Server-Side Latency Metrics Quiz5q
- Data Replication Monitoring
- Data Replication Monitoring Quiz5q
- Azure Monitor Alerts Configuration
- Azure Monitor Alerts Configuration Quiz5q
- Resource Logs Implementation
- Resource Logs Implementation Quiz5q
- Partition Throughput Monitoring
- Partition Throughput Monitoring Quiz5q
- Encryption Key Management
- Encryption Key Management Quiz5q
- Network-Level Access Control
- Network-Level Access Control Quiz5q
- Data Encryption Configuration
- Data Encryption Configuration Quiz5q
- Azure RBAC for Control Plane
- Azure RBAC for Control Plane Quiz5q
- Microsoft Entra ID for Data Plane
- Microsoft Entra ID for Data Plane Quiz5q
- CORS Settings Configuration
- CORS Settings Configuration Quiz5q
- Customer-Managed Keys
- Customer-Managed Keys Quiz5q
- Always Encrypted Implementation
- Always Encrypted Implementation Quiz5q
- Data Movement Strategy Selection
- Data Movement Strategy Selection Quiz5q
- SDK Bulk Operations for Data Movement
- SDK Bulk Operations for Data Movement Quiz5q
- Azure Data Factory Pipelines
- Azure Data Factory Pipelines Quiz5q
- Kafka Connector Integration
- Kafka Connector Integration Quiz5q
- Azure Stream Analytics Integration
- Azure Stream Analytics Integration Quiz5q
- Cosmos DB Spark Connector
- Cosmos DB Spark Connector Quiz5q
Enjoying the courses?
Everything stays free. Pro shows fewer ads, doubles the points you earn on every lesson and quiz so you progress twice as fast, unlocks half of every practice exam — plus full case studies — with the Learn & Exam study modes, and lets you read each lesson on one page.
- ✓ Fewer advertisements
- ✓ 2× points per lesson & quiz
- ✓ 50% of every exam unlocked
- ✓ Learn & Exam modes
- ✓ Distraction-free lessons