Chapter 20 · Materialized Views and Derived-Table Alternatives

CDC / Streams / Application Dual Writes vs Materialized Views for Derived Read Models

Compare raw Cassandra CDC, external stream/application projection workflows, direct dual writes, and materialized views by replay, transformation, lag, and ownership.

Intermediate → Advanced120–165 minutesCDC/event projection labApache Cassandra 5.0.9 · experimental MV isolated cluster · Java 17 image · Java Driver 4.19.3 optionalLast reviewed: September 2026

Learning outcomes

AtlasMart now wants one order change to feed a customer dashboard, fraud pipeline, warehouse export, and notification service. A materialized view can only maintain a constrained Cassandra table. The requirement has become an event-distribution problem, so this lesson compares raw Cassandra CDC, an external stream/consumer, and direct application dual writes.

01

Explain Cassandra CDC as durable commit-log exposure that requires a consumer, checkpointing, disk-space management, and replay ownership.

02

Compare CDC/stream projection, direct application dual writes, and materialized views by transformation power and failure/recovery model.

03

Observe local CDC files/index offsets without requiring a paid stream or proprietary connector.

04

Use an application event/request table to make projection completion/reconciliation explicit.

05

Demonstrate why dual writes without an outbox/request/reconciliation contract eventually diverge.

Chapter 20 isolated lab baseline

Apache Cassandra 5.0 materialized views are experimental, disabled by default, and not recommended by the Apache Cassandra documentation for production use. Therefore this chapter does not alter the shared atlasmart-course cluster. It builds a separate disposable image atlasmart-cassandra-derived:5.0.9 from the pinned Docker Official Image cassandra:5.0.9 and enables materialized_views_enabled: true. The same isolated image enables cdc_enabled: true only so Lesson 4 can observe Change Data Capture (CDC) files locally; CDC remains off in the earlier shared course cluster. The lab cluster is atlasmart-derived-course on Docker network atlasmart-derived-net, nodes atlasmart-derived-cass-1..3, datacenter dc1, racks rack1..rack3, 16 virtual nodes per node, and named disposable data volumes. The keyspace atlasmart_derived uses NetworkTopologyStrategy with replication factor (RF) 3; ordinary lab reads/writes use LOCAL_QUORUM. New tables/views explicitly use UnifiedCompactionStrategy (UCS), no default time-to-live (TTL), and Cassandra's normal gc_grace_seconds unless stated otherwise. Authentication, client/internode Transport Layer Security (TLS), and remote Java Management Extensions (JMX) are disabled only on this private single-host lab network. Apache Cassandra Java Driver 4.19.3 is optional for projection/reconciliation examples. Exact build timing, view propagation timing, repair behavior, CDC filenames, latency, and failure outcomes are learner-captured evidence—not canned benchmark results.

Execution and safety note

Run commands only against the disposable Apache Cassandra course lab or another explicitly approved non-production environment. Confirm node, keyspace, table, container, volume, path, and datacenter targets before destructive, failure-injection, cleanup, repair, restore, security, or topology operations. Capture current state and expected rollback/recovery evidence first; output and timings can differ by host, operating system, Java runtime, Docker/runtime, driver, and Cassandra configuration.

Terms and mental model

A base table is the Cassandra table the application writes directly. A materialized view (MV) is an automatically maintained Cassandra table derived from one base table with a different primary-key layout; applications cannot mutate the view directly. In Cassandra 5.0 this feature is explicitly experimental. A derived table is a broader design idea: any table whose rows are produced from authoritative data for another read pattern. An explicit projection or query table is a derived table owned by application/stream/reconciliation logic rather than Cassandra's MV mechanism. Storage-Attached Indexing (SAI) is Cassandra 5.0's storage-integrated secondary indexing framework and is a different way to support non-primary-key predicates. Change Data Capture (CDC) exposes durable portions of commit-log segments for an external consumer; Cassandra does not turn those raw records into a projection for you.

A primary key determines Cassandra partition placement and clustering order. The partition key is hashed to a token; replicas store token ranges according to the keyspace replication strategy. A request-scoped coordinator routes work to replicas and waits for the chosen consistency level (CL). A view replica stores a materialized-view partition, whose partition key can hash to a different token than the corresponding base row. Batchlog is Cassandra's durable coordination mechanism for certain multi-mutation operations and is used internally by MV maintenance; it is not a relational transaction log. Hints, repair, and streaming are separate convergence mechanisms. An immutable SSTable (Sorted String Table) stores flushed table/view data on disk and is managed by compaction.

1. CDC is a source of durable change records—not a projection engine

