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

Tuples and User-Defined Types for Structured Values and Schema Evolution

Model structured values with tuples and user-defined types while making positional semantics, named fields, freezing, driver decoding, and schema evolution explicit.

Intermediate90–120 minutesUDT evolution + tuple labApache Cassandra 5.0.9 · cqlsh/nodetool · Java Driver 4.19.3 optional · UCSLast reviewed: September 2026

Learning outcomes

AtlasMart needs shipping addresses and geographic coordinates in several query tables. A tuple can pack a small positional structure, while a user-defined type (UDT) gives named fields and a reusable schema inside one keyspace. Those conveniences have different mutation and evolution contracts, so they should be chosen deliberately rather than because they look object-like.

01

Explain tuples as fixed positional values and UDTs as named keyspace-scoped structured values.

02

Create frozen and non-frozen UDT columns and state when individual field mutation is available.

03

Use ALTER TYPE add/rename safely and understand that arbitrary field-type changes are not a modern CQL migration mechanism.

04

Observe existing UDT values after an additive schema change and account for NULL in the new field.

05

Decode tuple and UDT values with a maintained driver instead of string parsing.

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. Tuple and UDT solve different structure problems

A tuple has ordered typed positions such as tuple<double,double>. Its meaning depends on an external convention—“position 0 is latitude, position 1 is longitude”—and tuple values are effectively frozen as one value. A UDT has named fields such as line1, city, and country. UDTs belong to a keyspace and can be reused by tables in that keyspace. In Cassandra 5.0, a UDT containing only non-collection fields can be stored non-frozen, allowing individual field mutation; a frozen UDT is treated as one cell and replaced as a whole.

2. Lab: create, mutate, and evolve a UDT

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 structured types and an order table
CREATE TYPE atlasmart_types.address_v1 (    line1 text,    city text,    country text);CREATE TABLE atlasmart_types.orders_structured (    order_id uuid PRIMARY KEY,    ship_to atlasmart_types.address_v1,    ship_to_snapshot frozen<atlasmart_types.address_v1>,    drop_point tuple<double,double>,    total decimal) WITH compaction = {'class':'UnifiedCompactionStrategy'};DESCRIBE TYPE atlasmart_types.address_v1;DESCRIBE TABLE atlasmart_types.orders_structured;INSERT INTO atlasmart_types.orders_structured(order_id,ship_to,ship_to_snapshot,drop_point,total)VALUES(aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa, {line1:'1 Atlas Ave',city:'Tehran',country:'IR'}, {line1:'1 Atlas Ave',city:'Tehran',country:'IR'}, (35.6892,51.3890), 49.90);SELECT * FROM atlasmart_types.orders_structured;
sql · prove mutation granularity and additive evolution
-- Non-frozen UDT: mutate one field.UPDATE atlasmart_types.orders_structuredSET ship_to.city = 'Baku'WHERE order_id=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa;-- Frozen UDT and tuple: replace the whole value.UPDATE atlasmart_types.orders_structuredSET ship_to_snapshot = {line1:'1 Atlas Ave',city:'Baku',country:'AZ'},    drop_point = (40.4093,49.8671)WHERE order_id=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa;ALTER TYPE atlasmart_types.address_v1 ADD postal_code text;DESCRIBE TYPE atlasmart_types.address_v1;SELECT ship_to,ship_to_snapshot,drop_point FROM atlasmart_types.orders_structured;

Existing values predate postal_code, so the new field is expected to read as NULL until written. This is a schema-evolution fact, not automatic backfill. Current CQL supports adding and renaming UDT fields; arbitrary type changes were removed from modern CQL, so incompatible evolution requires a migration strategy.

3. Driver decoding preserves named and positional structure

java · optional Java Driver 4.19.3 structured decoding
Row row = session.execute(    "SELECT ship_to, ship_to_snapshot, drop_point FROM atlasmart_types.orders_structured WHERE order_id=?",    UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")).one();UdtValue live = row.getUdtValue("ship_to");String city = live.getString("city");String postalCode = live.getString("postal_code"); // null until writtenUdtValue snapshot = row.getUdtValue("ship_to_snapshot");TupleValue point = row.getTupleValue("drop_point");double latitude = point.getDouble(0);double longitude = point.getDouble(1);
Wrong approach: evolve the UDT as if it were an application class with arbitrary field rewrites.

Existing serialized values and all dependent tables/drivers must continue to agree on the CQL type. Additive/rename evolution has explicit semantics; incompatible representation changes need a new field/type/table plus dual-read/write or backfill plan and rollback criteria.

4. Production judgment

Use tuples when the structure is tiny, stable, and positional meaning is obvious to every consumer. Use UDTs when named fields materially improve correctness and several query tables share the same embedded value shape. Decide frozen versus non-frozen from mutation granularity: snapshots often belong frozen; mutable profile-like structures may benefit from field-level updates if their field types permit it. Keep UDTs from becoming a deeply nested object graph. Every extra nested value increases schema coupling, driver codec surface, migration testing, and cross-team coordination.

Verification checklist

  • DESCRIBE TYPE shows the expected keyspace-scoped field schema.
  • A non-frozen UDT field can be updated without replacing unrelated fields.
  • The frozen snapshot and tuple are replaced as whole values.
  • After ALTER TYPE ADD, historical rows expose NULL for the new field until updated.
  • Optional driver decoding uses UdtValue/TupleValue APIs and preserves nullability.

Check your understanding

  1. What is the main semantic weakness of a tuple?
  2. What does frozen change for a UDT?
  3. What happens to old values after ALTER TYPE ADD?
  4. Can modern CQL arbitrarily change a UDT field type in place?
  5. Why should drivers decode UDTs structurally?
Review the answers

1. Its fields are positional rather than named, so meaning lives in an external convention.

2. The entire UDT is stored/mutated as one value rather than independently addressable fields.

3. The new field is NULL until data is written; Cassandra does not backfill it automatically.

4. No. Current CQL evolution centers on adding or renaming fields; incompatible type changes need migration.

5. Named/typed decoding preserves schema semantics and avoids brittle string parsing.

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

Tuples are positional frozen structures; UDTs add names and controlled evolution. Next we focus directly on frozen versus multi-cell storage, nested types, and how mutation granularity changes tombstone and write behavior.

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.