Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Cassandra Application Development Anti-Patterns

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-PatternSeverityFailure Mode
Session per requestCriticalResource exhaustion, connection storms
Retrying non-idempotent writesCriticalData corruption, duplicates
Ignoring write timeoutsCriticalSilent data loss or duplication
Read-modify-write without LWTCriticalRace conditions, lost updates
Synchronous calls in async contextHighThread pool exhaustion, deadlocks
Unbounded IN clausesHighCoordinator OOM, cascading timeouts
Missing local datacenter configHighCross-DC latency, failover failures
Catching generic exceptionsHighHidden failures, incorrect recovery
Unprepared statements in loopsMediumPerformance degradation, cache churn
Batch misuseMediumCoordinator overload, timeouts
Missing timeoutsMediumRequest hangs, resource leaks
LWT overuseMediumPerformance bottleneck

// WRONG: New session for every request
public User getUser(UUID userId) {
try (CqlSession session = CqlSession.builder()
.addContactPoint(new InetSocketAddress("localhost", 9042))
.withLocalDatacenter("dc1")
.build()) {
return session.execute(
"SELECT * FROM users WHERE user_id = ?", userId
).one();
}
}
PhaseActionCost
ConnectTCP handshake to contact point1-10 ms
NegotiateCQL protocol handshake5-20 ms
DiscoverFetch cluster topology50-200 ms
PoolOpen connections to all nodes100-500 ms
ExecuteRun the actual query2-10 ms
CloseTear down all connections10-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")
.build();
this.selectUser = session.prepare(
"SELECT * FROM users WHERE user_id = ?"
);
}
public User getUser(UUID userId) {
Row row = session.execute(selectUser.bind(userId)).one();
return mapToUser(row);
}
public void shutdown() {
session.close();
}
}
Terminal window
# Connection churn in Cassandra logs
grep "connections" /var/log/cassandra/debug.log | grep -E "(opened|closed)"
# High client connection counts
nodetool clientstats
# In application metrics: session creation rate should be ~0

// WRONG: Automatic retry on all writes
public void incrementCounter(UUID pageId) {
int attempts = 0;
while (attempts < 3) {
try {
session.execute(
"UPDATE page_stats SET views = views + 1 WHERE page_id = ?",
pageId
);
return;
} catch (Exception e) {
attempts++;
if (attempts >= 3) throw e;
}
}
}
Timeline:
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!
T+2010ms: ACK succeeds
Result: views = 102 (should be 101)
OperationIdempotent?Safe to Retry?
SELECT *YesYes
INSERT ... IF NOT EXISTSYesYes
UPDATE ... SET x = 5YesYes
UPDATE ... SET x = x + 1NoNo
DELETEYesYes
INSERT (without IF NOT EXISTS)DependsMaybe
// CORRECT: Mark idempotency explicitly
public void setPageViews(UUID pageId, long views) {
// Idempotent: setting to specific value
Statement stmt = setViewsStmt.bind(pageId, views)
.setIdempotent(true);
session.execute(stmt);
}
public void incrementCounter(UUID pageId) {
// Non-idempotent: no automatic retry
Statement stmt = incrementStmt.bind(pageId)
.setIdempotent(false)
.setRetryPolicy(FallthroughRetryPolicy.INSTANCE);
try {
session.execute(stmt);
} catch (WriteTimeoutException e) {
// Log for manual investigation - do NOT retry
log.error("Counter increment may have failed for page {}: {}",
pageId, e.getMessage());
// 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 = ?",
pageId, requestId
).one();
if (existing != null) {
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",
pageId, requestId
);
if (rs.wasApplied()) {
session.execute(
"UPDATE page_stats SET views = views + 1 WHERE page_id = ?",
pageId
);
}
}