Node-wide cdc_enabled is already true only in the isolated custom image, but a table participates only after cdc=true. Cassandra exposes durable portions of commit-log segments in cdc_raw_directory with companion _cdc.idx offsets. A consumer must decode records, checkpoint progress, handle duplicates/restarts, and delete/archive processed segments. If CDC space fills while the default blocking behavior is active, writes to CDC-enabled tables can be rejected. That operational backpressure is part of the feature contract.

bash / WSL · start the isolated three-node derived-data lab
docker network inspect atlasmart-derived-net >/dev/null 2>&1 || docker network create atlasmart-derived-netdocker volume create atlasmart-derived-cass-1-datadocker volume create atlasmart-derived-cass-2-datadocker volume create atlasmart-derived-cass-3-datadocker inspect atlasmart-derived-cass-1 >/dev/null 2>&1 || docker run -d --name atlasmart-derived-cass-1 --hostname atlasmart-derived-cass-1 --network atlasmart-derived-net -e CASSANDRA_CLUSTER_NAME=atlasmart-derived-course -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack1 -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch -e CASSANDRA_NUM_TOKENS=16 -v atlasmart-derived-cass-1-data:/var/lib/cassandra atlasmart-cassandra-derived:5.0.9# Wait for node 1 to become UN before starting peers.docker exec atlasmart-derived-cass-1 nodetool statusdocker inspect atlasmart-derived-cass-2 >/dev/null 2>&1 || docker run -d --name atlasmart-derived-cass-2 --hostname atlasmart-derived-cass-2 --network atlasmart-derived-net -e CASSANDRA_CLUSTER_NAME=atlasmart-derived-course -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack2 -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch -e CASSANDRA_NUM_TOKENS=16 -e CASSANDRA_SEEDS=atlasmart-derived-cass-1 -v atlasmart-derived-cass-2-data:/var/lib/cassandra atlasmart-cassandra-derived:5.0.9docker inspect atlasmart-derived-cass-3 >/dev/null 2>&1 || docker run -d --name atlasmart-derived-cass-3 --hostname atlasmart-derived-cass-3 --network atlasmart-derived-net -e CASSANDRA_CLUSTER_NAME=atlasmart-derived-course -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack3 -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch -e CASSANDRA_NUM_TOKENS=16 -e CASSANDRA_SEEDS=atlasmart-derived-cass-1 -v atlasmart-derived-cass-3-data:/var/lib/cassandra atlasmart-cassandra-derived:5.0.9# Continue only when all nodes are UN in dc1.docker exec atlasmart-derived-cass-1 nodetool versiondocker exec atlasmart-derived-cass-1 java -versiondocker exec atlasmart-derived-cass-1 nodetool statusdocker exec atlasmart-derived-cass-1 sh -lc "grep -E '^(materialized_views_enabled|cdc_enabled|materialized_views_on_repair_enabled):' /etc/cassandra/cassandra.yaml || true"
CQL · create the AtlasMart base/derived schema
CREATE KEYSPACE IF NOT EXISTS atlasmart_derivedWITH replication = {'class':'NetworkTopologyStrategy','dc1':3};CREATE TABLE IF NOT EXISTS atlasmart_derived.orders_by_day (    tenant_id text,    order_day date,    order_id uuid,    customer_id text,    created_at timestamp,    status text,    total decimal,    shipping_city text,    note text,    updated_at timestamp,    PRIMARY KEY ((tenant_id,order_day),order_id)) WITH compaction = {'class':'UnifiedCompactionStrategy'}  AND cdc = false;CONSISTENCY LOCAL_QUORUM;CREATE MATERIALIZED VIEW IF NOT EXISTS atlasmart_derived.orders_by_customer_mv ASSELECT tenant_id,order_day,order_id,customer_id,created_at,status,total,shipping_cityFROM atlasmart_derived.orders_by_dayWHERE tenant_id IS NOT NULL  AND order_day IS NOT NULL  AND order_id IS NOT NULL  AND customer_id IS NOT NULLPRIMARY KEY ((tenant_id,customer_id,order_day),order_id)WITH compaction = {'class':'UnifiedCompactionStrategy'};CREATE TABLE IF NOT EXISTS atlasmart_derived.orders_by_customer_projection (    tenant_id text,    customer_id text,    order_month date,    created_at timestamp,    order_id uuid,    status text,    total decimal,    shipping_city text,    source_updated_at timestamp,    projection_request_id uuid,    PRIMARY KEY ((tenant_id,customer_id,order_month),created_at,order_id)) WITH CLUSTERING ORDER BY (created_at DESC,order_id ASC)  AND compaction = {'class':'UnifiedCompactionStrategy'};
CQL · enable CDC only on the AtlasMart base table
ALTER TABLE atlasmart_derived.orders_by_day WITH cdc = true;DESCRIBE TABLE atlasmart_derived.orders_by_day;CONSISTENCY LOCAL_QUORUM;INSERT INTO atlasmart_derived.orders_by_day(tenant_id,order_day,order_id,customer_id,created_at,status,total,shipping_city,note,updated_at)VALUES ('tenant-a','2026-09-08',20000000-0000-0000-0000-000000000020,'cust-77','2026-09-08T10:30:00Z','PAID',44.00,'Baku','cdc-demo','2026-09-08T10:30:00Z');UPDATE atlasmart_derived.orders_by_day SET status='PACKING'WHERE tenant_id='tenant-a' AND order_day='2026-09-08'  AND order_id=20000000-0000-0000-0000-000000000020;
bash · observe CDC raw files safely
# Flush can help old commit-log segments become recyclable; exact CDC file timing is version/workload dependent.for n in 1 2 3; do docker exec atlasmart-derived-cass-$n nodetool flush atlasmart_derived orders_by_day; donefor n in 1 2 3; do  echo "=== node $n cdc_raw ==="  docker exec atlasmart-derived-cass-$n sh -lc "ls -lah /var/lib/cassandra/cdc_raw | tail -20"  docker exec atlasmart-derived-cass-$n sh -lc "for f in /var/lib/cassandra/cdc_raw/*_cdc.idx; do [ -f \"$f\" ] && echo ---\ $f && cat \"$f\"; done"done

