Firestore Data Modeling: When to Denormalize for Performance
Stop fighting the lack of JOINs in Firestore. Learn when to duplicate data via denormalization to reduce latency and costs, and how to handle the resulting update complexity.
18 Dec 2025, 01:50 UTC

The Cost of the Missing JOIN
In a traditional SQL database, you normalize your data to avoid redundancy and use JOIN operations to pull related information together. In Google Cloud Firestore, JOIN operations do not exist. If you model your data strictly by normalization—keeping users in one collection and posts in another—your application must perform multiple network requests to display a simple feed: one to get the posts, and one for every unique user who authored those posts.
This "N+1 query problem" increases latency and drives up your Firebase bill, as every single document fetch counts as a read. The solution is denormalization: the intentional duplication of data to optimize for read patterns.
Choosing Between Subcollections and Duplication
When handling one-to-many relationships, you generally have two paths: subcollections or flat denormalization.
- Subcollections: These are collections nested under a document (e.g.,
/users/{userId}/posts/{postId}). This is ideal for data that is strictly owned by a parent and rarely accessed independently. It keeps the parent document size under the 1MB limit. - Denormalization: This involves copying a piece of data from one document into another. For example, instead of just storing a
userIdin a post document, you also store theusernameandprofilePictureUrl.
Worked Example: The Social Feed Pattern
Imagine a social app where you need to display a list of posts, each showing the author's name and avatar. Here is how the data structure shifts from normalized to denormalized.
Normalized (Inefficient for Reads)
// Collection: users
{ uid: "user123", name: "Jane Doe", avatar: "jane.jpg" }
// Collection: posts
{ postId: "p456", authorId: "user123", content: "Hello World!" }
To render this, the client fetches the post, sees user123, and then makes a second request to the users collection.
Denormalized (Optimized for Reads)
// Collection: posts
{
postId: "p456",
authorId: "user123",
authorName: "Jane Doe",
authorAvatar: "jane.jpg",
content: "Hello World!"
}
Now, the client makes one request. The data needed for the UI is contained entirely within the post document.
Managing the Update Trade-off
Denormalization isn't free; it shifts the complexity from the read operation to the write operation. When "Jane Doe" changes her name to "Jane Smith," you no longer update a single user document. You must now update every post she has ever written.
To handle this without blocking the client UI, use a Cloud Function. Trigger the function on the users document update to asynchronously find and update all corresponding posts in the background.
The Contention Limit and Distributed Counters
A critical limitation in Firestore is the document write limit: a single document cannot be updated more than once per second consistently. If you store a likeCount directly on a post document, a viral post will cause write failures as hundreds of users attempt to increment the counter simultaneously.
To solve this, use distributed counters. Create a subcollection called shards under the post. Instead of one counter, create 10 shards. When a user likes a post, randomly pick one of the 10 shards to increment. To get the total count, sum the values of all shards.
Verification and Constraints
To verify your model's efficiency, use the Firebase Emulator Suite. Monitor the logs to count the number of reads triggered by a single page load. If you see a spike in reads that scales linearly with the number of items on screen, you have a normalization problem.
Keep in mind that documents have a hard limit of 1MB. Avoid storing large arrays of IDs (like a list of 10,000 followers) inside a single document; move those to a subcollection to ensure the document remains scalable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.