// WRONG: Treating timeout as definite failure
public void createOrder(Order order) {
try {
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:

WriteTypeWhat HappenedData State
SIMPLECoordinator timed out waiting for replicasWrite may exist on 0, 1, 2, or all replicas
BATCHBatch log written, mutations may be partialCompletion is not guaranteed; mutations may or may not be replayed
BATCH_LOGBatch log write timed outBatch may or may not execute
UNLOGGED_BATCHSome mutations may have appliedPartial writes possible
// CORRECT: Handle timeout as unknown state
public OrderResult createOrder(Order order) {
try {
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",
order.getId(),
e.getWriteType(),
e.getReceived(),
e.getBlockFor());
// 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)
).one();
if (row != null) {
return OrderResult.success(orderId);
}
return OrderResult.failed(orderId);
}

// WRONG: Non-atomic read-modify-write
public void transferFunds(UUID from, UUID to, BigDecimal amount) {
// Read current balances
Row fromRow = session.execute(
"SELECT balance FROM accounts WHERE account_id = ?", from
).one();
Row toRow = session.execute(
"SELECT balance FROM accounts WHERE account_id = ?", to
).one();
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
session.execute(
"UPDATE accounts SET balance = ? WHERE account_id = ?",
newFromBalance, from
);
session.execute(
"UPDATE accounts SET balance = ? WHERE account_id = ?",
newToBalance, to
);
}
Thread A Thread B
──────── ────────
Read from_balance = 100
Read from_balance = 100
Calculate: 100 - 50 = 50
Calculate: 100 - 30 = 70
Write from_balance = 50
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)
).one();
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
);
if (!rs.wasApplied()) {
// Balance changed - retry or fail
return TransferResult.concurrentModification();
}
// Credit destination (separate LWT if needed)
session.execute(
"UPDATE accounts SET balance = balance + ? WHERE account_id = ?",
amount, to
);
return TransferResult.success();
}
PatternLWT Required?
Last-write-wins updatesNo
Append-only data (logs, events)No
Counter increments (approximate)No
Unique constraint enforcementYes
Balance/inventory checksYes
State machine transitionsYes

// 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"));
}
ProblemImpact
Thread pool exhaustionAll async workers blocked on I/O
Deadlock potentialIf pool is bounded and all threads wait
Latency spikesNew requests queue behind blocked threads
Throughput collapseAsync 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()))
.thenApply(rs -> {
Row row = rs.one();
if (row == null) {
throw new NotFoundException(request.getId());
}
return new Response(row.getString("data"));
})
.exceptionally(e -> {
if (e.getCause() instanceof NotFoundException) {
return Response.notFound();
}
log.error("Query failed for {}", request.getId(), e);
return Response.error();
});
}
// CORRECT: Reactive pagination
public Flux<User> getAllUsers() {
return Flux.from(session.executeReactive(selectAllUsers))
.map(this::mapToUser);
}
// CORRECT: Backpressure-aware processing
public Mono<Long> countActiveUsers() {
return Flux.from(session.executeReactive(selectAllUsers))
.filter(row -> "active".equals(row.getString("status")))
.count();
}

// 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 (" +
productIds.stream()
.map(UUID::toString)
.collect(Collectors.joining(",")) +
")";
return session.execute(cql).all().stream()
.map(this::mapToProduct)
.collect(Collectors.toList());
}
IN Clause SizeCoordinator MemoryConcurrent Sub-queries
10~10 KB10
100~100 KB100
1,000~1 MB1,000
10,000~10 MB10,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) {
int batchSize = 20;
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)));
}
return results;
}
// 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(() -> {
try {
semaphore.acquire();
try {
Row row = session.execute(selectProduct.bind(id)).one();
return row != null ? mapToProduct(row) : null;
} finally {
semaphore.release();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}))
.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
ScenarioResult
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
.build();
// CORRECT: From configuration
CqlSession session = CqlSession.builder()
.withConfigLoader(
DriverConfigLoader.fromClasspath("application.conf")
)
.build();
// application.conf
datastax-java-driver {
basic {
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) {
try {
return session.execute(selectUser.bind(userId)).one();
} catch (Exception e) {
log.error("Query failed", e);
return null; // Hides the real problem
}
}
Exception TypeCorrect ActionGeneric Handler Does
NoNodeAvailableExceptionCircuit breaker, alert opsReturns null
ReadTimeoutExceptionMaybe retryReturns null
InvalidQueryExceptionFix the query (bug)Returns null
AuthenticationExceptionFix credentialsReturns 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) {
try {
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) {
// Other driver errors
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
}
}
ProblemImpact
No prepared statement cacheCQL parsed on every execution
No token-aware routingCoordinator may not route optimally
CQL injection vulnerabilityuser.getName() could contain '); DROP TABLE users; --
String formatting overheadCPU 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(
user.getId(),
user.getName(),
user.getEmail()
));
}
}
// 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(
user.getId(),
user.getName(),
user.getEmail()
)).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(
product.getId(),
product.getName(),
product.getPrice()
));
}
session.execute(batch); // 10,000 statements in one batch!
}