An index offset/word such as COMPLETED tells a consumer how far the corresponding commit-log segment is safe to parse. It does not tell you the business projection was applied. Parsing binary commit-log records is deliberately not hidden in a one-line shell command; production consumers use Cassandra's commit-log reader APIs or a maintained integration and own schema/version compatibility.

2. Build an explicit event/request contract

CQL · application-visible event/checkpoint tables
CREATE TABLE IF NOT EXISTS atlasmart_derived.projection_events_by_request (    tenant_id text,    request_id uuid,    order_day date,    order_id uuid,    event_type text,    source_updated_at timestamp,    projection_state text,    last_error text,    PRIMARY KEY ((tenant_id),request_id)) WITH compaction = {'class':'UnifiedCompactionStrategy'};INSERT INTO atlasmart_derived.projection_events_by_request(tenant_id,request_id,order_day,order_id,event_type,source_updated_at,projection_state,last_error)VALUES ('tenant-a',40000000-0000-0000-0000-000000000020,'2026-09-08',20000000-0000-0000-0000-000000000020,'ORDER_STATUS_CHANGED','2026-09-08T10:31:00Z','PENDING',null);SELECT * FROM atlasmart_derived.projection_events_by_requestWHERE tenant_id='tenant-a';

This table is a teaching checkpoint, not a claim that Cassandra provides a transactional outbox across unrelated partitions. If the source mutation and event record must be atomic, redesign them into a single partition where appropriate, use a carefully bounded logged batch after measuring its tradeoffs, or choose infrastructure with the transaction semantics the business invariant truly requires. The important point is explicit ownership of “pending/applied/failed” rather than invisible best effort.

3. Dual write failure and reconciliation

CQL · simulate source success + skipped projection
UPDATE atlasmart_derived.orders_by_day SET status='SHIPPED'WHERE tenant_id='tenant-a' AND order_day='2026-09-08'  AND order_id=20000000-0000-0000-0000-000000000020;-- Intentionally DO NOT write orders_by_customer_projection yet.UPDATE atlasmart_derived.projection_events_by_requestSET projection_state='PENDING', last_error='simulated worker crash before projection'WHERE tenant_id='tenant-a'  AND request_id=40000000-0000-0000-0000-000000000020;
CQL · worker/reconciler applies deterministic projection and marks completion
INSERT INTO atlasmart_derived.orders_by_customer_projection(tenant_id,customer_id,order_month,created_at,order_id,status,total,shipping_city,source_updated_at,projection_request_id)VALUES ('tenant-a','cust-77','2026-09-01','2026-09-08T10:30:00Z',20000000-0000-0000-0000-000000000020,'SHIPPED',44.00,'Baku','2026-09-08T10:31:00Z',40000000-0000-0000-0000-000000000020);UPDATE atlasmart_derived.projection_events_by_requestSET projection_state='COMPLETE', last_error=nullWHERE tenant_id='tenant-a'  AND request_id=40000000-0000-0000-0000-000000000020;SELECT * FROM atlasmart_derived.projection_events_by_requestWHERE tenant_id='tenant-a';
Wrong approach: “Write the base, then write three projections; if one fails, log the error and move on.”

