use std::collections::HashMap;
use std::sync::Mutex;
pub type AccountId = u64;
pub type Cents = i64;
#[derive(Debug, Clone)]
pub struct Account {
pub id: AccountId,
pub balance: Cents,
}
#[derive(Debug, PartialEq)]
pub enum LedgerError {
UnknownAccount(AccountId),
SameAccount,
InsufficientFunds { account: AccountId, needed: Cents },
Overflow,
Poisoned,
}
pub struct Ledger {
accounts: Mutex<HashMap<AccountId, Account>>,
}
impl Ledger {
pub fn new() -> Self {
Ledger { accounts: Mutex::new(HashMap::new()) }
}
pub fn open(&self, id: AccountId, opening: Cents) -> Result<(), LedgerError> {
let mut guard = self.accounts.lock().map_err(|_| LedgerError::Poisoned)?;
guard.insert(id, Account { id, balance: opening });
Ok(())
}
}
impl Ledger {
pub fn transfer(
&self,
from: AccountId,
to: AccountId,
amount: Cents,
) -> Result<(), LedgerError> {
if from == to {
return Err(LedgerError::SameAccount);
}
let mut guard = self.accounts.lock().map_err(|_| LedgerError::Poisoned)?;
let src_balance = guard
.get(&from)
.ok_or(LedgerError::UnknownAccount(from))?
.balance;
let dst_balance = guard
.get(&to)
.ok_or(LedgerError::UnknownAccount(to))?
.balance;
// Validate everything before applying either side.
let new_src = src_balance
.checked_sub(amount)
.filter(|b| *b >= 0)
.ok_or(LedgerError::InsufficientFunds { account: from, needed: amount })?;
let new_dst = dst_balance
.checked_add(amount)
.ok_or(LedgerError::Overflow)?;
// Both mutations happen under the same held guard: atomic.
guard.get_mut(&from).unwrap().balance = new_src;
guard.get_mut(&to).unwrap().balance = new_dst;
Ok(())
}
pub fn balance(&self, id: AccountId) -> Result<Cents, LedgerError> {
let guard = self.accounts.lock().map_err(|_| LedgerError::Poisoned)?;
guard.get(&id).map(|a| a.balance).ok_or(LedgerError::UnknownAccount(id))
}
pub fn total(&self) -> Result<Cents, LedgerError> {
let guard = self.accounts.lock().map_err(|_| LedgerError::Poisoned)?;
Ok(guard.values().map(|a| a.balance).sum())
}
}
use std::sync::Arc;
use std::thread;
#[test]
fn conservation_holds_under_contention() {
let ledger = Arc::new(Ledger::new());
ledger.open(1, 100_000).unwrap();
ledger.open(2, 100_000).unwrap();
let starting_total = ledger.total().unwrap();
let mut handles = Vec::new();
for i in 0..16 {
let l = Arc::clone(&ledger);
handles.push(thread::spawn(move || {
let (from, to) = if i % 2 == 0 { (1, 2) } else { (2, 1) };
for _ in 0..1_000 {
// Some of these legitimately fail with InsufficientFunds;
// the invariant must still hold regardless.
let _ = l.transfer(from, to, 37);
}
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(ledger.total().unwrap(), starting_total);
assert!(ledger.balance(1).unwrap() >= 0);
assert!(ledger.balance(2).unwrap() >= 0);
}
This snippet implements a small double-entry ledger where money moves between accounts atomically, entirely in memory. The core idea in accounting is that every transfer touches at least two accounts and the sum of all balances must never change — a debit on one side is matched by a credit on the other. The challenge in a concurrent program is making that pair of mutations appear indivisible: no other thread may observe a state where the debit landed but the credit did not, and a failed transfer must leave both accounts exactly as they were.
In the Ledger types tab, Account holds a balance in integer minor units (cents), which avoids the rounding pitfalls of floating-point money. The whole account map lives behind a single Mutex<HashMap<AccountId, Account>> inside Ledger. Guarding the entire map with one lock is a deliberate trade-off: it serializes all transfers, so throughput is limited, but it makes atomicity trivial because no partial state is ever visible while the guard is held. Per-account locks would scale better but reintroduce lock-ordering and deadlock concerns that this design sidesteps.
The Transfer logic tab contains transfer, the heart of the module. It locks once, resolves both accounts, and crucially performs all validation — existence checks, the same-account guard, and the InsufficientFunds check via checked_sub — BEFORE mutating anything. This validate-then-apply ordering is what gives the operation all-or-nothing semantics: if any check fails it returns an Err and no balance has changed. Because debit and credit happen under the same held guard, the pair is effectively one atomic step. checked_add also guards against overflow on the credit side.
Note the .lock() call uses map_err to convert a poisoned mutex into a domain error rather than panicking. A Mutex becomes poisoned if a thread panics while holding it; recovering explicitly keeps one bad transfer from taking down every future one.
The Concurrent test tab spawns many threads hammering transfer in both directions and then asserts the total balance is unchanged. This is the real proof of correctness for a ledger: individual transfers may fail with InsufficientFunds, but the conservation invariant holds regardless of interleaving. Reach for this pattern when correctness and auditability matter more than raw parallel throughput.
Related snips
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
package pools
import (
"bytes"
"sync"
)
sync.Pool for bytes.Buffer to reduce allocations in hot paths
class CreateDeadJobs < ActiveRecord::Migration[7.1]
def change
create_table :dead_jobs do |t|
t.string :jid, null: false
t.string :queue, null: false
t.string :klass, null: false
Background Job Dead Letter Queue (DLQ) Table
Share this code
Here's the card — post it anywhere.