A small, self-contained event-sourced ledger implemented in Rust using Axum.
This project demonstrates:
- clean domain modelling
- value objects (
Money,Currency) - append-only event sourcing
- derived state (balance replay)
- API boundary separated from domain logic
- error handling and invariants
- idiomatic Rust with Clippy compliance
- Create new accounts
- Deposit funds
- Withdraw funds (no overdraft allowed)
- Fetch current balance (derived from events)
- View full audit trail (account events)
Everything is in-memory and append-only.
- Rust
- Axum
- Tokio
- Serde
A logical balance-holding entity identified by a UUID.
Accounts do not store a numeric balance. They are reconstructed entirely from events.
Immutable record of a business fact.
Current event types:
ACCOUNT_OPENEDDEPOSITWITHDRAWAL
Every event has:
id— unique UUIDaccount_idcreated_atpayload(event type + data)
Events are append-only and never mutated.
A value object representing an amount in minor units (e.g. pence) with a currency.
Money can never be negative. Arithmetic uses checked operations to enforce safety.
Balances are derived by replaying events.
Withdrawals require sufficient funds.
flowchart LR
Client[Client / API Consumer] -->|HTTP| API
subgraph LedgerService["Mini Ledger Service"]
API[Axum HTTP Layer]
Domain["Domain Layer (Ledger, Events, Money, Invariants)"]
Memory[(In-Memory Event Store)]
end
API --> Domain
Domain --> Memory
flowchart TD
subgraph API["HTTP API (Axum)"]
A1[POST /accounts]
A2[POST /accounts/:id/deposit]
A3[POST /accounts/:id/withdraw]
A4[GET /accounts/:id/balance]
A5[GET /accounts/:id/events]
end
subgraph DOMAIN["Domain Layer"]
D1["Ledger (event-sourced)"]
D2["Money & Currency (value objects)"]
D3["LedgerEvent (append-only)"]
D4["Balance Replay (derived state)"]
end
A1 --> D1
A2 --> D1
A3 --> D1
A4 --> D4
A5 --> D3
D1 --> D3
D1 --> D4
sequenceDiagram
participant C as Client
participant API as Axum Handler
participant L as Ledger
participant E as Events (in-memory)
C->>API: POST /accounts/:id/deposit
API->>L: ledger.deposit(account, amount)
L->>E: append LedgerEvent::Deposit
API-->>C: 201 Created
Requires Rust 1.91+.
cargo runThen in another terminal:
curl -X POST http://localhost:3000/accountsOr use Insomnia / Postman.
cargo testAll endpoints return JSON.
Liveness / readiness probe.
Response:
{ "status": "ok" }Create a new account.
Response:
{ "id": "..." }Deposit money into an account.
Request:
{
"amount_minor": 1000,
"currency": "GBP"
}Response:
{
"id": "...",
"account_id": "...",
"amount_minor": 1000,
"currency": "GBP"
}Withdraw money, enforcing no overdraft.
Request:
{
"amount_minor": 300,
"currency": "GBP"
}Response:
{
"id": "...",
"account_id": "...",
"amount_minor": 300,
"currency": "GBP"
}Insufficient funds example:
{ "error": "Insufficient funds" }Return the derived balance for the account.
Response:
{
"account_id": "...",
"amount_minor": 700,
"currency": "GBP",
"display": "£7.00"
}Return the full event stream (audit trail) for the account.
Example:
[
{
"id": "...",
"account_id": "...",
"created_at": "...",
"payload": { "type": "ACCOUNT_OPENED" }
},
{
"id": "...",
"account_id": "...",
"created_at": "...",
"payload": {
"type": "DEPOSIT",
"amount_minor": 1000,
"currency": "GBP"
}
}
]Written as a compact example to demonstrate:
- event sourcing
- invariants and safety
- a small but extensible example of a webserver in rust
- clean Rust API design
- separation of domain and transport
- stable and auditable money handling
These would be natural next steps but are not included in the current minimal version:
- Persistent event store (Postgres, SQLite, EventStoreDB)
- Transfers (TRANSFER_DEBIT / TRANSFER_CREDIT)
- Idempotency keys
- Multi-currency support
- OpenAPI documentation
- Replay performance optimisations