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.
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.
Explain tuples as fixed positional values and UDTs as named keyspace-scoped structured values.
Create frozen and non-frozen UDT columns and state when individual field mutation is available.
Use ALTER TYPE add/rename safely and understand that arbitrary field-type changes are not a modern CQL migration mechanism.
Observe existing UDT values after an additive schema change and account for NULL in the new field.
Decode tuple and UDT values with a maintained driver instead of string parsing.
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.
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
# 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"
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;
-- 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
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);
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
- What is the main semantic weakness of a tuple?
- What does frozen change for a UDT?
- What happens to old values after ALTER TYPE ADD?
- Can modern CQL arbitrarily change a UDT field type in place?
- 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.
# 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
- CQL data types — scalar, collection, tuple, user-defined type, frozen, and literal semantics.
- Creating collections — bounded collection guidance and current collection guardrails.
- CQL data definition — static-column behavior, table/type definitions, and schema restrictions.
- CREATE TABLE reference — frozen/non-frozen UDT and static-column examples.
- Tombstones — deletion markers, grace, reads, and compaction implications.
- Apache Cassandra downloads — current server and Java-driver release baselines.