These patterns compile without errors and pass basic testing. They fail in production when load increases, nodes fail, or edge cases occur. Each anti-pattern includes the failure scenario, symptoms, and correct implementation.
For data modeling anti-patterns (unbounded partitions, tombstones, ALLOW FILTERING), see Data Modeling Anti-Patterns .
Anti-Pattern Severity Failure Mode Session per request Critical Resource exhaustion, connection storms Retrying non-idempotent writes Critical Data corruption, duplicates Ignoring write timeouts Critical Silent data loss or duplication Read-modify-write without LWT Critical Race conditions, lost updates Synchronous calls in async context High Thread pool exhaustion, deadlocks Unbounded IN clauses High Coordinator OOM, cascading timeouts Missing local datacenter config High Cross-DC latency, failover failures Catching generic exceptions High Hidden failures, incorrect recovery Unprepared statements in loops Medium Performance degradation, cache churn Batch misuse Medium Coordinator overload, timeouts Missing timeouts Medium Request hangs, resource leaks LWT overuse Medium Performance bottleneck
// WRONG: New session for every request
public User getUser ( UUID userId ) {
try ( CqlSession session = CqlSession . builder ()
. addContactPoint ( new InetSocketAddress ( " localhost " , 9042 ))
. withLocalDatacenter ( " dc1 " )
" SELECT * FROM users WHERE user_id = ? " , userId
Phase Action Cost Connect TCP handshake to contact point 1-10 ms Negotiate CQL protocol handshake 5-20 ms Discover Fetch cluster topology 50-200 ms Pool Open connections to all nodes 100-500 ms Execute Run the actual query 2-10 ms Close Tear down all connections 10-50 ms
Illustrative overhead per request: ~170-790 ms (depending on network and cluster topology) for a typical 2-10 ms query.
At 100 requests/second:
100 full connection cycles per second
100 × (nodes in cluster) TCP connections opened and closed
Cluster sees connection storms during traffic spikes
nodetool clientstats shows thousands of connected clients
// CORRECT: Single session for application lifetime
public class CassandraService {
private final CqlSession session ;
private final PreparedStatement selectUser ;
public CassandraService () {
this . session = CqlSession . builder ()
. addContactPoint ( new InetSocketAddress ( " localhost " , 9042 ))
. withLocalDatacenter ( " dc1 " )
this . selectUser = session . prepare (
" SELECT * FROM users WHERE user_id = ? "
public User getUser ( UUID userId ) {
Row row = session . execute ( selectUser . bind ( userId )) . one () ;
# Connection churn in Cassandra logs
grep " connections " /var/log/cassandra/debug.log | grep -E " (opened|closed) "
# High client connection counts
# In application metrics: session creation rate should be ~0
// WRONG: Automatic retry on all writes
public void incrementCounter ( UUID pageId ) {
" UPDATE page_stats SET views = views + 1 WHERE page_id = ? " ,
if (attempts >= 3 ) throw e;
T+0ms: Client sends INCREMENT to Coordinator
T+5ms: Coordinator sends to Replica A
T+6ms: Replica A applies: views = 100 → 101
T+7ms: Network hiccup, ACK lost
T+2000ms: Client timeout, assumes failure
T+2001ms: Client retry #1 sends INCREMENT
T+2006ms: Replica A applies: views = 101 → 102 ← DUPLICATE!
Result: views = 102 (should be 101)
Operation Idempotent? Safe to Retry? SELECT *Yes Yes INSERT ... IF NOT EXISTSYes Yes UPDATE ... SET x = 5Yes Yes UPDATE ... SET x = x + 1No No DELETEYes Yes INSERT (without IF NOT EXISTS)Depends Maybe
// CORRECT: Mark idempotency explicitly
public void setPageViews ( UUID pageId, long views ) {
// Idempotent: setting to specific value
Statement stmt = setViewsStmt . bind ( pageId, views )
public void incrementCounter ( UUID pageId ) {
// Non-idempotent: no automatic retry
Statement stmt = incrementStmt . bind ( pageId )
. setRetryPolicy ( FallthroughRetryPolicy . INSTANCE ) ;
} catch ( WriteTimeoutException e ) {
// Log for manual investigation - do NOT retry
log . error ( " Counter increment may have failed for page {}: {} " ,
// Optionally: queue for reconciliation
// Make increment idempotent with request ID
public void incrementWithDedup ( UUID pageId, UUID requestId ) {
// Check if already processed
Row existing = session . execute (
" SELECT request_id FROM page_updates WHERE page_id = ? AND request_id = ? " ,
return ; // Already processed
// Atomic check-and-update
ResultSet rs = session . execute (
" INSERT INTO page_updates (page_id, request_id, processed_at) " +
" VALUES (?, ?, toTimestamp(now())) IF NOT EXISTS " ,
" UPDATE page_stats SET views = views + 1 WHERE page_id = ? " ,
// WRONG: Treating timeout as definite failure
public void createOrder ( Order order ) {
session . execute ( insertOrder . bind ( order . getId () , order . getData ())) ;
log . info ( " Order created: {} " , order . getId ()) ;
} catch ( WriteTimeoutException e ) {
// WRONG: Assuming the write failed
log . error ( " Order creation failed: {} " , order . getId ()) ;
throw new OrderCreationFailedException ( order .getId ()) ;
A WriteTimeoutException means: “The coordinator didn’t receive enough acknowledgments within the timeout period.”
It does NOT mean the write failed:
WriteType What Happened Data State SIMPLECoordinator timed out waiting for replicas Write may exist on 0, 1, 2, or all replicas BATCHBatch log written, mutations may be partial Completion is not guaranteed; mutations may or may not be replayed BATCH_LOGBatch log write timed out Batch may or may not execute UNLOGGED_BATCHSome mutations may have applied Partial writes possible
// CORRECT: Handle timeout as unknown state
public OrderResult createOrder ( Order order ) {
session . execute ( insertOrder . bind ( order . getId () , order . getData ())) ;
return OrderResult . success ( order . getId ()) ;
} catch ( WriteTimeoutException e ) {
log . warn ( " Order {} write timeout - state unknown. " +
" WriteType: {}, received: {}/{} acks " ,
// Option 1: Return unknown status, let caller decide
return OrderResult . unknown ( order . getId ()) ;
// Option 2: Check if write succeeded (if idempotent insert)
// return verifyOrderExists(order.getId());
// Option 3: Queue for reconciliation
// reconciliationQueue.add(order.getId());
// For critical operations: verify state
private OrderResult verifyOrderExists ( UUID orderId ) {
Row row = session . execute (
selectOrder . bind ( orderId )
. setConsistencyLevel ( ConsistencyLevel . ALL )
return OrderResult . success ( orderId ) ;
return OrderResult . failed ( orderId ) ;
// WRONG: Non-atomic read-modify-write
public void transferFunds ( UUID from, UUID to, BigDecimal amount ) {
Row fromRow = session . execute (
" SELECT balance FROM accounts WHERE account_id = ? " , from
Row toRow = session . execute (
" SELECT balance FROM accounts WHERE account_id = ? " , to
BigDecimal fromBalance = fromRow . getBigDecimal ( " balance " ) ;
BigDecimal toBalance = toRow . getBigDecimal ( " balance " ) ;
// Calculate new balances
BigDecimal newFromBalance = fromBalance . subtract ( amount ) ;
BigDecimal newToBalance = toBalance . add ( amount ) ;
// Write new balances - RACE CONDITION HERE
" UPDATE accounts SET balance = ? WHERE account_id = ? " ,
" UPDATE accounts SET balance = ? WHERE account_id = ? " ,
Write from_balance = 70 ← Overwrites!
Result: Account shows 70, but 80 was withdrawn
Lost update: $50 transfer was overwritten
// CORRECT: Atomic compare-and-set
public TransferResult transferFunds ( UUID from, UUID to, BigDecimal amount ) {
// Read with serial consistency
Row fromRow = session . execute (
SimpleStatement . newInstance (
" SELECT balance FROM accounts WHERE account_id = ? " , from
) . setSerialConsistencyLevel ( ConsistencyLevel . LOCAL_SERIAL )
BigDecimal currentBalance = fromRow . getBigDecimal ( " balance " ) ;
if ( currentBalance . compareTo ( amount ) < 0 ) {
return TransferResult . insufficientFunds () ;
// Atomic conditional update
BigDecimal newBalance = currentBalance . subtract ( amount ) ;
ResultSet rs = session . execute (
" UPDATE accounts SET balance = ? " +
" WHERE account_id = ? " +
" IF balance = ? " , // Only if balance hasn't changed
newBalance, from, currentBalance
// Balance changed - retry or fail
return TransferResult . concurrentModification () ;
// Credit destination (separate LWT if needed)
" UPDATE accounts SET balance = balance + ? WHERE account_id = ? " ,
return TransferResult . success () ;
Pattern LWT Required? Last-write-wins updates No Append-only data (logs, events) No Counter increments (approximate) No Unique constraint enforcement Yes Balance/inventory checks Yes State machine transitions Yes
// WRONG: Blocking call inside async handler
public CompletableFuture< Response > handleRequest ( Request request ) {
return CompletableFuture . supplyAsync ( () -> {
// This blocks the async thread pool!
Row row = session . execute ( selectStmt . bind ( request . getId ())) . one () ;
return new Response ( row .getString ( " data " )) ;
// WRONG: Blocking to get async result
public Response handleRequestBad ( Request request ) {
CompletionStage < AsyncResultSet > future = session . executeAsync (
selectStmt . bind ( request . getId ())
// Blocks thread waiting for async result - defeats the purpose
AsyncResultSet rs = future . toCompletableFuture () . join () ;
return new Response ( rs .one () .getString ( " data " )) ;
Problem Impact Thread pool exhaustion All async workers blocked on I/O Deadlock potential If pool is bounded and all threads wait Latency spikes New requests queue behind blocked threads Throughput collapse Async system behaves like sync with fewer threads
// CORRECT: Fully async chain
public CompletionStage< Response > handleRequest ( Request request ) {
return session . executeAsync ( selectStmt . bind ( request . getId ()))
. thenApply ( rs -> rs . one ())
. thenApply ( row -> new Response ( row .getString ( " data " ))) ;
// CORRECT: Async with error handling
public CompletionStage< Response > handleRequestWithErrors ( Request request ) {
return session . executeAsync ( selectStmt . bind ( request . getId ()))
throw new NotFoundException ( request .getId ()) ;
return new Response ( row .getString ( " data " )) ;
if ( e . getCause () instanceof NotFoundException) {
return Response . notFound () ;
log . error ( " Query failed for {} " , request . getId () , e ) ;
// CORRECT: Reactive pagination
public Flux< User > getAllUsers () {
return Flux . from ( session . executeReactive ( selectAllUsers ))
// CORRECT: Backpressure-aware processing
public Mono< Long > countActiveUsers () {
return Flux . from ( session . executeReactive ( selectAllUsers ))
. filter ( row -> " active " . equals ( row . getString ( " status " )))
// WRONG: Building IN clause from user input
public List< Product > getProducts ( List< UUID > productIds ) {
// productIds could have 1000+ items
String cql = " SELECT * FROM products WHERE product_id IN ( " +
. collect ( Collectors . joining ( " , " )) +
return session . execute ( cql ) . all () . stream ()
. collect ( Collectors . toList ()) ;
IN Clause Size Coordinator Memory Concurrent Sub-queries 10 ~10 KB 10 100 ~100 KB 100 1,000 ~1 MB 1,000 10,000 ~10 MB 10,000 → OOM
Additional problems:
Prepared statement cache pollution (each unique IN size = new entry)
Single slow sub-query delays entire response
No partial results on failure
// CORRECT: Batch in application with controlled parallelism
public List< Product > getProducts ( List< UUID > productIds ) {
List < Product > results = new ArrayList <>();
for ( int i = 0 ; i < productIds . size () ; i += batchSize) {
List < UUID > batch = productIds . subList (
i, Math . min ( i + batchSize, productIds . size ())
ResultSet rs = session . execute (
selectProductsIn . bind ( batch )
rs . forEach ( row -> results . add ( mapToProduct ( row ))) ;
// CORRECT: Parallel async with bounded concurrency
public CompletableFuture<List< Product >> getProductsAsync ( List< UUID > productIds ) {
Semaphore semaphore = new Semaphore ( 50 ) ; // Max 50 concurrent
List < CompletableFuture < Product >> futures = productIds . stream ()
. map ( id -> CompletableFuture . supplyAsync ( () -> {
Row row = session . execute ( selectProduct . bind ( id )) . one () ;
return row != null ? mapToProduct ( row ) : null ;
} catch ( InterruptedException e ) {
Thread . currentThread () . interrupt () ;
. collect ( Collectors . toList ()) ;
return CompletableFuture . allOf ( futures . toArray ( new CompletableFuture [ 0 ] ))
. thenApply ( v -> futures . stream ()
. map ( CompletableFuture :: join )
. filter ( Objects :: nonNull )
. collect ( Collectors . toList ())) ;
// WRONG: No datacenter configuration
CqlSession session = CqlSession . builder ()
. addContactPoint ( new InetSocketAddress ( " 10.0.1.1 " , 9042 ))
. build () ; // Will fail or route to wrong DC
Scenario Result Driver 4.x (any topology) Throws IllegalStateException: local datacenter is mandatory Multi-DC cluster (Driver 3.x) Queries route to random DC DC failure (Driver 3.x) Failover may route to wrong DC
// CORRECT: Always specify local datacenter
CqlSession session = CqlSession . builder ()
. addContactPoint ( new InetSocketAddress ( " 10.0.1.1 " , 9042 ))
. withLocalDatacenter ( " us-east-1 " ) // Always required
// CORRECT: From configuration
CqlSession session = CqlSession . builder ()
DriverConfigLoader . fromClasspath ( " application.conf " )
contact - points = [ " 10.0.1.1:9042 " , " 10.0.1.2:9042 " ]
local - datacenter = " us-east-1 "
// WRONG: Generic exception handling
public User getUser ( UUID userId ) {
return session . execute ( selectUser . bind ( userId )) . one () ;
log . error ( " Query failed " , e ) ;
return null ; // Hides the real problem
Exception Type Correct Action Generic Handler Does NoNodeAvailableExceptionCircuit breaker, alert ops Returns null ReadTimeoutExceptionMaybe retry Returns null InvalidQueryExceptionFix the query (bug) Returns null AuthenticationExceptionFix credentials Returns null SyntaxErrorFix CQL syntax (bug) Returns null
All failures look the same in logs. Bugs are hidden. Operations doesn’t know cluster is down.
// CORRECT: Specific exception handling
public User getUser ( UUID userId ) {
Row row = session . execute ( selectUser . bind ( userId )) . one () ;
return row != null ? mapToUser ( row ) : null ;
} catch ( NoNodeAvailableException e ) {
// Cluster is down - this is an emergency
log . error ( " CRITICAL: No Cassandra nodes available " , e ) ;
metrics . increment ( " cassandra.cluster_unavailable " ) ;
throw new ServiceUnavailableException ( " Database unavailable " ) ;
} catch ( ReadTimeoutException e ) {
// Query took too long - might retry
log . warn ( " Read timeout for user {}: received {}/{} responses " ,
userId, e . getReceived () , e . getBlockFor ()) ;
metrics . increment ( " cassandra.read_timeout " ) ;
if ( e . getReceived () > 0 ) {
// Got some data, might be consistent enough
throw new PartialDataException ( " Partial read for user " + userId ) ;
throw new QueryTimeoutException ( " User query timed out " ) ;
} catch ( QueryValidationException e ) {
// Bug in our code - query is malformed
log . error ( " BUG: Invalid query for user {} " , userId, e ) ;
throw new IllegalStateException ( " Invalid query " , e ) ;
} catch ( DriverException e ) {
log . error ( " Driver error for user {} " , userId, e ) ;
throw new DataAccessException ( " Database error " , e ) ;
// WRONG: String concatenation in loop
public void importUsers ( List< User > users ) {
for ( User user : users) {
String cql = String . format (
" INSERT INTO users (user_id, name, email) VALUES (%s, '%s', '%s') " ,
user . getId () , user . getName () , user . getEmail ()
session . execute ( cql ) ; // Parsed every time
Problem Impact No prepared statement cache CQL parsed on every execution No token-aware routing Coordinator may not route optimally CQL injection vulnerability user.getName() could contain '); DROP TABLE users; --String formatting overhead CPU wasted on string operations
// CORRECT: Prepare once, bind in loop
public void importUsers ( List< User > users ) {
PreparedStatement insertUser = session . prepare (
" INSERT INTO users (user_id, name, email) VALUES (?, ?, ?) "
for ( User user : users) {
session . execute ( insertUser . bind (
// BETTER: Async with batching
public CompletableFuture< Void > importUsersAsync ( List< User > users ) {
PreparedStatement insertUser = session . prepare (
" INSERT INTO users (user_id, name, email) VALUES (?, ?, ?) "
List < CompletableFuture < AsyncResultSet >> futures = users . stream ()
. map ( user -> session . executeAsync ( insertUser . bind (
)) . toCompletableFuture ())
. collect ( Collectors . toList ()) ;
return CompletableFuture . allOf ( futures . toArray ( new CompletableFuture [ 0 ] )) ;
// WRONG: Batch for bulk operations
public void importProducts ( List< Product > products ) {
BatchStatement batch = BatchStatement . builder ( BatchType . LOGGED ) . build () ;
for ( Product product : products) {
batch = batch . add ( insertProduct . bind (
session . execute ( batch ) ; // 10,000 statements in one batch!
Batches in Cassandra are NOT for bulk operations:
What You Think What Actually Happens ”Batch = faster bulk insert” Coordinator must hold entire batch in memory ”Batch = transaction” LOGGED batch only ensures atomicity, not isolation ”More in batch = better” Large batches timeout, overload coordinator
Batch Size Result 1-10 statements OK (if same partition) 10-100 statements Warning signs 100+ statements Timeouts, coordinator OOM
// CORRECT: Batches only for same-partition atomicity
public void updateUserWithAddress ( User user, Address address ) {
// Both statements target same partition key
BatchStatement batch = BatchStatement . builder ( BatchType . LOGGED )
. addStatement ( updateUser . bind ( user . getId () , user . getName ()))
. addStatement ( updateAddress . bind ( user . getId () , address . getStreet ()))
// CORRECT: Async for bulk operations
public CompletableFuture< Void > importProducts ( List< Product > products ) {
List < CompletableFuture < AsyncResultSet >> futures = products . stream ()
. map ( product -> session . executeAsync ( insertProduct . bind (
)) . toCompletableFuture ())
. collect ( Collectors . toList ()) ;
return CompletableFuture . allOf ( futures . toArray ( new CompletableFuture [ 0 ] )) ;
// CORRECT: UNLOGGED batch for same-partition bulk
public void addOrderItems ( UUID orderId, List< OrderItem > items ) {
// All items have same partition key (orderId)
BatchStatementBuilder batch = BatchStatement . builder ( BatchType . UNLOGGED ) ;
for ( OrderItem item : items) {
batch . addStatement ( insertOrderItem . bind (
orderId, // Same partition key
session . execute ( batch . build ()) ;
Use Case Batch Type Max Size Atomic multi-table update (same partition) LOGGED 5-10 Bulk insert to same partition UNLOGGED 10-50 Bulk insert to different partitions No batch (use async) N/A
// WRONG: No timeout configuration
CqlSession session = CqlSession . builder ()
. addContactPoint ( new InetSocketAddress ( " localhost " , 9042 ))
. withLocalDatacenter ( " dc1 " )
// Request can hang indefinitely
Row row = session . execute ( " SELECT * FROM large_table " ) . one () ;
Requests hang during network partitions
Thread pool exhaustion from blocked requests
No circuit breaker trigger (never fails, just hangs)
Application appears frozen
// CORRECT: Configure timeouts
CqlSession session = CqlSession . builder ()
. addContactPoint ( new InetSocketAddress ( " localhost " , 9042 ))
. withLocalDatacenter ( " dc1 " )
. withConfigLoader ( DriverConfigLoader . programmaticBuilder ()
. withDuration ( DefaultDriverOption . REQUEST_TIMEOUT , Duration . ofSeconds ( 5 ))
. withDuration ( DefaultDriverOption . CONNECTION_INIT_QUERY_TIMEOUT , Duration . ofSeconds ( 5 ))
. withDuration ( DefaultDriverOption . CONNECTION_CONNECT_TIMEOUT , Duration . ofSeconds ( 5 ))
// Per-query timeout override
Statement stmt = selectStmt . bind ( userId )
. setTimeout ( Duration . ofSeconds ( 2 )) ;
Timeout Recommended Description REQUEST_TIMEOUT5-10s Total request time CONNECTION_CONNECT_TIMEOUT5s TCP connect time CONNECTION_INIT_QUERY_TIMEOUT5s Initial handshake
// WRONG: LWT for every write
public void saveUser ( User user ) {
" INSERT INTO users (user_id, name, email) VALUES (?, ?, ?) IF NOT EXISTS " ,
user . getId () , user . getName () , user . getEmail ()
public void updateUser ( User user ) {
" UPDATE users SET name = ?, email = ? WHERE user_id = ? IF EXISTS " ,
user . getName () , user . getEmail () , user . getId ()
LWT uses Paxos consensus:
Operation Regular Write LWT Round trips 1 2-4 (varies by Cassandra version and contention) Latency Typically 2-5 ms Typically 10-50+ ms (varies with contention and topology) Throughput High (partition-independent) Significantly reduced (serial per partition)
// CORRECT: LWT only when needed
public void saveUser ( User user ) {
// Regular insert - last write wins
session . execute ( insertUser . bind (
user . getId () , user . getName () , user . getEmail ()
// LWT only for uniqueness constraint
public boolean registerEmail ( String email, UUID userId ) {
ResultSet rs = session . execute (
" INSERT INTO users_by_email (email, user_id) VALUES (?, ?) IF NOT EXISTS " ,
// LWT for state transitions
public boolean markOrderShipped ( UUID orderId ) {
ResultSet rs = session . execute (
" UPDATE orders SET status = 'shipped' WHERE order_id = ? IF status = 'pending' " ,
Use Case LWT Needed? Unique email registration Yes Balance check before debit Yes State machine transitions Yes Last-write-wins updates No Append-only logs No Idempotent upserts No
Symptom Likely Anti-Pattern Connection storm in logs Session per request Duplicate records appearing Retrying non-idempotent writes Data inconsistency after timeout Ignoring write timeout Lost updates Read-modify-write without LWT Thread pool exhaustion Sync in async context Coordinator OOM Unbounded IN clauses Cross-DC latency Missing local datacenter All errors look the same Generic exception handling Slow bulk imports Unprepared statements Batch timeouts Batch misuse Hanging requests Missing timeouts Slow partition writes LWT overuse