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.

Intermediate → Advanced110–150 minutesPrepared/token-aware routing labApache Cassandra 5.0.9 · Java 17 · Apache Java Driver 4.19.3 · RF=3 · LOCAL_QUORUM · UCSLast reviewed: September 2026

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.

01

Explain server PREPARE/PREPARED/EXECUTE behavior and driver prepared-statement caching/reprepare.

02

Inspect bind-variable metadata and partition-key indices on a prepared statement.

03

Show when a bound statement automatically exposes a routing key and when it cannot.

04

Correlate routing key/token awareness with the coordinator selected for repeated reads.

05

Reject hand-built CQL strings and hard-coded coordinator pinning as normal application routing strategies.

Chapter 21 lab baseline

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.

Execution and safety note

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

bash / PowerShell-friendly Docker commands · verify the shared cluster
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;"
CQL · create the driver-focused AtlasMart query table
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');
CQL · trace the target partition from cqlsh
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

XML · minimal pinned Maven project for the Apache Java Driver
<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>
Java · PreparedRoutingProbe.java
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());      }    }  }}
bash / PowerShell · compile and run on the Cassandra Docker network
# 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

Java · prepared but not automatically routable
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
Wrong repair: pin every request to a chosen node.

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.

Java · bad and good recurring-query patterns
// 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

  1. What metadata enables automatic token-aware routing for a bound statement?
  2. Can prepared CQL execute even when the driver has no routing key?
  3. Why can hard-coding one composite partition-key component remove routing metadata?
  4. Should applications call setNode for normal reads to guarantee locality?
  5. 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.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.