Spring Boot with Cassandra — Spring Data Cassandra
Every idea from the first four chapters — partition vs. clustering key, tunable consistency, query-first tables — shows up again here as annotations and method calls. This chapter is where they become code you'd actually ship.
Learning objectives
- Map a Cassandra table to a Spring Data entity using @Table, @PrimaryKey, and @PrimaryKeyClass.
- Distinguish PrimaryKeyType.PARTITIONED from PrimaryKeyType.CLUSTERED and connect each to Chapter 01's partition/clustering key distinction.
- Use CassandraRepository for standard access patterns and CassandraTemplate when a query doesn't fit the repository model.
- Set a per-query consistency level from Spring code.
For a table with a simple, single-column key, @PrimaryKey on one field is enough. Chapter 01's orders_by_user has a composite key — a partition key plus clustering columns — and Spring Data Cassandra requires a dedicated key class for that shape, annotated @PrimaryKeyClass:
@PrimaryKeyClass public class OrderKey implements Serializable { @PrimaryKeyColumn(name = "user_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED) private UUID userId; @PrimaryKeyColumn(name = "order_date", ordinal = 1, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING) private Instant orderDate; @PrimaryKeyColumn(name = "order_id", ordinal = 2, type = PrimaryKeyType.CLUSTERED) private UUID orderId; // equals(), hashCode(), getters/setters required — Spring Data uses equals/hashCode as the entity identity } @Table("orders_by_user") public class Order { @PrimaryKey private OrderKey key; private String status; private BigDecimal total; // getters/setters }
ordinal fixes each column's position in the underlying PRIMARY KEY clause, and PrimaryKeyType.PARTITIONED vs. PrimaryKeyType.CLUSTERED map directly onto Chapter 01's partition-key/clustering-key distinction — get this wrong (mark order_date as PARTITIONED, say) and you've silently rebuilt Chapter 01's wide-partition or full-scan mistakes, just through an annotation instead of raw CQL.
💻 Code example
@PrimaryKeyClass public class OrderKey implements Serializable { @PrimaryKeyColumn(name = "user_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED) private UUID userId; @PrimaryKeyColumn(name = "order_date", ordinal = 1, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING) private Instant orderDate; @PrimaryKeyColumn(name = "order_id", ordinal = 2, type = PrimaryKeyType.CLUSTERED) private UUID orderId; } @Table("orders_by_user") public class Order { @PrimaryKey private OrderKey key; private String status; private BigDecimal total; }
public interface OrderRepository extends CassandraRepository<Order, OrderKey> { // works: userId is the partition key component, reached through the key's property path List<Order> findByKeyUserId(UUID userId); }
CassandraRepository gives you the standard save, findById, deleteById, plus Spring Data's derived-query-method mechanism. But that mechanism can only generate CQL that Cassandra can actually run efficiently — a method like findByStatus(String status) fails at startup, for exactly the reason covered in Chapter 04: Spring Data won't silently generate a full-cluster scan any more than raw CQL will run one without ALLOW FILTERING. Spring Data Cassandra does expose an @AllowFiltering annotation for a repository method if you genuinely intend that semantics — but treat needing it here as the same design-smell signal as needing ALLOW FILTERING in raw CQL: it usually means the right answer is a dedicated table for that query pattern, not a filtered scan.
💻 Code example
public interface OrderRepository extends CassandraRepository<Order, OrderKey> { List<Order> findByKeyUserId(UUID userId); }
When a query needs to be built dynamically at runtime, or needs an option a derived repository method can't express (a non-default consistency level, a TTL, an explicit Query/Criteria), inject CassandraTemplate directly:
@Service public class OrderQueryService { private final CassandraTemplate cassandraTemplate; public OrderQueryService(CassandraTemplate cassandraTemplate) { this.cassandraTemplate = cassandraTemplate; } public List<Order> recentOrders(UUID userId, int limit) { Query query = Query.query(Criteria.where("user_id").is(userId)).limit(limit); return cassandraTemplate.select(query, Order.class); } }
For raw CQL that doesn't map cleanly onto the Query/Criteria API, cassandraTemplate.getCqlOperations() gives you the lower-level CqlOperations interface to run a CQL string directly, in the same spirit as JDBC's JdbcTemplate for relational code.
💻 Code example
@Service public class OrderQueryService { private final CassandraTemplate cassandraTemplate; public OrderQueryService(CassandraTemplate cassandraTemplate) { this.cassandraTemplate = cassandraTemplate; } public List<Order> recentOrders(UUID userId, int limit) { Query query = Query.query(Criteria.where("user_id").is(userId)).limit(limit); return cassandraTemplate.select(query, Order.class); } }
Chapter 02's per-query consistency-level choice is just as available from Spring code, via the write/query options builders:
// financial adjustment — the same QUORUM reasoning as Chapter 02 InsertOptions options = InsertOptions.builder() .consistencyLevel(DefaultConsistencyLevel.QUORUM) .build(); cassandraTemplate.insert(walletAdjustment, options); QueryOptions readOptions = QueryOptions.builder() .consistencyLevel(DefaultConsistencyLevel.QUORUM) .build(); Order order = cassandraTemplate.selectOne( Query.query(Criteria.where("user_id").is(userId)).queryOptions(readOptions), Order.class);
If no consistency level is set on a particular statement, it falls back to the driver's configured default (set in the driver profile / Spring Boot Cassandra properties) — most teams set a safe cluster-wide default (typically LOCAL_QUORUM) and override only the specific writes and reads, like a financial adjustment, that genuinely need something stronger, exactly the pattern shown here.
▲ Common mistake
Copy-pasting a ConsistencyLevel.ONE write path from a low-stakes feature (an activity feed) into a new feature without reconsidering it. The consistency level is a property of the call site, not of the entity or the table — it has to be chosen deliberately every time, the same way Chapter 02 chose it per query rather than per table.
💻 Code example
InsertOptions options = InsertOptions.builder() .consistencyLevel(DefaultConsistencyLevel.QUORUM) .build(); cassandraTemplate.insert(walletAdjustment, options); QueryOptions readOptions = QueryOptions.builder() .consistencyLevel(DefaultConsistencyLevel.QUORUM) .build(); Order order = cassandraTemplate.selectOne( Query.query(Criteria.where("user_id").is(userId)).queryOptions(readOptions), Order.class);
Want a visual for this concept?
Generate a diagram tailored to “Spring Boot with Cassandra — Spring Data Cassandra” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →