Skip to content

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

Cassandra CQL Triggers

Triggers execute custom server-side code in response to data mutations (INSERT, UPDATE, DELETE). Triggers are implemented as Java classes and deployed to each node in the cluster.

Limited Use Cases

Triggers add operational complexity and are rarely used in production. Consider alternatives:

  • Change Data Capture (CDC) - For event streaming and audit logging
  • Materialized Views - For denormalized query tables
  • Application logic - For business rules validation

  • Triggers execute on the coordinator node before the mutation is applied to replicas
  • Trigger code runs in the same thread as the mutation processing
  • Mutations generated by triggers are combined with the original mutation (not atomic across partitions or tables)
  • Trigger exceptions cause the entire mutation to be rejected
  • IF NOT EXISTS provides idempotent trigger creation
  • Triggers are invoked for mutations in batches as well

Undefined Behavior

The following behaviors are undefined and must not be relied upon:

  • Execution order: When multiple triggers are attached to a table, execution order is undefined
  • Idempotency: Triggers may execute multiple times for retried mutations
  • Cross-node consistency: Trigger JAR versions may differ between nodes during rolling deployments
  • Performance isolation: Slow triggers block mutation processing and can cause timeouts
  • Resource limits: Triggers run without memory or CPU isolation; misbehaving triggers affect cluster stability
  • Distributed transactions: Triggers cannot participate in cross-partition or cross-table transactions
AspectGuarantee
Execution timingBefore mutation is applied to memtable
Execution locationCoordinator node only
Thread contextSame thread as mutation processing
Exception handlingException aborts the entire mutation
Return valueCollection of additional mutations to apply
RequirementDescription
JAR location$CASSANDRA_HOME/conf/triggers/ directory
Class loadingLoaded on first trigger invocation
Node coverageJAR must be deployed to all nodes
Hot reloadSupported via nodetool reloadtriggers
Failure ModeOutcomeClient Action
Trigger throws exceptionMutation rejectedFix trigger code or data
Trigger JAR missing on coordinatorMutation failsDeploy JAR to all nodes
Trigger returns invalid mutationUndefined behaviorFix trigger implementation
Trigger timeout (blocking)Mutation times outOptimize trigger or increase timeout
VersionBehavior
2.0+Basic trigger support (CASSANDRA-1311)
2.1+Improved trigger lifecycle
3.0+Better error messages for trigger failures

AspectDescription
ExecutionRuns on the coordinator node for each mutation
ImplementationJava class implementing ITrigger interface
DeploymentJAR file in triggers/ directory on each node
ScopePer-table configuration
TimingExecutes before the mutation is applied
Trigger execution during a mutation on the coordinatorClientCoordinatorTriggerReplicasClientClientCoordinatorCoordinatorTriggerTriggerReplicasReplicasMutation(INSERT/UPDATE/DELETE)Execute triggerAdditional mutations(or empty)Combine original +trigger mutationsReplicate combinedmutationsAcknowledgmentSuccess

Triggers can:

  • Validate data before write
  • Generate additional mutations (e.g., audit records)
  • Transform data before storage
  • Reject mutations by throwing exceptions

Attach a trigger to a table.

CREATE TRIGGER [ IF NOT EXISTS ] trigger_name
ON [keyspace_name.]table_name
USING 'fully.qualified.ClassName'
ParameterDescription
trigger_nameIdentifier for the trigger
table_nameTable to attach trigger to
ClassNameFully qualified Java class name
-- Create audit trigger
CREATE TRIGGER audit_trigger
ON users
USING 'com.example.triggers.AuditTrigger';
-- With keyspace qualification
CREATE TRIGGER validation_trigger
ON production.orders
USING 'com.example.triggers.OrderValidationTrigger';
-- Idempotent creation
CREATE TRIGGER IF NOT EXISTS sync_trigger
ON products
USING 'com.example.triggers.ProductSyncTrigger';
  • Superuser privileges required
  • Trigger class must be deployed to all nodes
  • Table must exist

Remove a trigger from a table.

DROP TRIGGER [ IF EXISTS ] trigger_name
ON [keyspace_name.]table_name
-- Drop trigger
DROP TRIGGER audit_trigger ON users;
-- Idempotent drop
DROP TRIGGER IF EXISTS old_trigger ON orders;

Triggers implement the org.apache.cassandra.triggers.ITrigger interface.

