Skip to content

mateocampagna/bank-transfer-simulation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bank-transaction-simulation

A concurrent bank transaction simulator written in Rust. Models a pull-based debit system where services (Netflix, YouTube, etc.) automatically charge a client account. Multiple charges can run simultaneously, so shared state must be protected against race conditions.

Built as a hands-on exercise to learn threads, shared state, and concurrent programming in Rust.


What it does

  • Models bank accounts with debit and credit operations
  • Registers and manages accounts through a central Bank struct
  • Simulates automatic subscription charges running concurrently on a shared account
  • Protects shared state using Arc<Mutex<T>>
  • Prevents deadlocks by enforcing a consistent lock ordering based on account ID
  • Returns typed errors for domain failures (insufficient funds, invalid amount, account not found) and concurrency failures (lock poisoning)

Project structure

src/
  main.rs
  bank/
    mod.rs
    account.rs      # Account struct, debit/credit logic, AccountError
    transfer.rs     # Transfer struct, execute logic, TransferError
    bank.rs         # Bank struct — account registry and transfer orchestration

Concepts learned

Concurrency

  • thread::spawn and thread::join for launching and synchronizing threads
  • Arc<T> — atomically reference-counted pointer for shared ownership across threads
  • Mutex<T> — mutual exclusion lock for safe mutable access to shared data
  • MutexGuard<T> and RAII — the lock is held as long as the guard is in scope, released automatically on drop
  • Race conditions — what they are and how Rust prevents data races at compile time
  • Deadlock — how it occurs when two threads acquire the same locks in opposite order, and how to prevent it by enforcing a canonical lock order

Rust

  • Arc<Mutex<T>> as the standard pattern for shared mutable state across threads
  • Result<T, E> for error handling and the ? operator for propagation
  • From<E> trait for automatic error type conversion between layers
  • Enums as typed errors (AccountError, TransferError, BankError)
  • Deref — how MutexGuard<T> transparently exposes the inner type
  • move closures — required when passing owned data into threads
  • Module system and visibility (pub, pub(crate), private fields)

Design

  • Separation of concerns: each struct has one job and no knowledge of the others
  • Repository pattern: Bank is the only entry point to account data — internal storage is an implementation detail
  • Command pattern: Transfer encapsulates the intent of an operation and exposes execute()
  • Domain invariants enforced at construction (Transfer::new returns Result and rejects invalid state before the struct is created)
  • Typed errors that carry context (AccountNotFound(u64), DuplicateAccount(u64))

Lock ordering and deadlock prevention

When two transfers involving the same accounts run concurrently in opposite directions, naive lock acquisition causes deadlock:

Thread A locks account 1, waits for account 2
Thread B locks account 2, waits for account 1
→ both wait forever

The solution: always acquire locks in ascending order by account ID, regardless of which is from and which is to. This guarantees no circular wait is possible.

if from_id < to_id {
    (Arc::clone(&self.from_account), Arc::clone(&self.to_account))
} else {
    (Arc::clone(&self.to_account), Arc::clone(&self.from_account))
}

Running

cargo run
cargo test
cargo test -- --nocapture   # show println output inside tests

Stack

  • Language: Rust
  • Standard library only — no external crates

About

Concurrent bank transfer simulation in Rust. Threads, mutexes, and deadlock prevention.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages