CQRS in Rust and Whether It Pays Off for You Compared
CQRS splits the model you write to from the model you read from. See the idea in Rust types, where it helps (different read shapes, scale) and where it only adds moving parts.

CRUD code uses one struct for everything: it is what you validate on write, store, and send to the screen. As an application grows, that struct strains. Writes want strict rules and normalized data; reads want joins and summaries shaped for each page. CQRS gives each side its own model. Everything compiles on Rust 1.90 with edition 2024 and uses only std types in the examples.
The smallest version: two kinds of function
use std::collections::HashMap;
pub type OrderId = u64;
// ---- write side ----
pub struct PlaceOrder {
pub customer: String,
pub items: Vec<(String, u32)>,
}
#[derive(Debug)]
pub enum OrderError {
Empty,
BadQuantity,
}
#[derive(Default)]
pub struct Db {
next_id: OrderId,
orders: HashMap<OrderId, (String, Vec<(String, u32)>)>,
}
impl Db {
pub fn place_order(&mut self, cmd: PlaceOrder) -> Result<OrderId, OrderError> {
if cmd.items.is_empty() {
return Err(OrderError::Empty);
}
if cmd.items.iter().any(|(_, q)| *q == 0) {
return Err(OrderError::BadQuantity);
}
self.next_id += 1;
self.orders.insert(self.next_id, (cmd.customer, cmd.items));
Ok(self.next_id)
}
}
// ---- read side ----
#[derive(Debug)]
pub struct OrderSummary {
pub id: OrderId,
pub customer: String,
pub item_count: u32,
}
impl Db {
pub fn recent_orders(&self, limit: usize) -> Vec<OrderSummary> {
let mut ids: Vec<_> = self.orders.keys().copied().collect();
ids.sort_unstable_by(|a, b| b.cmp(a));
ids.into_iter()
.take(limit)
.map(|id| {
let (customer, items) = &self.orders[&id];
OrderSummary {
id,
customer: customer.clone(),
item_count: items.iter().map(|(_, q)| q).sum(),
}
})
.collect()
}
}PlaceOrder is a command: a request to change state, validated and applied. OrderSummary is a read model: a shape designed for a list page, with a computed item_count. Neither is the stored representation. Handlers on the write side return an id or an error; query functions return plain data and never mutate.
What you gain
- **Write rules stay focused.** The command handler checks invariants and nothing about display.
- **Reads can be as denormalized as the page needs,** without polluting the domain model: a summary type per screen, one query per type.
- **Different scaling.** Reads often outnumber writes by orders of magnitude and can use replicas or caches.
- **Evolution is easier,** because adding a new screen means adding a read model, not changing the aggregate.
A separate read store
At larger scale the read side is a different table, index or database, updated from write-side changes:
command -> write model -> database -> change feed -> projector -> read store
^
query ---+The projector consumes events or change records and updates the read store. Delivery needs to be reliable, hence patterns like the outbox (outbox pattern). The price is **eventual consistency**: right after a command, a query may not show the change yet. The UI has to cope (return the created id and show the item from the response, poll, or read your own write from the primary).
When it does not pay
| Situation | Verdict |
|---|---|
| Forms over tables, reads match writes | plain CRUD |
| Moderate load, one database | plain CRUD, maybe a read function per screen |
| Complex rules plus many different read shapes | CQRS in one database |
| Heavy read load or search needs | separate read store |
| Audit trail or temporal queries | pair with event sourcing |
A lot of "CQRS" benefit comes from the first step only: stop returning the write struct everywhere, and write dedicated query types. Do that before adding buses and stores. Rust helps because types are cheap: a read model per screen is a small struct and a FromRow derive (FromRow).
Organizing the code
src/
commands/ place_order.rs (validate, change state)
queries/ recent_orders.rs (read-only, returns summaries)
domain/ order.rs (invariants)Keep query code free of domain imports and command code free of display concerns. The layering ideas are in hexagonal architecture and clean architecture.


