Cassandra User-Defined Types
A User-Defined Type (UDT) lets you group related fields into a named, reusable data structure. Instead of flattening every attribute into individual columns or duplicating a cluster of columns across multiple tables, you define a UDT once and use it as a column type anywhere in your schema.
The Business Card Analogy
Imagine storing contact information for people. Without UDTs, you would add columns like street, city, state, postal_code, and country directly to every table that needs an address. With a UDT, you define an address type once and use it as a single column everywhere — just like printing a business card template and reusing it.
Without UDT (flat columns): CREATE TABLE customers ( customer_id UUID PRIMARY KEY, billing_street TEXT, billing_city TEXT, billing_state TEXT, billing_zip TEXT, ship_street TEXT, ship_city TEXT, ship_state TEXT, ship_zip TEXT ); With UDT (structured address): CREATE TYPE address ( street TEXT, city TEXT, state TEXT, zip TEXT ); CREATE TABLE customers ( customer_id UUID PRIMARY KEY, billing_address FROZEN<address>, shipping_address FROZEN<address> );
Creating a UDT
USE ecommerce; CREATE TYPE address ( street TEXT, city TEXT, state TEXT, zip TEXT, country TEXT );
Using a UDT in a Table
UDTs used as column types must be wrapped in FROZEN. A frozen UDT is serialized as a single blob — you replace the entire value when you update it rather than updating individual fields.
CREATE TABLE customers ( customer_id UUID PRIMARY KEY, name TEXT, billing_address FROZEN<address>, shipping_address FROZEN<address> );
Inserting Data with a UDT
INSERT INTO customers (customer_id, name, billing_address, shipping_address)
VALUES (
uuid(),
'Alice Smith',
{street: '10 Main St', city: 'Boston', state: 'MA', zip: '02101', country: 'US'},
{street: '5 Oak Ave', city: 'Cambridge', state: 'MA', zip: '02139', country: 'US'}
);
Reading UDT Fields
-- Select the full UDT value:
SELECT name, billing_address FROM customers
WHERE customer_id = [uuid];
name | billing_address
─────────────────────────────────────────────────────────────────────
Alice Smith | {street: '10 Main St', city: 'Boston', state: 'MA', ...}
-- Select a specific field within a UDT:
SELECT billing_address.city FROM customers
WHERE customer_id = [uuid];
billing_address.city
──────────────────────
Boston
Updating a UDT Column
Because UDT columns are FROZEN, you replace the entire value on every update. You cannot update just one field inside the UDT.
UPDATE customers
SET billing_address = {
street: '20 Elm St',
city: 'Boston',
state: 'MA',
zip: '02110',
country:'US'
}
WHERE customer_id = [uuid];
UDTs Inside Collections
UDTs work well inside collections to build rich data structures without creating extra tables.
List of UDTs
CREATE TYPE phone_number (
type TEXT,
number TEXT
);
CREATE TABLE contacts (
contact_id UUID PRIMARY KEY,
name TEXT,
phones LIST<FROZEN<phone_number>>
);
INSERT INTO contacts (contact_id, name, phones)
VALUES (
uuid(),
'Bob Jones',
[
{type: 'mobile', number: '555-1001'},
{type: 'work', number: '555-2002'}
]
);
Map of UDTs
CREATE TABLE company_contacts (
company_id UUID PRIMARY KEY,
name TEXT,
locations MAP<TEXT, FROZEN<address>>
);
INSERT INTO company_contacts (company_id, name, locations)
VALUES (
uuid(),
'Acme Corp',
{
'headquarters': {street: '1 Corp Blvd', city: 'New York', state: 'NY', zip: '10001', country: 'US'},
'warehouse': {street: '5 Dock Rd', city: 'Newark', state: 'NJ', zip: '07101', country: 'US'}
}
);
Nested UDTs
UDTs can reference other UDTs, but all nested types must be FROZEN.
CREATE TYPE geo_point ( latitude DOUBLE, longitude DOUBLE ); CREATE TYPE location_detail ( address FROZEN<address>, gps FROZEN<geo_point>, timezone TEXT ); CREATE TABLE offices ( office_id UUID PRIMARY KEY, name TEXT, location FROZEN<location_detail> );
Altering a UDT
You can add new fields to a UDT using ALTER TYPE. You cannot rename or remove existing fields.
ALTER TYPE address ADD floor_number INT; ALTER TYPE address ADD apartment_number TEXT;
Existing rows that used the old UDT structure return NULL for the new fields until the data is updated.
Dropping a UDT
DROP TYPE IF EXISTS address;
You cannot drop a UDT that is still in use by any table. Drop or modify those tables first.
Listing UDTs in a Keyspace
DESCRIBE TYPES; address phone_number geo_point location_detail
UDTs vs Separate Tables
Use UDT When Use Separate Table When ────────────────────────────────────────────────────────────── The group of fields always moves Individual fields are queried together (address, phone) independently by primary key The data is small and bounded The data can grow unboundedly You never need to query by You query by individual fields individual fields inside the UDT You want cleaner, self-documenting You need fine-grained updates schema without replacing whole objects
Summary
User-Defined Types group related fields into a named, reusable structure. They must be wrapped in FROZEN when used as column types, which means updates replace the entire UDT value. UDTs work inside collections to create rich, nested data structures. Use them for tightly coupled attribute groups like addresses, coordinates, or contact details. Use ALTER TYPE to add fields non-destructively; existing rows will return NULL for new fields until rewritten.
