Marcio Cunha

High Frequency Transaction Processing Using Event Sourcing and CQRS in Rust

Learn how to build resilient architectures for high-frequency financial and operational transactions using Rust, Event Sourcing, and CQRS.

Marcio Cunha•5 min
Also available in:EspañolPortuguês
Summary
  • Rust guarantees memory safety without a garbage collector, eliminating unpredictable pauses in high-frequency systems.
  • Event-based storage preserves an immutable history of all transactions, easing audits and rollbacks.
  • Separating read and write operations optimizes database performance under thousands of requests per second.
  • Proper use of asynchronous channels and lightweight thread concurrency prevents I/O bottlenecks.
  • Designing distributed workflows requires explicitly handling eventual consistency and message reprocessing.

The Performance Challenge in High-Frequency Systems

Modern systems handling millions of operations per second, such as stock exchanges or payment processors, face a severe physical and logical dilemma: how to write data securely without losing speed. In practice, this means every click, payment, or transfer must be recorded instantly, without freezing the server and without corrupting users' final balances. When volume grows beyond what traditional databases can handle on a single machine, architecture must shift radically.

Conventional approaches usually update a database row directly. While this works for smaller applications, that strategy creates lock contention when multiple processes try to alter the same record simultaneously. To solve this structural problem, engineers rely on architectural patterns that separate how we record what happened from how we query current state. This is where foundational concepts of distributed design come into play.

Understanding Event Sourcing and Immutable History

Event Sourcing is a technique where instead of storing only the current state of an object, you store every change that occurred as an immutable event. In practice, think of a bank account: rather than keeping just 100 dollars in a balance field, the system holds a list of events like a 150 dollar deposit and a 50 dollar withdrawal. To know the current balance, the system simply sums all historical events in the order they occurred. This ensures complete traceability and removes ambiguities about who changed what and when.

The major operational advantage of this approach is ease of auditing and the ability to travel back in time. If a bug corrupts data, you can recalculate application state by replaying the event history up to the moment before the failure. However, the operational cost is the volume of data generated, which grows continuously, requiring efficient compaction strategies and snapshot creation.

Decoupling Read and Write with CQRS

CQRS stands for Command Query Responsibility Segregation. In practice, the idea splits the application into two separate paths: an exclusive route to receive actions that change data, called commands, and another route optimized purely for fast queries, called reads. In traditional architectures, the same data model serves both inserts and lookups, creating compromises and mutual slowdowns.

By separating these responsibilities, the write database can be highly specialized in recording events sequentially and quickly, while the read database can be duplicated and structured into flat tables or ultra-fast search indexes like Elasticsearch. In practice, this means the screen where a user checks their statement does not compete for processing resources with the engine validating real-time transactions.

Why Choose Rust for Concurrent Processing

Rust has earned critical space in high-performance systems by delivering speed comparable to C and C++ alongside an unprecedented mathematical guarantee of memory safety at compile time. In traditional garbage-collected languages like Java or Go, sporadic processing pauses occur when the system clears unused memory. In high-frequency transactions, those pauses create unwanted latency and loss of critical operational deadlines.

Rust's ownership and borrowing system eliminates the need for a garbage collector and prevents common concurrency bugs like race conditions where two threads try to modify the same memory space simultaneously. In practice, this allows developers to build massively parallel processing pipelines with the peace of mind that the compiler will reject any code prone to segmentation faults or data corruption.

Implementing the Event Core in Code

To illustrate the practical application of these concepts, imagine a simple transaction engine written in Rust that receives deposit commands and generates corresponding events. We use idiomatic data structures and strong typing to ensure invalid states are impossible to represent in code.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum AccountEvent {
    Opened { account_id: String, initial_balance: f64, timestamp: DateTime<Utc> },
    Deposited { account_id: String, amount: f64, timestamp: DateTime<Utc> },
    Withdrawn { account_id: String, amount: f64, timestamp: DateTime<Utc> },
}

#[derive(Debug, Default)]
pub struct AccountAggregate {
    pub account_id: String,
    pub balance: f64,
    pub version: u64,
}

impl AccountAggregate {
    pub fn apply(&mut self, event: &AccountEvent) {
        match event {
            AccountEvent::Opened { account_id, initial_balance, .. } => {
                self.account_id = account_id.clone();
                self.balance = *initial_balance;
                self.version += 1;
            }
            AccountEvent::Deposited { amount, .. } => {
                self.balance += amount;
                self.version += 1;
            }
            AccountEvent::Withdrawn { amount, .. } => {
                self.balance -= amount;
                self.version += 1;
            }
        }
    }
}

The code above demonstrates how the financial aggregate rebuilds its current state by applying events sequentially through the apply method. This pattern ensures balance is always derived purely from past actions, maintaining mathematical integrity without relying on heavy locks in the relational database.

Managing Concurrency and Consistency Guarantees

When multiple events arrive simultaneously for the same account, ensuring order preservation becomes the primary engineering challenge. In distributed systems, networks can delay messages or deliver them out of order. To bypass this, the event engine uses optimistic version numbers or logical partitions based on account identifiers, ensuring events for a single account are processed strictly within the same sequential queue.

This partitioning strategy avoids global contention and allows the system to scale horizontally by adding more processing nodes for different accounts. In practice, the system gains the capacity to absorb massive traffic spikes without breaking transaction consistency for individual accounts involved in operations.

Final Thoughts on High-Performance Architectures

Adopting Event Sourcing and CQRS in Rust requires a profound mindset shift for engineering teams, trading the simplicity of traditional CRUD apps for a message-driven, immutable architecture. Although the initial learning curve is steep, gains in scalability, resilience, and audit clarity amply reward design efforts. Systems built this way survive catastrophic failures and keep performance intact even under extreme load pressures.