Skip to content

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

Digital Currency & Token Systems

Digital currencies, whether cryptocurrency, loyalty points, in-game currencies, or corporate tokens, share fundamental requirements: accurate balance tracking, transaction integrity, and prevention of double-spending. While Cassandra is not a blockchain, it provides the building blocks for high-performance token systems that require enterprise-grade reliability without blockchain's limitations.


Understanding the trade-offs helps choose the right technology:

RequirementCassandraBlockchain
Trust modelTrusted operatorTrustless/decentralized
Transaction throughputHigh (workload-dependent)Lower (varies by chain)
Finality latencyLow (sub-second typical)Seconds to minutes
Storage efficiencyOptimizedEvery node stores everything
PrivacyControlled accessPublic (or complex privacy layers)
Regulatory complianceStandard enterpriseComplex, jurisdiction-dependent
Operational complexityStandard database opsSpecialized blockchain ops

Performance Characteristics

Throughput and latency depend heavily on cluster topology, workload, and consistency requirements. Systems requiring double-spend prevention must use application-level concurrency control (LWT or external locking), which affects achievable throughput.

Use Cassandra for:

  • Loyalty programs (airline miles, retail points)
  • In-game virtual currencies
  • Corporate internal tokens
  • Gift card systems
  • Private/permissioned digital assets
  • High-throughput payment systems

Use blockchain for:

  • Trustless systems requiring decentralization
  • Cross-organizational settlement without trusted intermediary
  • Public cryptocurrencies
  • NFTs requiring provenance proof

A token system built on Cassandra consists of several interacting components:

Token System ArchitectureToken System ArchitectureApplication LayerData LayerCassandraWalletServiceTransferServiceMint/BurnQueryServiceTransaction Coordinator(Serialization, Validation, LWT)WalletsTxnLogTokenSupply

-- Primary wallet record
CREATE TABLE wallets (
wallet_id UUID,
owner_id UUID,
wallet_type TEXT, -- USER, MERCHANT, SYSTEM, ESCROW
currency_code TEXT,
balance DECIMAL,
available_balance DECIMAL, -- Balance minus holds
pending_balance DECIMAL, -- Incoming pending transactions
status TEXT, -- ACTIVE, FROZEN, CLOSED
created_at TIMESTAMP,
updated_at TIMESTAMP,
version BIGINT, -- Optimistic locking
PRIMARY KEY (wallet_id)
);
-- Wallet lookup by owner
CREATE TABLE wallets_by_owner (
owner_id UUID,
currency_code TEXT,
wallet_id UUID,
balance DECIMAL,
status TEXT,
PRIMARY KEY ((owner_id), currency_code)
);
-- Balance holds (for pending transactions)
CREATE TABLE balance_holds (
wallet_id UUID,
hold_id UUID,
amount DECIMAL,
reason TEXT,
expires_at TIMESTAMP,
created_at TIMESTAMP,
released_at TIMESTAMP,
PRIMARY KEY ((wallet_id), hold_id)
);
-- Immutable transaction log
CREATE TABLE transactions (
wallet_id UUID,
transaction_id TIMEUUID,
transaction_type TEXT, -- CREDIT, DEBIT, TRANSFER, MINT, BURN
amount DECIMAL,
currency_code TEXT,
balance_before DECIMAL,
balance_after DECIMAL,
counterparty_wallet_id UUID,
reference_id UUID, -- External reference
description TEXT,
metadata MAP<TEXT, TEXT>,
status TEXT, -- PENDING, COMPLETED, FAILED, REVERSED
created_at TIMESTAMP,
PRIMARY KEY ((wallet_id), transaction_id)
) WITH CLUSTERING ORDER BY (transaction_id DESC);
-- Transaction by reference (for idempotency)
CREATE TABLE transactions_by_reference (
reference_id UUID,
wallet_id UUID,
transaction_id TIMEUUID,
status TEXT,
PRIMARY KEY (reference_id)
);
-- Global transaction index (for audit)
CREATE TABLE transactions_global (
date_bucket DATE,
transaction_id TIMEUUID,
from_wallet_id UUID,
to_wallet_id UUID,
amount DECIMAL,
currency_code TEXT,
transaction_type TEXT,
PRIMARY KEY ((date_bucket), transaction_id)
) WITH CLUSTERING ORDER BY (transaction_id DESC);
-- Token supply tracking
CREATE TABLE token_supply (
currency_code TEXT,
total_supply DECIMAL,
circulating_supply DECIMAL,
burned_supply DECIMAL,
max_supply DECIMAL, -- NULL for unlimited
last_mint_at TIMESTAMP,
last_burn_at TIMESTAMP,
PRIMARY KEY (currency_code)
);
-- Supply change log
CREATE TABLE supply_changes (
currency_code TEXT,
change_id TIMEUUID,
change_type TEXT, -- MINT, BURN
amount DECIMAL,
supply_before DECIMAL,
supply_after DECIMAL,
authorized_by UUID,
reason TEXT,
PRIMARY KEY ((currency_code), change_id)
) WITH CLUSTERING ORDER BY (change_id DESC);

