Chapter 03 · Data Modeling by Access Pattern: Embedding, Referencing, Duplication, Fan-Out, and Denormalization
Fan-Out Writes, Materialized Views, Counters, Feeds, and Precomputed Query Shapes
Build safe fan-out writes, materialized views, counters, feeds, and precomputed query shapes with explicit idempotency, atomicity boundaries, repair paths, and measurable amplification.
Learning outcomes
Some AtlasMart screens need data in a shape that is expensive to assemble on demand: a seller dashboard, a user's activity feed, or a product review count. Firestore can support these experiences by writing derived documents ahead of the read. That shifts work from read time to write time and creates a consistency/repair problem that must be designed explicitly.
Distinguish bounded atomic fan-out from asynchronous/event-driven projection updates.
Design materialized Firestore views with source identity, projection version, and repairability.
Use idempotency keys so retries do not create duplicate feed/dashboard effects.
Choose a single-document counter, distributed counter, or query-time aggregation based on update/read needs rather than folklore.
Measure write amplification and inject a partial-fan-out failure that a reconciliation job can detect and repair.
The lab continues Chapters 01–02 exactly: project
demo-atlasmart-firestore; Firestore emulator
127.0.0.1:8080; Auth emulator
127.0.0.1:9099; Emulator UI
127.0.0.1:4000; Firebase CLI
15.30.0; Firebase JavaScript SDK
12.19.0; Firebase Admin Node.js SDK
14.4.0; Node.js 22 or newer. Standard Native Core
semantics are the default. Enterprise Native Pipeline or
MongoDB-compatibility behavior is mentioned only when the
distinction changes a modeling decision.
The mandatory lab is emulator-first and free/local. Emulator reads and writes are useful for deterministic operation counting and correctness tests, but they are not billable production operations and do not reproduce regional latency, production index topology, contention, autoscaling, or billing. Where a table estimates production operations, it reports document/query/write counts only; convert them to money only after re-checking the current production pricing contract for the exact edition, region, query shape, listener state, and index work.
1. Fan-out moves complexity from reads to writes
A fan-out write updates multiple documents
because one business event must appear in several read-optimized
places. When order o-9003 is created, AtlasMart
might write the authoritative order, append a user feed event,
update a seller dashboard bucket, and mark an idempotency/outbox
record. The benefit is cheap, direct reads later. The cost is
more writes, more failure modes, and a consistency window if the
whole fan-out cannot or should not be one atomic commit.
Use one bounded atomic transaction/batch only when the business semantics require all included Firestore writes to commit together and the operation fits current service limits. Do not freeze an old numeric batch/transaction limit into architecture; re-check current quotas at implementation time. Large or open-ended fan-out belongs in a resumable trusted workflow with progress and idempotency.
2. Materialized views need provenance
A materialized view is a document or collection optimized for a
read pattern and derived from authoritative data. Examples
include
sellers/s-2001/dashboard/daily-2026-09-15 and
users/u-1001/feed/e-order-o-9003. A projection
should normally carry enough provenance to answer: which source
event/version produced it, whether replay is safe, and how to
detect staleness.
{ "projectionType": "seller_daily_orders", "sourceEventId": "order:o-9003:created:v1", "sourceVersion": 1, "schemaVersion": 1, "orderCount": 1, "grossCents": 12990, "lastAppliedAt": "server timestamp at write time"}
Do not let a dashboard document become an unlabeled second source of truth. Checkout/order services should write authoritative order state; dashboards and feeds are rebuildable projections.
3. Idempotency turns retries into safe replay
Network failures and worker retries mean an event may be delivered more than once. A fan-out handler that simply increments totals and adds an auto-ID feed item can double-count on retry. An idempotent design derives stable projection IDs from a business event and records whether that event has already been applied.
const eventId = "order:o-9003:created:v1";const markerRef = db.doc(`projectionEvents/${eventId}`);const feedRef = db.doc("users/u-1001/feed/e-order-o-9003");const dashRef = db.doc("sellers/s-2001/dashboard/daily-2026-09-15");await db.runTransaction(async tx => { if ((await tx.get(markerRef)).exists) return; const dash = await tx.get(dashRef); const oldCount = dash.exists ? dash.get("orderCount") ?? 0 : 0; const oldGross = dash.exists ? dash.get("grossCents") ?? 0 : 0; tx.set(feedRef, { type:"order_created", orderId:"o-9003", sourceEventId:eventId, schemaVersion:1 }); tx.set(dashRef, { orderCount:oldCount + 1, grossCents:oldGross + 12990, schemaVersion:1 }, { merge:true }); tx.create(markerRef, { applied:true, schemaVersion:1 });});
The exact transaction can retry, so it must not send email, charge a card, or invoke an irreversible external side effect inside the callback. Those external actions need their own durable idempotency/outbox strategy. For high contention dashboards, a single aggregate document may also become a bottleneck; choose a different aggregation mechanism rather than assuming one document can absorb unlimited updates.
4. Counters: choose the consistency/update shape deliberately
A single document counter is simple and strongly readable after the committed update, but sufficiently frequent concurrent updates can contend on that document. Firestore's documented distributed-counter pattern splits updates across shard documents and sums them when reading; throughput can increase with shard count, but read complexity and staleness/aggregation strategy also change. Read-time aggregation queries and materialized aggregates are other options and are covered in depth in Chapter 16.
| Need | Candidate | Main tradeoff |
|---|---|---|
| Low/moderate write rate, immediate simple value | Single materialized counter | Potential contention at higher concurrency. |
| Higher write concurrency | Distributed/sharded counter | More documents and read/aggregation complexity. |
| Occasional count/sum/avg from indexed data | Read-time aggregation query | Query-time work/cost; not a realtime materialized field. |
| Dashboard with custom dimensions | Materialized view | Write amplification, lag, replay/reconciliation. |
5. Hands-on lab: inject a partial fan-out failure and repair it
Use the same local workspace and pinned package baseline from
Chapter 02. The examples assume
firebase emulators:start --only auth,firestore is
running and that Admin SDK code points at
FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 with
GCLOUD_PROJECT=demo-atlasmart-firestore. Browser
examples connect the modular Web SDK to the Firestore/Auth
emulators. Keep the fixtures synthetic and delete only the
Chapter 03 paths you create.
await db.doc("orders/o-9003").set({ userId:"u-1001", sellerId:"s-2001", totalCents:12990, status:"placed", schemaVersion:3, createdAt:new Date("2026-09-15T11:00:00Z")});await db.doc("users/u-1001/feed/e-order-o-9003").set({ type:"order_created", orderId:"o-9003", sourceEventId:"order:o-9003:created:v1", schemaVersion:1});// Failure injection: do NOT write the seller dashboard projection or marker yet.
Write a reconciliation script that scans the small deterministic order fixture set, derives the expected feed/dashboard IDs, and reports missing projections. The first run must report the dashboard/marker as missing. Apply the idempotent projection function, run it twice, then reconcile again. The second and third runs must not double-count.
const orderSnap = await db.doc("orders/o-9003").get();const expected = [ "users/u-1001/feed/e-order-o-9003", "sellers/s-2001/dashboard/daily-2026-09-15", "projectionEvents/order:o-9003:created:v1"];for (const path of expected) { const snap = await db.doc(path).get(); console.log(path, snap.exists ? "present" : "MISSING");}
Record logical amplification: one authoritative order event causes the source write plus each projection/marker write. If you later move this to Cloud Functions/Eventarc or another managed worker, re-check delivery guarantees, retries, region, billing, and trigger semantics on that implementation date. The mandatory lesson does not require a paid deployment.
Verification checklist
- Partial fan-out creates a detectable missing projection.
- Retrying the projection handler does not duplicate feed entries or dashboard totals.
- Materialized documents identify source event/version/schema.
- External side effects are excluded from retryable Firestore transaction callbacks.
- Write amplification is recorded explicitly.
6. Deliberately wrong approach: "Firestore scales, so denormalization is free"
Automatic scaling does not remove write amplification, index fan-out, hot-document contention, retry cost, or operational repair. A feed fan-out to thousands of recipients is still thousands of logical writes. A single seller dashboard document updated by every order can still become a contention point. A large duplicated field can still multiply index/storage work.
The repair is to bound fan-out, split hot aggregates when needed, exempt fields from indexes when they are not queried and current guidance supports it, use resumable workers for large projection sets, and retain a reconciliation path. Scale is an architectural capability, not permission to ignore workload shape.
Knowledge check
Check your understanding
- What is the main benefit and main cost of fan-out writes?
- Why should a materialized view carry source-event/version metadata?
- How does a stable projection ID improve retry behavior?
- Why should external side effects stay out of a retryable transaction callback?
- When might a distributed counter be preferable to one counter document?
Review the answers
1. Fan-out precomputes read shapes so reads become direct; it adds write amplification, partial-failure/idempotency concerns, and repair work.
2. It makes staleness/replay/reconciliation observable and prevents the projection from becoming an undocumented second source of truth.
3. Replaying the same business event targets the same document/marker, making duplicate effects detectable or avoidable.
4. The transaction function may execute multiple times; irreversible external calls could be duplicated even if Firestore eventually commits once.
5. When update contention on a single counter document becomes material and the extra shard/read aggregation complexity is acceptable.
Summary and next step
Materialized Firestore views can make AtlasMart screens fast and queryable, but every saved read becomes a write/replay/reconciliation obligation somewhere else. Idempotent event IDs, source provenance, bounded atomicity, and repair tools turn denormalization from ad-hoc duplication into an operationally defensible design. The final lesson applies all of these choices to a relational-to-Firestore refactor.
Next: Refactor a Relational Model into Firestore and Document Every Consistency and Cost Tradeoff.
Authoritative references
- Choose a data structure — Official guidance for nested data, subcollections, and root-level collections.
- Cloud Firestore data model — Documents, collections, subcollections, paths, and parent-deletion behavior.
- Perform simple and compound queries — Collection and collection-group query semantics.
- Securely query data — Security Rules and query constraints; rules are not filters.
- Transactions and batched writes — Atomicity and retry boundaries used by bounded fan-out workflows.
- Distributed counters — Official sharded-counter pattern for higher update rates.
- Best practices for Cloud Firestore — IDs, hotspotting, index fan-out, location, and write-shape guidance.
- Delete data — Parent/subcollection lifecycle and bulk-delete considerations.
- Cloud Firestore pricing — Living production billing contract; re-check before converting operation counts into currency.
- Firebase release notes — Current Firebase SDK and CLI version baseline.
- Connect to the Firestore Emulator — Local development path and production-difference guidance.