How to Handle Cassandra UDTs in Java with Custom Codecs (Step-by-Step Guide + Examples)

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 Java Amount object into a Cassandra UDTValue.
  • deserialize() converts a Cassandra UDTValue back into a Java Amount.

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

IssueCauseFix
CodecNotFoundExceptionCodec registered after session creationRegister codec before creating Session
InvalidTypeExceptionJava field names don’t match Cassandra UDT field namesMatch field names exactly (currency / amount)
NullPointerExceptionserialize() or deserialize() not handling nullsAdd 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.
This article is inspired by real-world challenges we tackle in our projects. If you're looking for expert solutions or need a team to bring your idea to life,

Let's talk!

    Please fill your details, and we will contact you back

      Please fill your details, and we will contact you back