When working with Apache Cassandra in real-world enterprise applications, you often need to store structured data such as monetary amounts, contact information, or complex settings. Instead of flattening everything into multiple columns, User-Defined Types (UDTs) let you model complex objects as a single column.
However, to use UDTs effectively in Java (especially with frameworks like Spring or the DataStax Java driver), you must map UDTs to Java classes and often create a custom codec to serialize and deserialize these values automatically.
In this article you’ll learn:
- What Cassandra UDTs are and why they’re useful.
- How to create a UDT in Cassandra.
- How to create a Java class matching the UDT.
- How to implement a custom codec using the
MappingCodec
API. - Examples of inserting and retrieving UDTs with prepared statements.
Let’s dive in.
Step 1: Creating a User Defined Type (UDT) in Cassandra
Suppose we have an amount UDT with two fields: currency
and amount
.
CREATE TYPE IF NOT EXISTS accounting.amount (
currency text,
amount text
);
This creates a UDT named amount
inside the accounting
keyspace.
Next, create a table that uses this UDT:
CREATE TABLE IF NOT EXISTS accounting.transactions (
id uuid PRIMARY KEY,
description text,
transaction_amount frozen<amount>
);
Step 2: Create a Matching Java Class
In your Java project, create a simple POJO mirroring the UDT structure:
public class Amount {
private String currency;
private String amount;
public Amount() {}
public Amount(String currency, String amount) {
this.currency = currency;
this.amount = amount;
}
public String getCurrency() { return currency; }
public String getAmount() { return amount; }
public void setCurrency(String currency) { this.currency = currency; }
public void setAmount(String amount) { this.amount = amount; }
@Override
public String toString() {
return "Amount{" +
"currency='" + currency + '\'' +
", amount='" + amount + '\'' +
'}';
}
}
💡 Tip: The field names must match the UDT field names defined in Cassandra.
Step 3: Implement a Custom Codec Using MappingCodec
The DataStax Java driver provides MappingCodec<UDTValue, T>
which makes mapping between UDTValue
and a Java object straightforward.
import com.datastax.driver.core.UDTValue;
import com.datastax.driver.core.UserType;
import com.datastax.driver.extras.codecs.MappingCodec;
import wildengineer.cassandra.data.copy.udt.Amount;
public class AmountCodec extends MappingCodec<UDTValue, Amount> {
private final UserType userType;
public AmountCodec(UserType userType) {
super(com.datastax.driver.core.TypeCodec.udt(userType), Amount.class);
this.userType = userType;
}
@Override
protected UDTValue serialize(Amount value) {
if (value == null) return null;
return userType.newValue()
.setString("currency", value.getCurrency())
.setString("amount", value.getAmount());
}
@Override
protected Amount deserialize(UDTValue value) {
if (value == null) return null;
return new Amount(
value.getString("currency"),
value.getString("amount")
);
}
}
Key points:
serialize()
converts the JavaAmount
object into a CassandraUDTValue
.deserialize()
converts a CassandraUDTValue
back into a JavaAmount
.
Step 4: Register the Codec
You must register the codec before creating a session:
Cluster cluster = Cluster.builder()
.addContactPoint("127.0.0.1")
.build();
UserType amountUserType = cluster.getMetadata()
.getKeyspace("accounting")
.getUserType("amount");
CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry();
codecRegistry.register(new AmountCodec(amountUserType));
Session session = cluster.connect("accounting");
Step 5: Insert Data with Prepared Statements
Now you can insert data using Amount
objects directly:
PreparedStatement ps = session.prepare(
"INSERT INTO transactions (id, description, transaction_amount) VALUES (?, ?, ?)"
);
Amount amount = new Amount("USD", "150.00");
BoundStatement bs = ps.bind(UUID.randomUUID(), "Office supplies", amount);
session.execute(bs);
Notice you don’t manually build a UDTValue
; the codec does the conversion.
Step 6: Retrieve Data
When selecting rows, you can directly get an Amount
object:
Row row = session.execute("SELECT * FROM transactions").one();
Amount retrievedAmount = row.get("transaction_amount", Amount.class);
System.out.println(retrievedAmount);
The driver automatically applies the custom codec when reading data.
Step 7: Common Pitfalls & Troubleshooting
Issue | Cause | Fix |
---|---|---|
CodecNotFoundException | Codec registered after session creation | Register codec before creating Session |
InvalidTypeException | Java field names don’t match Cassandra UDT field names | Match field names exactly (currency / amount ) |
NullPointerException | serialize() or deserialize() not handling nulls | Add null checks |
Extended Example: UDT List
Cassandra also supports collections of UDTs, e.g.:
CREATE TABLE accounting.invoices (
id uuid PRIMARY KEY,
items list<frozen<amount>>
);
Java code to retrieve a list of Amount
:
List<Amount> items = row.getList("items", Amount.class);
Or insert:
List<Amount> amounts = Arrays.asList(
new Amount("EUR","99.99"),
new Amount("EUR","15.00")
);
session.execute(ps.bind(UUID.randomUUID(), amounts));
The same codec works automatically.
Why Use Custom Codecs Instead of Raw UDTValue?
- Cleaner, domain-driven code.
- Easier testing and maintenance.
- Less boilerplate converting to/from
UDTValue
. - Automatic handling of collections of UDTs.
Conclusion
By following these steps, you can confidently model complex data structures in Cassandra using UDTs and handle them in Java with custom codecs. This approach results in cleaner code, less manual conversion, and more maintainable applications — whether you’re building a financial platform, IoT application, or any large-scale data system.
Key Takeaways:
- Create UDT in Cassandra.
- Mirror it with a Java class.
- Implement a
MappingCodec
to handle conversions automatically. - Register the codec before creating the
Session
. - Enjoy clean insertion and retrieval of structured data.