Chapter 08 · CQL Data Types, Collections, Tuples, UDTs, Static Columns, and Frozen Values

Frozen vs Multi-Cell Values, Nested Collections, and Mutation Granularity

Distinguish frozen single-cell values from multi-cell collections and UDTs so mutation granularity, tombstones, nesting rules, and read/write cost are predictable.

Intermediate90–120 minutesFrozen/multi-cell mutation labApache Cassandra 5.0.9 · cqlsh/nodetool · Java Driver 4.19.3 optional · UCSLast reviewed: September 2026

Learning outcomes

AtlasMart has two kinds of structured state: settings that change one entry at a time and snapshots that must move as a coherent unit. Cassandra's frozen modifier is the line between those mutation models. Treating it as syntax trivia produces rejected CQL, unnecessary whole-value rewrites, or unexpected tombstone patterns.

01

Explain multi-cell versus frozen single-cell representation at the CQL mutation boundary.

02

Mutate one element of a non-frozen map and prove that the same operation is invalid for a frozen map.

03

Use frozen inner values where nested collection rules require them.

04

Compare non-frozen and frozen UDT update semantics with the same logical address data.

05

Choose mutation granularity from business invariants, cardinality, tombstone rate, and retry behavior.

Chapter 08 lab baseline

The mandatory labs use the pinned cassandra:5.0.9 image. Java 17, cqlsh, and nodetool are the versions bundled by that image. The course topology is three disposable nodes (atlasmart-cass-1..3) in cluster atlasmart-course, datacenter dc1, racks rack1..rack3, 16 vnodes per node, replication factor (RF) 3, and LOCAL_QUORUM for consistency-sensitive examples. Authentication, client TLS, internode TLS, and remote JMX are not enabled in this isolated learning network; production must secure those boundaries separately. Chapter 08 uses keyspace atlasmart_types; new tables explicitly use UnifiedCompactionStrategy (UCS), default table TTL is zero unless stated, and gc_grace_seconds is not changed. Storage-Attached Indexing (SAI) and vector search are not required. Apache Cassandra Java Driver 4.19.3 is used only in optional decoding snippets; the mandatory path remains free/local with cqlsh.

Execution disclosure and resource path

The commands and CQL below are documentation- and syntax-reviewed but were not executed in this generation environment. Treat output as an expected shape, then capture exact UUIDs, time values, SSTable paths, tombstone counters, and driver-decoded values on your machine. If three nodes are too heavy, use one disposable node and RF=1 to learn type/mutation semantics, but do not treat that reduced topology as evidence about RF=3 availability or repair behavior.

1. Frozen means one value at the mutation boundary

A non-frozen collection is multi-cell: map keys/set elements/list elements are stored as independently addressable cells. That enables element mutations but also means element deletion/expiry can create element tombstones. A frozen<map<...>> is serialized as one value. Cassandra does not let you update only one key inside it; replace the entire map. Tuples are inherently frozen. UDTs may be multi-cell when their definition permits, or explicitly frozen for snapshot semantics.

2. Lab: compare the same logical data in frozen and multi-cell columns

bash · verify or recreate the disposable course cluster
# Verify the shared course lab if it already exists.docker exec atlasmart-cass-1 nodetool versiondocker exec atlasmart-cass-1 nodetool status# Standalone local recreation path. Skip resources that already exist.docker network inspect atlasmart-cassandra >/dev/null 2>&1 || docker network create atlasmart-cassandradocker volume create atlasmart-cass-1-datadocker volume create atlasmart-cass-2-datadocker volume create atlasmart-cass-3-datadocker inspect atlasmart-cass-1 >/dev/null 2>&1 || docker run -d --name atlasmart-cass-1 --hostname atlasmart-cass-1 --network atlasmart-cassandra -e CASSANDRA_CLUSTER_NAME=atlasmart-course -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack1 -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch -e CASSANDRA_NUM_TOKENS=16 -v atlasmart-cass-1-data:/var/lib/cassandra cassandra:5.0.9# Wait for node 1 to answer before starting peers.docker exec atlasmart-cass-1 nodetool statusdocker inspect atlasmart-cass-2 >/dev/null 2>&1 || docker run -d --name atlasmart-cass-2 --hostname atlasmart-cass-2 --network atlasmart-cassandra -e CASSANDRA_CLUSTER_NAME=atlasmart-course -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack2 -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch -e CASSANDRA_NUM_TOKENS=16 -e CASSANDRA_SEEDS=atlasmart-cass-1 -v atlasmart-cass-2-data:/var/lib/cassandra cassandra:5.0.9docker inspect atlasmart-cass-3 >/dev/null 2>&1 || docker run -d --name atlasmart-cass-3 --hostname atlasmart-cass-3 --network atlasmart-cassandra -e CASSANDRA_CLUSTER_NAME=atlasmart-course -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack3 -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch -e CASSANDRA_NUM_TOKENS=16 -e CASSANDRA_SEEDS=atlasmart-cass-1 -v atlasmart-cass-3-data:/var/lib/cassandra cassandra:5.0.9# Continue only after all nodes show UN.docker exec atlasmart-cass-1 nodetool statusdocker exec atlasmart-cass-1 cqlsh -e "CREATE KEYSPACE IF NOT EXISTS atlasmart_types WITH replication = {'class':'NetworkTopologyStrategy','dc1':3};"docker exec atlasmart-cass-1 cqlsh -e "DESCRIBE KEYSPACE atlasmart_types"
sql · create live and snapshot representations
CREATE TYPE atlasmart_types.contact_v1 (    city text,    country text);CREATE TABLE atlasmart_types.profile_shapes (    customer_id uuid PRIMARY KEY,    live_preferences map<text,text>,    preferences_snapshot frozen<map<text,text>>,    live_contact atlasmart_types.contact_v1,    contact_snapshot frozen<atlasmart_types.contact_v1>,    channel_groups map<text,frozen<list<text>>>) WITH compaction = {'class':'UnifiedCompactionStrategy'};INSERT INTO atlasmart_types.profile_shapes(customer_id,live_preferences,preferences_snapshot,live_contact,contact_snapshot,channel_groups)VALUES(11111111-1111-1111-1111-111111111111, {'currency':'USD','locale':'en-US'}, {'currency':'USD','locale':'en-US'}, {city:'Tehran',country:'IR'}, {city:'Tehran',country:'IR'}, {'urgent':['sms','push'],'normal':['email']});SELECT * FROM atlasmart_types.profile_shapes;
sql · mutate multi-cell values and replace frozen snapshots
UPDATE atlasmart_types.profile_shapesSET live_preferences['currency']='AZN',    live_contact.city='Baku'WHERE customer_id=11111111-1111-1111-1111-111111111111;UPDATE atlasmart_types.profile_shapesSET preferences_snapshot={'currency':'AZN','locale':'en-US'},    contact_snapshot={city:'Baku',country:'AZ'}WHERE customer_id=11111111-1111-1111-1111-111111111111;SELECT * FROM atlasmart_types.profile_shapesWHERE customer_id=11111111-1111-1111-1111-111111111111;

