Skip to main content

lib-ledger-core

lib-ledger-core is an extensible, asynchronous Python library that provides standard domain models, ports, and storage adapters for immutable double-entry accounting and event-sourced state tracking.

Built around the Ports and Adapters (Hexagonal Architecture) design pattern, lib-ledger-core decouples high-level business rules from storage drivers—allowing you to easily swap underlying databases, log engines, or external financial backends without altering domain logic.


System Philosophy: CQRS & Event Sourcing

At its core, lib-ledger-core enforces a clean Command-Query Responsibility Segregation (CQRS) pattern coupled with Event Sourcing:

flowchart TD
    A["Incoming Instruction"] --> B

    subgraph Command ["1. COMMAND / VALIDATION SIDE (Ledger Backend)"]
        direction TB
        B["Accepts TransferCommand Instructions"] --> C["Validates Domain Invariants<br><i>(Account Verification, Double-Entry Rules)</i>"]
        C --> D["Commits State Transition & Returns Result"]
    end

    D -->|"Valid Transfer Result"| E

    subgraph Query ["2. QUERY / READ SIDE (Event Store Backend)"]
        direction TB
        E["Receives Validated Ledger Execution State"] --> F["Appends Immutable Events to Streams<br><i>(Enforces Optimistic Concurrency Control)</i>"]
        F --> G["Acts as Single Source of Truth<br><i>(Powers Projections & Aggregates)</i>"]
    end

    style Command fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
    style Query fill:#1e1e2e,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
    style A fill:#313244,stroke:#f5e0dc,stroke-width:1px,color:#cdd6f4

Why Event Sourced Aggregates?

  1. Schema-less Aggregate Evolution: Instead of modifying SQL relational table schemas whenever business rules change, domain aggregates (such as account balances, tenant summaries, or risk profiles) are projected directly by reading and replaying historical event streams. Adding new aggregate views requires no database migrations.
  2. Complete Auditability: Traditional database updates overwrite previous state. The event store retains every historical event sequentially, providing a permanent, tamper-evident audit trail required for compliance and financial reconciliation.
  3. Optimistic Concurrency Control (OCC): Event appending guarantees that concurrent attempts to modify the same stream are safely rejected if the sequence version changes unexpectedly.

Universal Balance Tracking: Use Cases

While designed to handle double-entry financial bookkeeping and warehouse inventory movements for ERP environments, the transfer command abstraction fits any system that moves units between two balance states:

  • Financial & Double-Entry Bookkeeping: Manages credits and debits across general ledger accounts, revenue tracking, and accounts payable.
  • Stock Keeping & Warehouse Movements: Validates and records inventory transfers between physical warehouses, storage bins, or supply chain nodes.
  • Crypto & Coin Wallets: Manages balances, gas fees, and token movements between user wallets, cold storage, and hot pools.
  • Carbon Credits & Offsets: Tracks issuance, transfer, and retirement of verified metric tons of carbon emissions ($tCO_2e$) between reserves and corporate accounts.
  • Loyalty Points & Rewards: Handles issuance, transfers, holds, and redemptions of promotional rewards points.
  • Compute & API Quotas: Controls consumption, allocation, and rate-limiting credits for multi-tenant microservices.

Core Architectural Components

1. Abstract Ports (Interfaces)

  • LedgerPort (Validation & Command Side): Defines operations for evaluating transfer instructions, checking current balance snapshots, processing pending holds, and executing multi-leg compound movements.
  • EventStorePort (Audit & Read Side): Defines append-only operations for persisting versioned event streams and querying stream history.

2. Data Models

  • TransferCommand: Immutable instruction specifying debit/credit target accounts, transaction reference, metadata, pending status, and multi-leg transfer definitions.
  • Entry: Immutable transaction record representing an individual debit or credit line item.

3. Backend Adapters

  • SqlAlchemyLedger & SqlAlchemyEventStore: Relational backends utilizing async SQL engines to process transfers and maintain versioned JSON event streams.
  • TigerBeetleLedger: High-throughput ledger integration utilizing TigerBeetle for low-latency balance tracking and account flags.
  • KurrentEventStore: Event-sourcing integration built on top of KurrentDB (EventStoreDB) utilizing msgspec for fast serialization.

Dynamic Adapter Registry

Custom adapters can be created by implementing either the LedgerPort or EventStorePort interface. Third-party modules can register their custom drivers using Python entry-point groups (ledger_core.ledger and ledger_core.event_store). Once registered, the core library can dynamically discover and load adapters at runtime.


Exception & Error Hierarchy

  • LedgerError: Base exception class for all errors generated by the library.
  • InsufficientBalanceError: Raised when a transfer command violates non-negative balance constraints.
  • OccError: Raised when a stream version mismatch occurs during an append operation to the event store.

Using lib-ledger-core

The lib-ledger-core library provides generic ledger primitives, event sourcing ports, dynamic adapter registration, and programmatic database migrations.

1. Database Migrations Programmatically

lib-ledger-core encapsulates its Alembic migration scripts internally. Higher-level applications like book-keeper use the run_migrations helper to initialize or upgrade the schema without maintaining duplicate SQL migration files:

import asyncio
from ledger_core.migrations import run_downgrade, run_migrations
from sqlalchemy.ext.asyncio import create_async_engine

DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/book_keeper"


async def setup_database():
    engine = create_async_engine(DATABASE_URL, future=True)

    # Run all pending ledger-core Alembic migrations
    await run_migrations(engine)

    # ... application setup ...

    await engine.dispose()


if __name__ == "__main__":
    asyncio.run(setup_database())

2. Basic Ledger & Event Store Usage

lib-ledger-core exposes LedgerPort and EventStorePort implementations (such as SqlAlchemyLedger, TigerBeetleLedger, SqlAlchemyEventStore, and KurrentEventStore).

import asyncio
from decimal import Decimal
from ledger_core.adapters.sqlalchemy import SqlAlchemyEventStore, SqlAlchemyLedger
from ledger_core.models import TransferCommand
from sqlalchemy.ext.asyncio import create_async_engine


async def main():
    engine = create_async_engine("sqlite+aiosqlite:///:memory:", future=True)

    # Instantiate Adapters
    ledger = SqlAlchemyLedger(engine)
    event_store = SqlAlchemyEventStore(engine)

    # Seed an account with initial funds
    await ledger.seed_account(
        tenant_id="tenant_1", account="CASH", amount=Decimal("1000.00")
    )

    # Execute a Transfer
    cmd = TransferCommand(
        tenant_id="tenant_1",
        debit_account="EQUIPMENT",
        credit_account="CASH",
        amount=Decimal("250.00"),
        reference="INV-2026-001",
        description="Purchased office equipment",
    )
    transfer_id = await ledger.transfer(cmd)
    print(f"Executed Transfer ID: {transfer_id}")

    # Record Domain Event
    await event_store.append(
        tenant_id="tenant_1",
        stream_id="equipment-purchases",
        events=[
            {
                "type": "EquipmentPurchased",
                "transfer_id": transfer_id,
                "amount": "250.00",
            }
        ],
        expected_version=0,
    )

    # Check Balances
    cash_bal = await ledger.get_balance("tenant_1", "CASH")
    equipment_bal = await ledger.get_balance("tenant_1", "EQUIPMENT")
    print(f"CASH Balance: {cash_bal}")  # Outputs: 750.00
    print(f"EQUIPMENT Balance: {equipment_bal}")  # Outputs: 250.00

    await ledger.close()
    await event_store.close()


if __name__ == "__main__":
    asyncio.run(main())

3. Loading Adapters via Registry

Adapters can also be loaded dynamically using the entry-point registry:

from ledger_core import load_event_store_adapter, load_ledger_adapter
from sqlalchemy.ext.asyncio import create_async_engine

# Dynamically resolve factories using entry-point identifiers
ledger_factory = load_ledger_adapter("sqlalchemy")
event_store_factory = load_event_store_adapter("kurrent")

engine = create_async_engine("postgresql+asyncpg://...")
ledger = ledger_factory(engine)
event_store = event_store_factory("esdb://localhost:2113?tls=false")

4. Application Integration (book-keeper example)

Inside book-keeper, lib-ledger-core adapters are conditionally selected during application startup based on settings:

from ledger_core.adapters.kurrent import KurrentEventStore
from ledger_core.adapters.sqlalchemy import SqlAlchemyEventStore, SqlAlchemyLedger
from ledger_core.adapters.tigerbeetle import TigerBeetleLedger
from ledger_core.interfaces import EventStorePort, LedgerPort


def create_ledger(engine, settings) -> LedgerPort:
    if settings.ledger_type == "postgres":
        return SqlAlchemyLedger(engine)
    return TigerBeetleLedger(
        addresses=settings.tigerbeetle_addresses,
        account_namespace=settings.account_namespace,
    )


def create_event_store(engine, settings) -> EventStorePort:
    if settings.event_store_type == "postgres":
        return SqlAlchemyEventStore(engine)
    return KurrentEventStore(connection_string=settings.kurrent_connection_string)

License

Apache 2.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lib_ledger_core-0.4.1.tar.gz (26.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lib_ledger_core-0.4.1-py3-none-any.whl (22.9 kB view details)

Uploaded Python 3

File details

Details for the file lib_ledger_core-0.4.1.tar.gz.

File metadata

  • Download URL: lib_ledger_core-0.4.1.tar.gz
  • Upload date:
  • Size: 26.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.14.7

File hashes

Hashes for lib_ledger_core-0.4.1.tar.gz
Algorithm Hash digest
SHA256 c4b1a3007b60366421b039d32d48638b729637911158e3b53c7de126c1bc6451
MD5 83e44e2af40a7b4a31ed4d0ae19f5156
BLAKE2b-256 bc2369ee33a856267f5e811534eaa1ce7d769e143d837bffb13e3dc585c65b7f

See more details on using hashes here.

File details

Details for the file lib_ledger_core-0.4.1-py3-none-any.whl.

File metadata

File hashes

Hashes for lib_ledger_core-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5a326173f22fcc035e6351b7b5a7b92a4b4c96e6c40c92f8bbf521d0f02a9d10
MD5 3159e5d918fc21c5e832d27ef1fcdc5a
BLAKE2b-256 34ee39392f67efe59ef1705a6fac7781d9f0ea3e61d3b6309e493aafa1b7a3dd

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.4.1 This release

2 files

0.3.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page