The fundamental challenge in any currency system is preventing double-spending: ensuring the same funds cannot be spent twice.

Double-Spend Prevention with Optimistic LockingDouble-Spend Prevention with Optimistic LockingTransferTransferClient AClient BTransferWalletClient AClient AClient BClient BTransferServiceTransferServiceWallet(version=1balance=100)Wallet(version=1balance=100)TransferTransferTransfer $80(ref: txn-A)Load wallet(version=1)balance=100, v=1Transfer $50(ref: txn-B)Load wallet(version=1)balance=100, v=1UPDATE balance=20WHERE version=1Client A's updateapplies firstApplied (v→2)UPDATE balance=50WHERE version=1Client B's updatefails: version mismatchNOT AppliedSuccess(balance: 20)ConcurrentModificationException(retry required)Version check prevents double-spend:- Both clients saw balance=100- Only one update succeeds- Failed client must retry with fresh state
public class WalletService {
public TransferResult transfer(UUID fromWalletId, UUID toWalletId,
BigDecimal amount, UUID referenceId) {
// Idempotency check
Transaction existing = findByReference(referenceId);
if (existing != null) {
return TransferResult.duplicate(existing);
}
// Load wallets with versions
Wallet fromWallet = loadWallet(fromWalletId);
Wallet toWallet = loadWallet(toWalletId);
// Validate
if (fromWallet.getAvailableBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException(fromWalletId, amount);
}
// Calculate new balances
BigDecimal newFromBalance = fromWallet.getBalance().subtract(amount);
BigDecimal newToBalance = toWallet.getBalance().add(amount);
long newFromVersion = fromWallet.getVersion() + 1;
long newToVersion = toWallet.getVersion() + 1;
// Conditional update with version check
ResultSet fromResult = session.execute(
updateWalletBalance.bind(
newFromBalance, newFromBalance, newFromVersion,
fromWalletId, fromWallet.getVersion()
).setSerialConsistencyLevel(ConsistencyLevel.LOCAL_SERIAL)
);
if (!fromResult.wasApplied()) {
throw new ConcurrentModificationException(
"Wallet " + fromWalletId + " was modified concurrently");
}
ResultSet toResult = session.execute(
updateWalletBalance.bind(
newToBalance, newToBalance, newToVersion,
toWalletId, toWallet.getVersion()
).setSerialConsistencyLevel(ConsistencyLevel.LOCAL_SERIAL)
);
if (!toResult.wasApplied()) {
// Rollback from wallet
compensateFromWallet(fromWallet, amount);
throw new ConcurrentModificationException(
"Wallet " + toWalletId + " was modified concurrently");
}
// Record transactions
recordTransactions(fromWallet, toWallet, amount, referenceId,
newFromBalance, newToBalance);
return TransferResult.success(referenceId, amount);
}
}

For safer transfers, use a two-phase approach:

