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.

Beginner → Advanced100–130 minutesAtlasMart emulator-first modeling labFirebase CLI 15.30.0 · Web SDK 12.19.0 · Admin Node 14.4.0 · Node.js 22+Firestore Standard Native Core semantics unless explicitly labeled EnterpriseLast reviewed: September 2026

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.

01

Distinguish bounded atomic fan-out from asynchronous/event-driven projection updates.

02

Design materialized Firestore views with source identity, projection version, and repairability.

03

Use idempotency keys so retries do not create duplicate feed/dashboard effects.

04

Choose a single-document counter, distributed counter, or query-time aggregation based on update/read needs rather than folklore.

05

Measure write amplification and inject a partial-fan-out failure that a reconciliation job can detect and repair.

Chapter 03 baseline reviewed 15 September 2026

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.

Execution, pricing, and evidence note

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.

projection contract · fields that make repair possible
{  "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.

Node.js · bounded idempotent projection transaction
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.

seed authoritative order and intentionally incomplete projections
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.

reconcile · compare authoritative events with projection IDs
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

  1. What is the main benefit and main cost of fan-out writes?
  2. Why should a materialized view carry source-event/version metadata?
  3. How does a stable projection ID improve retry behavior?
  4. Why should external side effects stay out of a retryable transaction callback?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.