The live columns allow targeted field/element changes. The frozen columns require whole-value replacement, which is appropriate when AtlasMart wants an immutable-ish snapshot and the value remains small.

3. Deliberately trigger the two common mistakes

sql · expected failures: frozen element mutation and unfrozen nesting
-- Intentionally invalid: a frozen map has no independently mutable entry.UPDATE atlasmart_types.profile_shapesSET preferences_snapshot['currency']='EUR'WHERE customer_id=11111111-1111-1111-1111-111111111111;-- Intentionally invalid schema: nested collection value needs a frozen boundary.CREATE TABLE atlasmart_types.bad_nested (    id uuid PRIMARY KEY,    groups map<text,list<text>>);

The exact error wording can vary by patch, but both requests should be rejected. The corrected nested form in the lab uses map<text,frozen<list<text>>>. Do not respond to the error by freezing everything automatically: freezing changes update cost and conflict/retry behavior because the whole value becomes the write unit.

Wrong approach: “frozen is faster, so freeze every complex type.”

That erases element-level mutation and can turn a tiny setting change into a whole-value read/construct/write in the application. The opposite extreme—multi-cell values with high churn—can create many element tombstones. Choose the boundary from actual update patterns.

4. Observe storage consequences cautiously

bash · flush and inspect table-level evidence
docker exec atlasmart-cass-1 nodetool flush atlasmart_types profile_shapesdocker exec atlasmart-cass-1 nodetool tablestats atlasmart_types profile_shapes# Optional SSTable forensic view; output format/path is version dependent.docker exec atlasmart-cass-1 bash -lc 'F=$(find /var/lib/cassandra/data/atlasmart_types/profile_shapes-* -name "*Data.db" | head -1); echo "$F"; test -n "$F" && sstabledump "$F" | head -120 || true'

This experiment is qualitative. Do not infer production write amplification or compaction efficiency from one tiny fixture. Measure with your real value sizes, update frequency, TTL/delete rate, concurrency, RF/CL, disk, and compaction workload.

5. Production judgment

Frozen values are strongest when the logical object is small and should be replaced atomically as one cell: configuration snapshots, immutable coordinates, or versioned value objects. Multi-cell values are strongest when individual fields/elements change independently and the collection/UDT remains bounded. Under retries, whole-value replacements are usually easier to reason about if they are deterministic; read-modify-write in the application can still race. Multi-cell deletions and expirations increase tombstone activity. Neither choice changes partition placement: the table's primary key still determines replica ownership and hot-partition risk.

Verification checklist

  • Targeted updates succeed only on the multi-cell columns.
  • Frozen snapshots are changed by whole-value replacement.
  • The intentionally unfrozen nested collection schema is rejected.
  • The corrected nested schema has an explicit frozen inner boundary.
  • Operational evidence is treated as qualitative unless measured under a representative workload.

Check your understanding

  1. What is the mutation unit of a frozen map?
  2. Why might a non-frozen map create more tombstones?
  3. Are tuples independently mutable by position?
  4. Why is freezing everything not a universal optimization?
  5. Does frozen change which replicas own the row?
Review the answers

1. The whole map value.

2. Individual keys are separate cells, so key removals/TTL expiry can create element-level tombstones.

3. No. Tuple values are inherently frozen and replaced as one value.

4. It removes targeted mutation and can force whole-value rewrites for small logical changes.

5. No. Partition-key/token/replication rules are unchanged.

bash · reset Chapter 08 data when desired
# Destructive only to this disposable chapter keyspace.docker exec atlasmart-cass-1 cqlsh -e "DROP KEYSPACE IF EXISTS atlasmart_types;"# Keep shared course containers for Chapter 09, or remove them for a full reset:# docker rm -f atlasmart-cass-1 atlasmart-cass-2 atlasmart-cass-3# docker volume rm atlasmart-cass-1-data atlasmart-cass-2-data atlasmart-cass-3-data# docker network rm atlasmart-cassandra

Summary and next bridge

frozen defines a write/read boundary, not a decoration. Next we apply the same physical thinking to static columns, where one value is shared by every clustering row in a partition.

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.