Two-Phase Transfer State MachineTwo-Phase Transfer State MachinePhase 1: HoldHold CreatedAvailable BalanceReducedPending TxnRecordedPhase 2: ExecuteSource Debited(hold converted)DestinationCreditedTransactionCompletedRollbackHold ReleasedAvailable BalanceRestoredTransactionCancelledPhase 1 guarantees fundsare reserved but not moved.Safe to retry or cancel.Phase 2 is idempotent.Can retry on failure.initiateTransfer()completeTransfer()cancelTransfer()Hold expires
public class TwoPhaseTransfer {
public UUID initiateTransfer(UUID fromWalletId, UUID toWalletId,
BigDecimal amount, UUID referenceId) {
// Phase 1: Place hold on source wallet
UUID holdId = createHold(fromWalletId, amount, referenceId);
try {
// Reduce available balance
reduceAvailableBalance(fromWalletId, amount);
// Record pending transaction
Transaction pending = recordPendingTransaction(
fromWalletId, toWalletId, amount, referenceId, holdId);
return pending.getTransactionId();
} catch (Exception e) {
// Release hold on failure
releaseHold(fromWalletId, holdId);
throw e;
}
}
public void completeTransfer(UUID transactionId) {
Transaction pending = loadPendingTransaction(transactionId);
// Phase 2: Execute the transfer
try {
// Debit source (convert hold to actual debit)
debitWallet(pending.getFromWalletId(), pending.getAmount(),
pending.getHoldId());
// Credit destination
creditWallet(pending.getToWalletId(), pending.getAmount());
// Mark transaction complete
markTransactionComplete(transactionId);
} catch (Exception e) {
// Compensation: release hold, restore available balance
compensateFailedTransfer(pending);
markTransactionFailed(transactionId, e.getMessage());
throw e;
}
}
public void cancelTransfer(UUID transactionId) {
Transaction pending = loadPendingTransaction(transactionId);
// Release hold
releaseHold(pending.getFromWalletId(), pending.getHoldId());
// Restore available balance
restoreAvailableBalance(pending.getFromWalletId(), pending.getAmount());
// Mark cancelled
markTransactionCancelled(transactionId);
}
}

Token supply management requires careful control:

Token Supply LifecycleToken Supply LifecycleMint ProcessBurn Process1. Auth Check2. Max SupplyValidation3. CreditTarget Wallet4. UpdateTotal Supply5. Audit Log1. Auth Check2. BalanceValidation3. DebitSource Wallet4. UpdateCirculating Supply5. Audit LogToken Issuer(Authorized)Token Supply- total_supply- circulating_supply- burned_supply- max_supplyInvariants:- total_supply = circulating_supply + burned_supply- total_supply <= max_supply (if set)- circulating_supply = sum(all wallet balances)mint()burn()
public class TokenMintService {
public MintResult mint(String currencyCode, BigDecimal amount,
UUID targetWalletId, UUID authorizedBy, String reason) {
// Authorization check
if (!isAuthorizedMinter(authorizedBy, currencyCode)) {
throw new UnauthorizedMintException(authorizedBy, currencyCode);
}
// Check max supply constraint
TokenSupply supply = getSupply(currencyCode);
if (supply.getMaxSupply() != null &&
supply.getTotalSupply().add(amount).compareTo(supply.getMaxSupply()) > 0) {
throw new MaxSupplyExceededException(currencyCode);
}
UUID mintId = UUID.randomUUID();
// Update supply
updateSupply(currencyCode, supply.getTotalSupply().add(amount),
supply.getCirculatingSupply().add(amount));
// Credit target wallet
creditWallet(targetWalletId, amount, "MINT", mintId);
// Record supply change
recordSupplyChange(currencyCode, "MINT", amount,
supply.getTotalSupply(),
supply.getTotalSupply().add(amount),
authorizedBy, reason);
// Audit
auditService.logEvent(AuditEvent.builder()
.eventType("TOKEN_MINTED")
.resourceType("TOKEN_SUPPLY")
.resourceId(currencyCode)
.action("MINT")
.metadata(Map.of(
"amount", amount.toString(),
"target_wallet", targetWalletId.toString(),
"authorized_by", authorizedBy.toString(),
"reason", reason
))
.build());
return MintResult.success(mintId, amount);
}
}
public class TokenBurnService {
public BurnResult burn(String currencyCode, UUID sourceWalletId,
BigDecimal amount, UUID authorizedBy, String reason) {
// Authorization check
if (!isAuthorizedBurner(authorizedBy, currencyCode)) {
throw new UnauthorizedBurnException(authorizedBy, currencyCode);
}
// Check wallet balance
Wallet wallet = loadWallet(sourceWalletId);
if (wallet.getAvailableBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException(sourceWalletId, amount);
}
UUID burnId = UUID.randomUUID();
// Debit wallet
debitWallet(sourceWalletId, amount, "BURN", burnId);
// Update supply
TokenSupply supply = getSupply(currencyCode);
updateSupply(currencyCode, supply.getTotalSupply(),
supply.getCirculatingSupply().subtract(amount),
supply.getBurnedSupply().add(amount));
// Record supply change
recordSupplyChange(currencyCode, "BURN", amount,
supply.getCirculatingSupply(),
supply.getCirculatingSupply().subtract(amount),
authorizedBy, reason);
return BurnResult.success(burnId, amount);
}
}

