Referencing Between Documents

Earn 25 points (50 with Pro) in two steps

  1. ① Read through the lesson — each section gets a ✓ as you scroll through it.
  2. ② When every section has a ✓, tap Complete lesson.

0 of 9 read · keep scrolling

✦ See fewer ads and earn double points — 50 a lesson instead of 25 — 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.


Not read yet

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.


Not read yet

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:

  1. Fetch the product document using prod_phone_99.
  2. Extract category_id and 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.


Not read yet

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.


Not read yet

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.


Not read yet

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.


Not read yet

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.


Not read yet

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.


Not read yet

Summary of Key Takeaways

  1. 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.
  2. 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.
  3. Application-Level Joins: In the absence of SQL joins, your application logic must handle the task of fetching related documents. Use batching (the IN operator) to avoid the N+1 query problem.
  4. 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.
  5. 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.
  6. 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.
  7. 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.

Not read yet

Each section gets a ✓ as you scroll through it. Tap the button to jump to the next one.