Batches in Cassandra are NOT for bulk operations:

What You ThinkWhat 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 SizeResult
1-10 statementsOK (if same partition)
10-100 statementsWarning signs
100+ statementsTimeouts, 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()))
.build();
session.execute(batch);
}
// CORRECT: Async for bulk operations
public CompletableFuture<Void> importProducts(List<Product> products) {
List<CompletableFuture<AsyncResultSet>> futures = products.stream()
.map(product -> session.executeAsync(insertProduct.bind(
product.getId(),
product.getName(),
product.getPrice()
)).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
item.getProductId(),
item.getQuantity()
));
}
session.execute(batch.build());
}
Use CaseBatch TypeMax Size
Atomic multi-table update (same partition)LOGGED5-10
Bulk insert to same partitionUNLOGGED10-50
Bulk insert to different partitionsNo batch (use async)N/A

// WRONG: No timeout configuration
CqlSession session = CqlSession.builder()
.addContactPoint(new InetSocketAddress("localhost", 9042))
.withLocalDatacenter("dc1")
.build();
// 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))
.build())
.build();
// Per-query timeout override
Statement stmt = selectStmt.bind(userId)
.setTimeout(Duration.ofSeconds(2));
TimeoutRecommendedDescription
REQUEST_TIMEOUT5-10sTotal request time
CONNECTION_CONNECT_TIMEOUT5sTCP connect time
CONNECTION_INIT_QUERY_TIMEOUT5sInitial handshake

// WRONG: LWT for every write
public void saveUser(User user) {
session.execute(
"INSERT INTO users (user_id, name, email) VALUES (?, ?, ?) IF NOT EXISTS",
user.getId(), user.getName(), user.getEmail()
);
}
public void updateUser(User user) {
session.execute(
"UPDATE users SET name = ?, email = ? WHERE user_id = ? IF EXISTS",
user.getName(), user.getEmail(), user.getId()
);
}

LWT uses Paxos consensus:

OperationRegular WriteLWT
Round trips12-4 (varies by Cassandra version and contention)
LatencyTypically 2-5 msTypically 10-50+ ms (varies with contention and topology)
ThroughputHigh (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",
email, userId
);
return rs.wasApplied();
}
// LWT for state transitions
public boolean markOrderShipped(UUID orderId) {
ResultSet rs = session.execute(
"UPDATE orders SET status = 'shipped' WHERE order_id = ? IF status = 'pending'",
orderId
);
return rs.wasApplied();
}
Use CaseLWT Needed?
Unique email registrationYes
Balance check before debitYes
State machine transitionsYes
Last-write-wins updatesNo
Append-only logsNo
Idempotent upsertsNo

SymptomLikely Anti-Pattern
Connection storm in logsSession per request
Duplicate records appearingRetrying non-idempotent writes
Data inconsistency after timeoutIgnoring write timeout
Lost updatesRead-modify-write without LWT
Thread pool exhaustionSync in async context
Coordinator OOMUnbounded IN clauses
Cross-DC latencyMissing local datacenter
All errors look the sameGeneric exception handling
Slow bulk importsUnprepared statements
Batch timeoutsBatch misuse
Hanging requestsMissing timeouts
Slow partition writesLWT overuse