That is unbounded divergence. A viable dual-write/event design needs deterministic IDs, retry classification, checkpoints, dead-letter/poison handling, backfill, lag metrics, reconciliation, and an owner. Materialized views move some of that mechanism into Cassandra but do not supply arbitrary transforms or side effects.

4. Ownership matrix: MV vs CDC/stream vs application dual write

Dimension Materialized view CDC / external stream Direct application projection
Transformation restricted MV SELECT/key rules arbitrary consumer logic arbitrary application logic
Replay/backfill view build / Cassandra operations consumer offsets + source/backfill plan application backfill/reconciler
External side effects not purpose-built natural consumer use case possible but must deduplicate
Failure visibility Cassandra metrics/build/repair evidence lag/offset/dead-letter/consumer metrics request/projection state + app metrics
Backpressure Cassandra write/MV cost CDC disk/stream/consumer backlog application queue/thread/DB pressure
Feature status experimental in C* 5.0 CDC supported but operationally explicit ordinary tables + app logic

Kafka, Pulsar, managed CDC connectors, and commercial streaming platforms can be useful, but none is required for this lesson. The free/local path is raw Cassandra CDC evidence plus an explicit projection-event simulation. If a managed service does not expose raw CDC or materialized views, its documented connector/change-stream interface becomes a different product contract.

5. Verification and cleanup of CDC state

  • cdc_enabled=true is isolated to the Chapter 20 image.
  • orders_by_day explicitly shows cdc=true.
  • CDC raw/index files are observed without modifying live segments.
  • The simulated projection crash leaves a visible PENDING event.
  • Reconciliation produces the correct projection and marks the request COMPLETE.

Check your understanding

  1. Does enabling CDC automatically create a derived query table?
  2. Why can CDC backpressure affect database writes?
  3. What advantage does a stream/consumer have over an MV?
  4. Is an event/checkpoint table automatically a transactional outbox?
  5. Why are deterministic request IDs important?
Review the answers

1. No. CDC exposes durable change records; a consumer must decode, checkpoint, transform, write, and reconcile derived state.

2. CDC consumes bounded disk space; with blocking enabled, writes to CDC-enabled tables can be rejected when the configured space is exhausted.

3. Arbitrary transformations, multiple consumers/side effects, replay/checkpoint semantics, and independent deployment—at the cost of more operational ownership.

4. No. Atomicity across source/event partitions must be designed explicitly.

5. They let retries/consumers deduplicate work and prove whether a projection attempt is pending, complete, or needs reconciliation.

Production judgment

Do not choose a derived-data mechanism because it removes code from one layer. Evaluate who owns correctness, replay, backfill, deletion semantics, schema evolution, failure recovery, repair, observability, and rollback. For Cassandra materialized views, start with the documented fact that the feature is experimental and disabled by default. If an organization still evaluates it, test base/view key shape, update/delete mix, view build duration, write amplification, p95/p99 base-write latency, view-query latency/freshness, replica failures, streaming/repair behavior, disk headroom, compaction/tombstones, and upgrade behavior under the exact Cassandra patch. For explicit query tables or CDC/application projections, budget application/stream complexity, idempotency, checkpointing, retries, poison-event handling, backfill, reconciliation lag, and ownership. For SAI, measure selectivity/fanout, index build/disk cost, query limits, and failure behavior.

Replication factor and consistency level protect Cassandra replica operations but do not turn a derived-data pipeline into a global transaction. Security and tenant boundaries must be part of the query/table/index design; a local secondary access path is not authorization. Driver retry and speculative execution require idempotency analysis. Managed Cassandra services may disable, alter, or omit materialized views/CDC and may expose different repair/backup controls, so verify the actual service contract rather than assuming Apache Cassandra behavior. Keep migration reversible: dual-read comparison, backfill/reconciliation, explicit cutover criteria, and the ability to drop or stop the derived mechanism without losing the authoritative base data. Lesson 5 turns the chapter into a decision framework: base table, SAI, experimental MV, or explicit projection/CDC are selected from workload and ownership requirements rather than convenience.

Summary and next bridge

CDC and stream/application projections expose more moving parts than a materialized view, but they also expose ownership, replay, transformations, and side effects. The final lesson chooses among base tables, SAI, MV, and explicit projections using concrete AtlasMart requirements and operational acceptance criteria.

Authoritative references

Materialized-view behavior is version-sensitive and the feature remains experimental in Cassandra 5.0. Re-check these sources before promoting any example beyond a disposable lab.

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.