package org.apache.cassandra.triggers;
import java.util.Collection;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.partitions.Partition;
public interface ITrigger {
/**
* Called for each partition affected by a mutation.
*
* @param partition The partition data being mutated
* @return Additional mutations to apply, or null/empty
*/
Collection<Mutation> augment(Partition partition);
}
package com.example.triggers;
import java.util.Collection;
import java.util.Collections;
import java.nio.ByteBuffer;
import java.util.UUID;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.partitions.Partition;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.triggers.ITrigger;
public class AuditTrigger implements ITrigger {
@Override
public Collection<Mutation> augment(Partition partition) {
// Get audit table metadata
TableMetadata auditTable = Schema.instance
.getTableMetadata("audit_ks", "audit_log");
if (auditTable == null) {
return Collections.emptyList();
}
// Create audit record mutation
// Implementation details depend on schema
return Collections.emptyList(); // Return mutations or empty
}
}
package com.example.triggers;
import java.util.Collection;
import java.util.Collections;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.partitions.Partition;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.Unfiltered;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.triggers.ITrigger;
public class ValidationTrigger implements ITrigger {
@Override
public Collection<Mutation> augment(Partition partition) {
try (UnfilteredRowIterator iterator =
partition.unfilteredIterator()) {
while (iterator.hasNext()) {
Unfiltered unfiltered = iterator.next();
if (unfiltered instanceof Row) {
Row row = (Row) unfiltered;
// Validate row data
// Throw exception to reject mutation
}
}
}
return Collections.emptyList();
}
}

Terminal window
# Compile with Cassandra dependencies
javac -cp "/path/to/cassandra/lib/*" \
-d target/classes \
src/com/example/triggers/*.java
# Package as JAR
jar cf my-triggers.jar -C target/classes .

Deploy the JAR to each Cassandra node:

Terminal window
# Copy to triggers directory
cp my-triggers.jar $CASSANDRA_HOME/conf/triggers/
# Restart not required - triggers are loaded dynamically
# But a restart ensures clean class loading

The triggers/ directory location:

InstallationPath
Tarball$CASSANDRA_HOME/conf/triggers/
Package/etc/cassandra/triggers/
Docker/etc/cassandra/triggers/
-- List triggers on a table
DESCRIBE TABLE my_keyspace.my_table;
-- Query system schema
SELECT * FROM system_schema.triggers
WHERE keyspace_name = 'my_keyspace'
AND table_name = 'my_table';

FactorImpact
Execution overheadAdds latency to every mutation
Additional mutationsIncreases write amplification
Synchronous executionBlocks until trigger completes
No parallelismSingle-threaded per partition
LimitationDescription
No BATCH supportTriggers fire per-statement, not per-batch
Coordinator onlyExecutes only on coordinator node
No read accessCannot query other tables
Class loadingRequires JAR on all nodes
No hot reloadClass changes require restart
Limited error handlingExceptions fail the mutation

Trigger Guidelines

  1. Keep triggers lightweight - Avoid complex logic
  2. Handle exceptions - Uncaught exceptions fail mutations
  3. Test thoroughly - Triggers affect all mutations
  4. Monitor performance - Track trigger execution time
  5. Version control - Manage trigger JARs with deployment tools
  6. Document - Maintain clear documentation of trigger behavior
Use CaseAlternative
Audit loggingChange Data Capture (CDC)
DenormalizationMaterialized Views
ValidationApplication-layer checks
Event streamingCDC + Kafka/Pulsar
Derived dataApplication writes to multiple tables

Trigger metadata is stored in system_schema.triggers:

SELECT * FROM system_schema.triggers;
ColumnDescription
keyspace_nameKeyspace containing the table
table_nameTable with the trigger
trigger_nameTrigger identifier
optionsTrigger class and configuration

  1. Verify trigger is attached:

    SELECT * FROM system_schema.triggers
    WHERE keyspace_name = 'ks' AND table_name = 'tbl';
  2. Check JAR is deployed on all nodes

  3. Verify class name is correct and fully qualified

ERROR - Failed to execute trigger: ClassNotFoundException
  • Ensure JAR is in triggers/ directory on the coordinator node
  • Verify fully qualified class name matches
ERROR - Trigger execution failed: [exception message]
  • Check Cassandra logs for stack trace
  • Review trigger implementation for bugs
  • Ensure returned mutations are valid

TopicDescription
DDL OverviewSchema definition commands
TableTable creation and modification
Materialized ViewsAutomatic denormalization