Accounting might seem like an unusual subject for a systems programming project. But implementing a double-entry ledger turns out to be a useful exercise in data modeling, transactional storage, and enforcing invariants.
I've been exploring these ideas while developing libledger, a small accounting library written in C and backed by SQLite.
The central invariant
In double-entry bookkeeping, a journal entry contains two or more postings, and the total debits must equal the total credits.
For example, when a customer pays an invoice for $100:
| Account | Debit | Credit |
|---|---|---|
| Cash | $100 | |
| Accounts Receivable | $100 | |
| Total | $100 | $100 |
The important part is not the accounting terminology. It is the invariant that every valid entry must preserve.
This gives us a concrete problem for software design: how do we ensure that invalid transactions can never become part of the committed accounting history?
Money is not a floating-point number
An accounting system needs exact representations of monetary values.
In libledger, amounts are represented as 64-bit integers in the smallest unit of their currency, with overflow checks. This avoids introducing binary floating-point rounding errors into accounting calculations.
Multi-currency accounting adds another important constraint: entries must balance independently for each currency.
Currency conversion is a separate concern from the original transaction history.
SQLite as a transactional boundary
A journal entry may contain multiple postings, but those postings represent one logical operation.
We cannot safely write half an entry and hope to repair it later.
In this implementation, validation precedes persistence, and SQLite transactions provide atomicity. The use of BEGIN IMMEDIATE establishes a transactional boundary for updating the relevant accounting records.
The database should contain either the complete valid operation or none of it.
Checking more than the write path
Validation during normal operations is necessary, but not sufficient.
Database files may also be modified outside the application, including through manual SQL operations.
For that reason, libledger includes consistency checks intended to detect discrepancies between accounting history and stored balances.
This distinction matters: preventing inconsistent writes and detecting existing inconsistencies are separate responsibilities.
Why C?
C makes many implementation choices explicit: integer ranges, memory ownership, error handling, and the relationship between the library and its persistence layer.
For this particular project, I wanted a small implementation whose behavior could be examined directly.
The library is accompanied by a command-line interface, which also makes it possible to test accounting behavior independently of a graphical interface.
A literate implementation
One additional experiment is the development process itself.
The source code is extracted from a literate programming manuscript rather than maintained separately from its explanation.
The build also executes selected shell sessions from the manuscript and verifies that their outputs match the documented results.
This is an attempt to reduce the gap between what the documentation says and what the software actually does.
You can explore the full source code and follow the project on GitHub:
libledger
/
libledger
A double-entry accounting library in C and SQLite, built through literate programming. Includes a CLI, accounting consistency checks, multi-currency support, and MCP integration for AI assistants.
libledger
A small double-entry accounting library in C, backed by SQLite.
libledger is the companion implementation to Double-Entry Accounting: A Literate Programming Implementation, a book about building an accounting system from first principles using literate programming.
The project includes a C library, a ledger command-line application, and an MCP (Model Context Protocol) server that lets AI assistants interact with the books.
The book is the source. The C implementation is extracted from the book's
nowebsource rather than maintained separately from its explanation.
Features
- Double-entry bookkeeping: journal entries are checked for balanced debits and credits, separately for each currency.
- SQLite storage: multiple sets of books can be stored in a single database file.
- Hierarchical chart of accounts: accounts are organized using dotted codes; postings go to leaf accounts.
- Exact money representation: amounts are stored as integers in each currency's smallest unit, not floating-point values.
- Atomic posting: entries are…
Questions worth discussing
Building an accounting engine raises some interesting engineering questions:
- Which invariants belong in application code, and which should also be enforced by the database?
- How should an application detect inconsistencies introduced by external database modifications?
- What is the most understandable way to represent exact monetary amounts across currencies?
- Can executable documentation make a small systems project easier to maintain?
These questions are interesting well beyond accounting software.
What edge cases did I miss here? How do you approach data integrity and invariants in your systems projects? Let me know in the comments below!
Top comments (0)