Balance Verification and Transaction ProofsBalance Verification and Transaction ProofsBalance VerificationTransaction Proof (Hash Chain)Wallet Recordbalance: $150Transaction HistoryVerificationServiceResult:stored == computed?Txn 1hash: abc123prev: GENESISTxn 2hash: def456prev: abc123Txn 3hash: ghi789prev: def456Transaction Log:+$100 (MINT) → $100+$80 (CREDIT) → $180-$30 (DEBIT) → $150─────────────────────Computed: $150 ✓Hash includes:- transaction_id- wallet_id- amount- balance_after- previous_hash Tampering breaks the chain.stored_balancesum(transactions)chainchain

Prove a balance is correct by summing transactions:

public class BalanceVerification {
public VerificationResult verifyBalance(UUID walletId) {
// Get current balance
Wallet wallet = loadWallet(walletId);
BigDecimal storedBalance = wallet.getBalance();
// Compute balance from transaction history
BigDecimal computedBalance = session.execute(
selectAllTransactions.bind(walletId)
).all().stream()
.map(row -> {
BigDecimal amount = row.getDecimal("amount");
String type = row.getString("transaction_type");
return type.equals("CREDIT") || type.equals("MINT")
? amount : amount.negate();
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
boolean matches = storedBalance.compareTo(computedBalance) == 0;
return new VerificationResult(
walletId, storedBalance, computedBalance, matches,
matches ? null : "Balance mismatch detected"
);
}
}
public class TransactionProof {
public Proof generateProof(UUID walletId, UUID transactionId) {
// Get transaction
Transaction txn = loadTransaction(walletId, transactionId);
// Get surrounding transactions for context
List<Transaction> context = getTransactionContext(
walletId, transactionId, 5);
// Calculate hash chain
String previousHash = context.isEmpty() ?
"GENESIS" : context.get(context.size() - 1).getHash();
String transactionHash = calculateHash(txn, previousHash);
// Verify chain integrity
boolean chainValid = verifyChain(walletId, transactionId);
return Proof.builder()
.walletId(walletId)
.transactionId(transactionId)
.transactionHash(transactionHash)
.previousHash(previousHash)
.chainValid(chainValid)
.timestamp(txn.getCreatedAt())
.balanceAfter(txn.getBalanceAfter())
.build();
}
private String calculateHash(Transaction txn, String previousHash) {
String content = String.join("|",
txn.getTransactionId().toString(),
txn.getWalletId().toString(),
txn.getAmount().toString(),
txn.getTransactionType(),
txn.getBalanceAfter().toString(),
previousHash
);
return DigestUtils.sha256Hex(content);
}
}

Support multiple token types in a single system:

CREATE TABLE currencies (
currency_code TEXT,
currency_name TEXT,
currency_type TEXT, -- POINTS, TOKEN, STABLECOIN
decimals INT,
is_mintable BOOLEAN,
is_burnable BOOLEAN,
is_transferable BOOLEAN,
issuer_id UUID,
created_at TIMESTAMP,
metadata MAP<TEXT, TEXT>,
PRIMARY KEY (currency_code)
);
public class MultiCurrencyWalletService {
public void transfer(UUID fromWalletId, UUID toWalletId,
BigDecimal amount, String currencyCode,
UUID referenceId) {
// Validate currency
Currency currency = currencyRegistry.get(currencyCode);
if (!currency.isTransferable()) {
throw new NonTransferableCurrencyException(currencyCode);
}
// Validate wallets are same currency
Wallet fromWallet = loadWallet(fromWalletId);
Wallet toWallet = loadWallet(toWalletId);
if (!fromWallet.getCurrencyCode().equals(currencyCode) ||
!toWallet.getCurrencyCode().equals(currencyCode)) {
throw new CurrencyMismatchException(
fromWallet.getCurrencyCode(),
toWallet.getCurrencyCode(),
currencyCode
);
}
// Validate decimal places
if (amount.scale() > currency.getDecimals()) {
throw new InvalidAmountPrecisionException(
amount, currency.getDecimals());
}
// Execute transfer
doTransfer(fromWallet, toWallet, amount, referenceId);
}
}

Digital currency systems face regulatory requirements:

Transaction Validation PipelineTransaction Validation PipelineReceive Transfer RequestLimit ValidationCheck Single Transaction LimitReject: "Single limit exceeded"yesAmount > Max Single?Check Daily LimitReject: "Daily limit exceeded"yesDaily Total + Amount > Max Daily?Check Monthly LimitReject: "Monthly limit exceeded"yesMonthly Total + Amount > Max Monthly?Suspicious Activity DetectionAnalyze TransactionFlag: HIGH_VELOCITYyesHigh Velocity?Flag: ROUND_AMOUNTyesRound Amount?Flag: STRUCTURINGyesJust Under Limit?Create SAR ReportAlert Compliance TeamyesAny Flags?Execute TransferRecord Audit Trail
public class TransactionLimitService {
public void validateLimits(UUID walletId, BigDecimal amount,
String transactionType) {
Wallet wallet = loadWallet(walletId);
WalletLimits limits = getLimits(wallet.getWalletType(),
wallet.getCurrencyCode());
// Single transaction limit
if (amount.compareTo(limits.getMaxSingleTransaction()) > 0) {
throw new TransactionLimitExceededException(
"Single transaction limit exceeded");
}
// Daily limit
BigDecimal dailyTotal = getDailyTransactionTotal(walletId);
if (dailyTotal.add(amount).compareTo(limits.getMaxDailyTotal()) > 0) {
throw new TransactionLimitExceededException(
"Daily transaction limit exceeded");
}
// Monthly limit
BigDecimal monthlyTotal = getMonthlyTransactionTotal(walletId);
if (monthlyTotal.add(amount).compareTo(limits.getMaxMonthlyTotal()) > 0) {
throw new TransactionLimitExceededException(
"Monthly transaction limit exceeded");
}
}
}
@Component
public class TransactionMonitor {
@Async
public void analyzeTransaction(Transaction txn) {
List<SuspiciousPattern> patterns = new ArrayList<>();
// High velocity
if (isHighVelocity(txn.getWalletId())) {
patterns.add(SuspiciousPattern.HIGH_VELOCITY);
}
// Round amounts
if (isRoundAmount(txn.getAmount())) {
patterns.add(SuspiciousPattern.ROUND_AMOUNT);
}
// Structuring (just under limits)
if (isStructuring(txn.getWalletId(), txn.getAmount())) {
patterns.add(SuspiciousPattern.STRUCTURING);
}
// Unusual hours
if (isUnusualHours(txn.getCreatedAt())) {
patterns.add(SuspiciousPattern.UNUSUAL_HOURS);
}
if (!patterns.isEmpty()) {
createSuspiciousActivityReport(txn, patterns);
}
}
}

public class HighThroughputTransferService {
private final ExecutorService executor;
private final Semaphore concurrencyLimit;
public CompletableFuture<TransferResult> transferAsync(
TransferRequest request) {
return CompletableFuture.supplyAsync(() -> {
concurrencyLimit.acquire();
try {
return executeTransfer(request);
} finally {
concurrencyLimit.release();
}
}, executor);
}
// Batch transfers for high volume
public List<TransferResult> batchTransfer(List<TransferRequest> requests) {
// Group by source wallet to serialize per-wallet
Map<UUID, List<TransferRequest>> bySource = requests.stream()
.collect(Collectors.groupingBy(TransferRequest::getFromWalletId));
// Process each wallet's transfers sequentially
// but different wallets in parallel
return bySource.entrySet().parallelStream()
.flatMap(entry -> processWalletTransfers(entry.getValue()).stream())
.collect(Collectors.toList());
}
}

Digital currency systems on Cassandra require:

  1. Immutable transaction logs with running balances for verification
  2. Double-spend prevention through optimistic locking or two-phase transfers
  3. Supply management with authorized minting and burning
  4. Multi-currency support with currency-specific validation
  5. Regulatory compliance through limits and monitoring
  6. Audit capabilities with balance verification and transaction proofs

Cassandra provides the scale and reliability needed for production token systems while avoiding blockchain's throughput limitations. The patterns here enable enterprise-grade digital currency without the complexity of decentralized consensus.