Event-sourced double-entry ledger core in Go.
ledgercore is a small Go package that records financial movement as an append-only log of events and projects account balances from that log. It enforces five correctness invariants at write time: balanced postings, idempotency in TransactionID, strictly monotonic event ordering, configurable negative-balance prevention, and single-currency isolation per transaction. The package has zero third-party dependencies; go.mod is one line.
package main
import (
"fmt"
"log"
"github.com/xvica-ltd/ledgercore"
)
func main() {
l, err := ledgercore.New(ledgercore.NewInMemoryStore())
if err != nil { log.Fatal(err) }
if err := l.OpenAccount("cash:gbp", ledgercore.AccountAsset, "GBP"); err != nil { log.Fatal(err) }
if err := l.OpenAccount("sales:gbp", ledgercore.AccountRevenue, "GBP"); err != nil { log.Fatal(err) }
if _, err := l.Post(ledgercore.Transaction{
ID: "sale-1",
Entries: []ledgercore.Entry{
{Account: "cash:gbp", Amount: ledgercore.NewMoney(4250, "GBP")},
{Account: "sales:gbp", Amount: ledgercore.NewMoney(-4250, "GBP")},
},
}); err != nil { log.Fatal(err) }
cash, _ := l.Balance("cash:gbp")
sales, _ := l.Balance("sales:gbp")
fmt.Println(cash, sales)
}- Balanced postings: debits and credits sum to zero in every transaction.
TestInvariant_DoubleEntryBalance_RejectsUnbalancedTransaction - Idempotency: a duplicate
TransactionIDproduces one event and returns the originalEventIDwithErrIdempotentReplay.TestInvariant_Idempotency_DuplicateTransactionIDProducesOneEvent - Monotonic ordering:
EventIDis strictly increasing and replay is deterministic.TestInvariant_MonotonicOrdering_ReplayIsDeterministic - Negative-balance prevention: accounts with
AllowNegative=falsecannot be driven below zero; off by default for assets.TestInvariant_NegativeBalance_AssetAccountRejectsOverdraw - Currency isolation: every entry in a transaction shares one currency that matches the account it touches.
TestInvariant_CurrencyIsolation_RejectsMixedCurrencyTransaction
- Money is
int64minor units. No floats, nobig.Float, no rounding modes to argue about. - Idempotency is keyed by caller-supplied
TransactionID. Retries under network failure do not double-post. Clockis an interface injected at construction. The ledger never callstime.Nowdirectly, so tests are deterministic.- The
Ledgeris safe for concurrent use. The validate-append-apply sequence runs under one write lock so the event store and the in-memory projection cannot diverge. - Every failure mode is a named exported sentinel in
errors.go;errors.Isworks for every one.
- No persistence. The reference
EventStoreis in-memory; durable backends are the caller's responsibility behind theEventStoreinterface. - No HTTP or gRPC surface. This is a library, not a service.
- No multi-currency FX. Cross-currency movement must be modelled as an explicit FX transaction by the caller.
- No multi-tenancy. One
Ledgeris one set of accounts; namespacing across tenants is out of scope. - No regulatory reporting or reconciliation. The package exposes the primitives; report generation belongs in the layer above.
Use it when you need a small, auditable correctness core for financial movement and you are willing to wire your own persistence, transport, and reporting around it. The five invariants are the part most teams underestimate, and they are what this package addresses.
Do not use it as a drop-in ledger for an existing service. There is no migration story, no schema versioning, no admin UI, and no operational tooling. Those are concerns the caller owns.
A longer write-up of the design and the trade-offs behind each invariant lives in docs/design.md, with a companion post forthcoming on xvica.com.
ledgercore is maintained by XVICA, a UK
infrastructure firm. It is the open-source reference implementation
of proven patterns in financial systems, inspired by similar patterns in
Techvica, our banking infrastructure platform.
The repository is intentionally a library, not a product. If you're building production ledger infrastructure and want to discuss the patterns here applied to your context, reach out at hi@xvica.com, or contact the Techvica sales team at sales@techvica.com.
Apache License 2.0. See LICENSE.