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.

Leave a Comment

Your email address will not be published. Required fields are marked *