No history yet

Advanced Firestore Modeling

Structuring Data for Scale

When you first start with Firestore, it feels simple. You have collections, which hold documents, which hold data. But building a production-ready application requires thinking beyond basic storage. The goal is to design a structure that's not just logical, but also fast, cost-effective, and scalable.

Unlike relational databases where normalization is king, Firestore often rewards denormalization—the practice of storing duplicate data to optimize for frequent read operations. This is because Firestore's power comes from its shallow queries. It can fetch a single document or a group of documents from a collection incredibly quickly, but it can't perform server-side joins between collections. Your data structure must reflect how your app will actually access the data.

Collections vs. Subcollections

Your first major architectural decision is how to group your documents. You can place them in a root-level collection (e.g., /users) or nest them in a subcollection under another document (e.g., /users/{userId}/posts).

A root collection is straightforward. It's a top-level container for a single type of data. A subcollection, on the other hand, is tied to a specific parent document. This creates a logical hierarchy. For instance, a user's posts are naturally associated with that user. Placing them in a subcollection makes that relationship explicit in the data path itself.

FeatureRoot CollectionSubcollection
Query ScopeCan query across all documents in the collection.Queries are scoped to the parent document. Cannot query across all subcollections of the same name with a single query.
Data DeletionDeleting a document is a single operation.Deleting a parent document does not delete its subcollections. This requires a background process.
PerformanceGreat for fetching lists of unrelated items.Great for fetching data related to a specific parent item, like a user's profile and their first 10 posts.
SecurityRules are generally simpler.Allows for more granular, hierarchical security rules. (e.g., only a user can write to their own posts subcollection).

A common trap is the automatic deletion assumption. If a user deletes their account (/users/{userId}), their posts in the /users/{userId}/posts subcollection will remain. They become orphaned data. You must use a to listen for the parent document's deletion and then programmatically delete all documents in the subcollection. This is a critical consideration for maintaining data integrity.

Modeling Complex Relationships

Sooner or later, you'll need to model a many-to-many relationship. A classic example is a social media feed where users can "follow" many other users, and can also be "followed by" many users.

Let's say we want to build an activity feed for a user, showing the latest posts from everyone they follow. How do we model this efficiently?

The goal: Fetch the 20 most recent posts from all followed users in a single, efficient query.

A common approach is to store a list of who each user follows. We could have a following array inside each user document in our users collection. When a user opens their feed, the app would first read this array, and then fire off a separate query for each person they follow to get their posts. This is a terrible idea. If a user follows 50 people, that's 51 reads just to load one screen.

A much better way is to denormalize. When a user creates a new post, we can use a Cloud Function to copy that post's ID (or the full post data) into a special feed subcollection for each of their followers.

This pattern is called "fan-out on write." It means we do more work when data is created (writing the post to multiple feeds) to make reading the data extremely simple and fast. Now, loading a user's feed is just a single query to their personal feed subcollection, ordered by timestamp. This scales beautifully for reads, though it adds complexity to writes.

This also helps us deal with Firestore's 1 MiB document size limit. Storing an unbounded array of posts or followers in a single document is risky. Sooner or later, a power user will hit that limit. By using subcollections, each feed item is its own small document, so the total amount of feed data can grow infinitely.

Counters at Scale

What about something as simple as a "like" count on a post? The naive approach is to store a likeCount field on the post document and increment it every time someone clicks the like button. This works fine until your post goes viral.

Firestore limits a single document to about one write per second. If hundreds of people are liking a post at the same time, most of those updates will fail due to contention. The solution is to use s.

Instead of one counter, you create a subcollection (e.g., /posts/{postId}/shards) with a small number of documents, perhaps 10, called shards. When a user likes a post, your code picks a random shard and increments its count using an atomic increment operation. To get the total like count, you fetch all 10 shard documents and sum their values on the client side.

// Client-side code to increment a random shard
import { doc, collection, increment, writeBatch } from "firebase/firestore";

async function addLike(db, postId) {
  const shardId = Math.floor(Math.random() * 10).toString();
  const shardRef = doc(db, `posts/${postId}/shards`, shardId);

  // Use a transaction or batch to update the shard
  // increment() is an atomic operation
  const batch = writeBatch(db);
  batch.set(shardRef, { count: increment(1) }, { merge: true });
  await batch.commit();
}

This distributes the write load across multiple documents, easily handling thousands of likes per minute. It's another example of designing your data structure to work with Firestore's strengths, not against them.

These advanced techniques form the foundation of scalable Firestore applications. By understanding the trade-offs between different modeling patterns, you can build systems that remain fast and cost-effective as they grow.

Quiz Questions 1/5

Why is denormalization a common and recommended practice when designing a Firestore data structure?

Quiz Questions 2/5

If you delete a document in Firestore, all documents within its subcollections are automatically deleted as well.