Chapter 21 · Drivers, Prepared Statements, Token Awareness, Paging, Retries, and Load Balancing
Prepared Statements, Routing Keys, Token-Aware Routing, and Prepared Metadata
Use prepared-statement metadata and bound partition keys to make token-aware routing observable instead of hard-coding coordinators or building CQL strings manually.
Learning outcomes
AtlasMart's query is fast on the server but frequently uses a non-replica coordinator, adding an avoidable network hop. The CQL text looks correct; the missing piece is client routing metadata. Prepared statements solve more than string escaping: they tell the driver which bound variables form the partition key.
Explain server PREPARE/PREPARED/EXECUTE behavior and driver prepared-statement caching/reprepare.
Inspect bind-variable metadata and partition-key indices on a prepared statement.
Show when a bound statement automatically exposes a routing key and when it cannot.
Correlate routing key/token awareness with the coordinator selected for repeated reads.
Reject hand-built CQL strings and hard-coded coordinator pinning as normal application routing strategies.
The mandatory labs continue the established free/local
AtlasMart cluster: Docker Official Image
cassandra:5.0.9, Java 17 in that image, cluster
atlasmart-course, Docker network
atlasmart-cassandra, nodes
atlasmart-cass-1..3, datacenter dc1,
racks rack1..rack3, 16 virtual nodes per node,
and named disposable data volumes. Keyspace
atlasmart_driver uses
NetworkTopologyStrategy with replication factor
(RF) 3; ordinary reads/writes use LOCAL_QUORUM.
New tables explicitly use UnifiedCompactionStrategy (UCS), no
table default time-to-live (TTL), and Cassandra's normal
gc_grace_seconds. Authentication,
client/internode Transport Layer Security (TLS), and remote
Java Management Extensions (JMX) are disabled only on this
isolated single-host learning network. Application examples
use Apache Cassandra Java Driver
4.19.3
(org.apache.cassandra:java-driver-core:4.19.3)
with Java 17 and one long-lived CqlSession.
Driver documentation URLs still use the 4.19.0 documentation
set, while the ASF release is 4.19.3. Exact coordinator
choices, pool counts, traces, paging states, retry/speculation
counts, and latency percentiles are learner-captured runtime
evidence.
Run commands only against the disposable Apache Cassandra course lab or another explicitly approved non-production environment. Confirm node, keyspace, table, container, volume, path, and datacenter targets before destructive, failure-injection, cleanup, repair, restore, security, or topology operations. Capture current state and expected rollback/recovery evidence first; output and timings can differ by host, operating system, Java runtime, Docker/runtime, driver, and Cassandra configuration.
Terms and request-path mental model
The native protocol is Cassandra's binary client/server protocol over TCP, normally on port 9042. A driver implements that protocol for an application language. A contact point is an initial address used to bootstrap discovery; it is not a permanent leader or a complete static node list. A session is the driver's long-lived view of a cluster and owns topology metadata, control-plane state, connection pools, policies, prepared-statement caches, and request execution. A connection pool is the driver's set of TCP connections to a node; unlike a JDBC-style blocking pool, each Cassandra connection multiplexes many in-flight requests using native-protocol stream identifiers.
A request is sent to a coordinator, the Cassandra node that handles that request. The driver chooses that coordinator using a load-balancing policy. Token-aware routing prefers replicas for the target partition when the statement supplies a keyspace and routing key/token. The partition key hashes to a token; replicas own token ranges according to the keyspace replication strategy. Prepared statements let the server parse CQL once and return metadata, including bind-variable types and partition-key variable positions, which enables the driver to compute routing keys for bound statements. Paging splits a large result into multiple protocol responses; the paging state is an opaque continuation token tied to the exact statement and values. Idempotent means repeating a request has the same final database effect as executing it once. Retry and speculative-execution policies use that property to avoid duplicating unsafe mutations.
1. Prepared metadata is a routing contract
Preparing recurring CQL lets Cassandra parse/validate the query and return a prepared identifier plus result/bind metadata. The driver caches that metadata and prepares the query across nodes so it can execute it wherever load balancing sends the request. Crucially, the response identifies which bind variables correspond to the table's partition key. If all partition-key components are bound variables, the driver can encode the routing key, hash it to a token, inspect replica metadata and prioritize replicas.
A prepared statement whose partition key is partly hard-coded can lose automatic routing-key derivation even though Cassandra can execute the query. Correct CQL is therefore not the same thing as optimal client routing.
| Statement shape | Driver routing key | Operational consequence |
|---|---|---|
| all partition-key components bound | automatic | token-aware policy can prefer replicas |
| partition-key component hard-coded in CQL | may be unavailable | query can execute but may use generic coordinator plan |
| simple statement without explicit routing info | not auto-derived | default policy lacks token target |
| explicit setNode(...) | bypasses load balancing | reserve for node-local/admin cases, not normal queries |
2. Verify the table and natural replicas
docker exec atlasmart-cass-1 nodetool versiondocker exec atlasmart-cass-1 java -versiondocker exec atlasmart-cass-1 nodetool statusdocker exec atlasmart-cass-1 cqlsh -e "SELECT cluster_name,data_center,rack,release_version,native_protocol_version FROM system.local;"docker exec atlasmart-cass-1 cqlsh -e "SELECT peer,peer_port,data_center,rack,release_version FROM system.peers_v2;"
CREATE KEYSPACE IF NOT EXISTS atlasmart_driverWITH replication = {'class':'NetworkTopologyStrategy','dc1':3};CREATE TABLE IF NOT EXISTS atlasmart_driver.orders_by_customer_month ( tenant_id text, customer_id text, order_month date, order_time timestamp, order_id uuid, status text, total decimal, note text, PRIMARY KEY ((tenant_id,customer_id,order_month),order_time,order_id)) WITH CLUSTERING ORDER BY (order_time DESC,order_id ASC) AND compaction = {'class':'UnifiedCompactionStrategy'};CONSISTENCY LOCAL_QUORUM;INSERT INTO atlasmart_driver.orders_by_customer_month(tenant_id,customer_id,order_month,order_time,order_id,status,total,note)VALUES ('tenant-a','cust-42','2026-09-01','2026-09-08T08:00:00Z',21000000-0000-0000-0000-000000000001,'PAID',129.90,'baseline');INSERT INTO atlasmart_driver.orders_by_customer_month(tenant_id,customer_id,order_month,order_time,order_id,status,total,note)VALUES ('tenant-a','cust-42','2026-09-01','2026-09-08T08:10:00Z',21000000-0000-0000-0000-000000000002,'PACKING',59.00,'fragile');INSERT INTO atlasmart_driver.orders_by_customer_month(tenant_id,customer_id,order_month,order_time,order_id,status,total,note)VALUES ('tenant-a','cust-99','2026-09-01','2026-09-08T08:20:00Z',21000000-0000-0000-0000-000000000003,'PAID',210.00,'priority');
CONSISTENCY LOCAL_QUORUM;TRACING ON;SELECT order_time,order_id,status,totalFROM atlasmart_driver.orders_by_customer_monthWHERE tenant_id='tenant-a' AND customer_id='cust-42' AND order_month='2026-09-01';TRACING OFF;
3. Inspect prepared metadata and coordinator choices
<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>academy.atlasmart</groupId><artifactId>cassandra-driver-lab</artifactId><version>1.0.0</version> <properties><maven.compiler.release>17</maven.compiler.release></properties> <dependencies> <dependency> <groupId>org.apache.cassandra</groupId> <artifactId>java-driver-core</artifactId> <version>4.19.3</version> </dependency> <dependency><groupId>org.slf4j</groupId><artifactId>slf4j-simple</artifactId><version>2.0.17</version></dependency> </dependencies> <build><plugins><plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>3.5.0</version></plugin></plugins></build></project>
package academy.atlasmart;import com.datastax.oss.driver.api.core.CqlSession;import com.datastax.oss.driver.api.core.cql.*;import java.net.InetSocketAddress;import java.time.LocalDate;public class DriverLab { public static void main(String[] args) { try (CqlSession s=CqlSession.builder() .addContactPoint(new InetSocketAddress("atlasmart-cass-1",9042)) .withLocalDatacenter("dc1").build()) { PreparedStatement ps=s.prepare("SELECT order_time,order_id,status,total FROM atlasmart_driver.orders_by_customer_month WHERE tenant_id=? AND customer_id=? AND order_month=?"); System.out.println("variables="+ps.getVariableDefinitions()); System.out.println("partitionKeyIndices="+ps.getPartitionKeyIndices()); BoundStatement bs=ps.bind("tenant-a","cust-42",LocalDate.parse("2026-09-01")); System.out.println("routingKeyspace="+bs.getRoutingKeyspace()); System.out.println("routingKeyPresent="+(bs.getRoutingKey()!=null)); for(int i=0;i<10;i++) { ResultSet rs=s.execute(bs); System.out.println("coordinator="+rs.getExecutionInfo().getCoordinator().getEndPoint()); } } }}
# From the Maven project directory. Docker Desktop/WSL or Linux/macOS shell:docker run --rm --network atlasmart-cassandra -v "$PWD:/work" -w /work maven:3.9.16-eclipse-temurin-17 \ mvn -q -DskipTests compile exec:java -Dexec.mainClass=academy.atlasmart.DriverLab# PowerShell uses the same container and network; ${PWD} resolves to the current directory:# docker run --rm --network atlasmart-cassandra -v "${PWD}:/work" -w /work maven:3.9.16-eclipse-temurin-17 `# mvn -q -DskipTests compile exec:java -Dexec.mainClass=academy.atlasmart.DriverLab
Expected evidence: the prepared statement exposes three partition-key variable indices and the bound statement has a non-null routing key. Coordinator endpoints should usually be natural replicas under the default token-aware policy, but exact order changes with replica availability, policy state and topology.
4. Boundary case: hard-code one composite partition-key component
PreparedStatement partial = s.prepare( "SELECT * FROM atlasmart_driver.orders_by_customer_month " + "WHERE tenant_id='tenant-a' AND customer_id=? AND order_month=?");System.out.println(partial.getPartitionKeyIndices()); // expected empty: not all PK components are bind variablesBoundStatement b = partial.bind("cust-42", LocalDate.parse("2026-09-01"));System.out.println(b.getRoutingKey()); // expected null
Statement#setNode bypasses the load-balancing
policy and is intended for special node-local/admin cases. It
does not make the chosen node a data owner and removes normal
failover/routing behavior. The safer repair is recurring
prepared CQL with all partition-key components bound, plus the
correct local datacenter.
5. Prepared statements are not just SQL-injection protection
Binding avoids hand-built CQL values and performs type validation, but the important Cassandra-specific gains are cached server parsing/result metadata and partition-routing metadata. Prepare recurring query shapes once and reuse them. Repeatedly preparing many unique strings can create cache churn and defeats the intended model.
// WRONG: unique CQL strings; no reusable metadata/routing cache.String q = "SELECT * FROM atlasmart_driver.orders_by_customer_month WHERE tenant_id='" + tenant + "' ...";// GOOD: prepare once at DAO construction, bind values per request.private final PreparedStatement find = session.prepare( "SELECT * FROM atlasmart_driver.orders_by_customer_month WHERE tenant_id=? AND customer_id=? AND order_month=?");
Check your understanding
- What metadata enables automatic token-aware routing for a bound statement?
- Can prepared CQL execute even when the driver has no routing key?
- Why can hard-coding one composite partition-key component remove routing metadata?
- Should applications call setNode for normal reads to guarantee locality?
- Why prepare recurring statements once?
Review the answers
1. The keyspace plus all encoded partition-key bind variables, returned through prepared metadata and bound values.
2. Yes. The driver can choose a generic coordinator plan; execution correctness and optimal routing are different concerns.
3. The prepared metadata cannot identify all partition-key values as bind variables, so automatic routing-key construction may be unavailable.
4. No. It bypasses load balancing and failover; supply routing metadata and let the policy choose.
5. It reuses server/client prepared metadata and avoids repeated prepare traffic/cache churn.
Production judgment
Client correctness depends on more than successful TCP connectivity. Record Cassandra patch/native protocol negotiation, driver artifact/version, Java runtime, local datacenter, discovered topology, node distance, pool/in-flight metrics, prepared-statement/cache behavior, routing-key availability, page/fetch size, request/consistency/serial-consistency timeouts, retry policy, idempotence classification, speculative-execution policy, per-query execution profile, and p50/p95/p99 client latency. Correlate driver coordinator/attempt data with Cassandra tracing, server timeouts/failures, replica availability, RF/CL, partition size/cardinality, SSTable/compaction/tombstone state, disk/network/JVM pressure, repair state, and SAI/vector costs where those query paths are used.
Do not make a session per HTTP request, disable local-DC awareness casually, expose raw paging state as an authorization token, or enable broad retries/speculation to hide overload. Treat driver configuration as application production code: version it, test node/DC failures, test ambiguous timeouts with idempotent and non-idempotent mutations, measure extra traffic, and define rollback. Managed services may provide different endpoints, TLS/auth requirements, topology visibility, or restricted metrics while still using a Cassandra-compatible protocol; verify the service contract rather than assuming identical behavior. Lesson 3 keeps the same prepared query but focuses on result transport: fetch size controls one network page, while paging state is an opaque statement-bound continuation—not an arbitrary API offset or authorization credential.
Summary and next bridge
Prepared statements provide type/result metadata and, when the partition key is fully bound, automatic routing information. That lets the driver prefer replicas without hard-coded coordinators. Next, make large-result paging explicit and safe for APIs.
Authoritative references
Driver behavior is language- and version-specific. Re-check the exact maintained driver and Cassandra patch before freezing production defaults or error-handling behavior.
- Apache Cassandra downloads / current 5.0 patch
- Docker Official Cassandra 5.0 image source
- Apache Cassandra Java Driver overview
- Java Driver pooling
- Java Driver prepared statements
- Java Driver load balancing / token awareness
- Java Driver paging
- Java Driver retries
- Java Driver idempotence
- Java Driver speculative execution
- Maven Central Java Driver 4.19.3