Beankeeper
Idiomatic, ergonomic library of primitives for professional double-entry accounting in Rust.
Beankeeper enforces the fundamental accounting equation at the type level: every posted transaction guarantees that total debits equal total credits. Unbalanced transactions cannot exist.
Features
- Correctness by construction -- the builder pattern validates transactions at post time, rejecting unbalanced entries, zero amounts, and mixed currencies
- Exact arithmetic -- all monetary values use
i128minor-unit representation (cents, pence, yen), eliminating floating-point rounding errors - Multi-currency support -- ISO 4217 currencies with correct minor-unit precision (USD 2 decimals, JPY 0 decimals, BHD 3 decimals)
- Zero dependencies -- no external crates;
unsafecode is forbidden via#[deny(unsafe_code)] - Comprehensive reporting -- generate trial balances, query account balances, and filter by account type
Quick Start
use *;
// Define accounts
let cash = new;
let revenue = new;
// Build and post a balanced transaction
let txn = new
.debit
.unwrap
.credit
.unwrap
.post
.unwrap;
assert_eq!;
Installation
Add beankeeper to your project:
Minimum supported Rust version: 1.85 (Rust 2024 edition).
Core Concepts
Beankeeper models the complete double-entry bookkeeping cycle: define accounts, record journal entries, post transactions to a ledger, and generate reports. Each stage builds on the previous one, and the library validates data at every boundary.
The Accounting Equation
Every transaction enforces the fundamental equation:
Assets + Expenses = Liabilities + Equity + Revenue
(Debit normal) (Credit normal)
The five account types each have a normal balance direction. Debiting an asset increases it; crediting a revenue account increases it. Beankeeper encodes these rules so that signed_amount() on any entry returns a positive value when the entry increases the account and a negative value when it decreases.
Accounts
An Account combines three elements: a validated AccountCode, a human-readable name, and an AccountType that determines its normal balance behavior.
use *;
let cash = new;
// Account codes support hierarchical numbering
let parent = new.unwrap;
let child = new.unwrap;
assert!;
Account codes accept digits, hyphens, and dots, enabling standard chart-of-accounts numbering schemes like 1000, 1000.10, or 1-1000.
Transactions
The JournalEntry builder accumulates debit and credit entries, then validates them when you call post(). Validation enforces three rules: at least two entries, a single currency, and balanced totals.
use *;
let cash = new;
let revenue = new;
let tax_payable = new;
// Multi-leg transaction: $108 sale with 8% tax
let txn = new
.debit
.unwrap
.credit
.unwrap
.credit
.unwrap
.post
.unwrap;
assert_eq!;
Transactions can carry optional metadata for reference numbers, invoice IDs, or other tracking information:
use *;
let cash = new;
let revenue = new;
let txn = new
.with_metadata
.debit
.unwrap
.credit
.unwrap
.post
.unwrap;
assert_eq!;
The General Ledger
The Ledger is an append-only store for posted transactions, following standard accounting practice where corrections are made via reversing entries rather than deletion. It provides balance queries across all posted transactions.
use *;
let mut ledger = new;
let cash = new;
let revenue = new;
let rent = new;
// Post a sale
let sale = new
.debit
.unwrap
.credit
.unwrap
.post
.unwrap;
ledger.post;
// Pay rent
let payment = new
.debit
.unwrap
.credit
.unwrap
.post
.unwrap;
ledger.post;
// Query balances
let cash_balance = ledger.balance_for.unwrap;
assert_eq!; // 1000 - 500
assert!;
Reporting
The TrialBalance report lists all accounts with their debit and credit totals, serving as a basic integrity check on the ledger.
use *;
let mut ledger = new;
let cash = new;
let revenue = new;
let txn = new
.debit
.unwrap
.credit
.unwrap
.post
.unwrap;
ledger.post;
let tb = ledger.trial_balance.unwrap;
assert!;
assert_eq!;
// Filter by account type
let assets = tb.accounts_by_type;
assert_eq!;
Design Principles
Why Integer Arithmetic
Financial calculations require exact results. Floating-point types (f64) introduce rounding errors that compound across thousands of transactions. Beankeeper stores all monetary values as i128 counts of minor currency units (cents for USD, pence for GBP, yen for JPY). This ensures:
- Deterministic results across platforms
- No rounding surprises
- Correct behavior for all standard currencies, including those with 0 or 3 decimal places
Why the Builder Pattern
The JournalEntry builder separates construction from validation. You accumulate entries freely, then post() performs all validation at once. This design prevents partially-constructed transactions from entering the ledger and makes the API hard to misuse: a Transaction value proves that balance was checked.
Why Append-Only
The Ledger does not support deleting or modifying posted transactions. In professional accounting, corrections are recorded as new reversing entries. This preserves a complete audit trail and matches real-world accounting practice.
Multi-Currency Support
Beankeeper includes nine ISO 4217 currencies with correct minor-unit precision:
| Currency | Code | Minor Units |
|---|---|---|
| US Dollar | USD |
2 (cents) |
| Euro | EUR |
2 (cents) |
| British Pound | GBP |
2 (pence) |
| Japanese Yen | JPY |
0 |
| Swiss Franc | CHF |
2 |
| Canadian Dollar | CAD |
2 |
| Australian Dollar | AUD |
2 |
| Bahraini Dinar | BHD |
3 |
| Kuwaiti Dinar | KWD |
3 |
Arithmetic between different currencies is rejected at the type level. A transaction must use a single currency; attempting to mix USD and EUR entries produces a TransactionError::CurrencyMismatch.
use *;
// JPY has no minor units
let amount = jpy;
assert_eq!;
// EUR uses cents
let amount = eur;
assert_eq!;
Error Handling
All fallible operations return Result types with descriptive error variants. The top-level BeanError enum aggregates all domain errors, enabling ergonomic use of the ? operator:
use *;
Specific error types cover each domain:
- Transaction errors --
Unbalanced,CurrencyMismatch,NoEntries,SingleEntry - Entry errors --
ZeroAmount,NegativeAmount - Account code errors --
Empty,InvalidCharacter - Money errors --
CurrencyMismatch,Overflow - Currency errors --
InvalidCode,UnknownCode
Safety and Quality
Beankeeper applies strict quality standards:
#[deny(unsafe_code)]-- no unsafe Rust anywhere in the crate#[deny(clippy::unwrap_used)]and#[deny(clippy::expect_used)]-- fallible operations always use proper error handling#[warn(clippy::pedantic)]-- pedantic linting enabled- Zero external dependencies
- 204 tests covering unit, integration, and real-world accounting scenarios
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.