GitHunt
KA

kai-niemi/ledger

A financial accounting ledger for CockroachDB

Java CI

Ledger

Ledger represents a realistic, financial accounting ledger based on the double-entry
bookkeeping principle running on either CockroachDB or PostgreSQL. It manages monetary
accounts and a journal of balanced, multi-legged, multi-currency transfers between those accounts.

It's designed to showcase CockroachDB's scalability, survival, consistency and data domiciling
capabilities and not the actual domain complexity of accounting. It is however a realistic
implementation of a general ledger with most of the fundamental pieces like account plans,
reporting and invariant checks.

Ledger is the successor to Roach Bank with more focus on
load testing and operational ease-of-use. Conceptually they are the same, but Ledger has a simpler
design, improved UI and leverages JDK21 features such as virtual threads for better efficiency.

Compatibility

  • CockroachDB v23.2+
  • PostgreSQL 9+
  • macOS (main platform)
  • Linux
  • JDK 21

Terms of Use

This tool is not supported by Cockroach Labs. Use of this tool is entirely at your
own risk and Cockroach Labs makes no guarantees or warranties about its operation.

See MIT for terms and conditions.

Setup

Things you need to build and run Ledger locally.

Prerequisites

Install the JDK

MacOS (using sdkman):

curl -s "https://get.sdkman.io" | bash
sdk list java
sdk install java 21.0 (pick version)  

Ubuntu:

sudo apt-get install openjdk-21-jdk

Database Setup

CockroachDB Setup

See start a local cluster for setup instructions.

Then create the database, in this case assuming an insecure cluster:

cockroach sql --insecure -e "create database ledger"

An enterprise license
is only needed when using geo-partitioning and follower-reads (opt-out features if n/a).

To set an enterprise license:

cockroach sql --insecure --host=localhost -e "SET CLUSTER SETTING cluster.organization = '...'; SET CLUSTER SETTING enterprise.license = '...';"

PostgreSQL Setup (optional)

Install on MacOS using brew:

brew install postgresql

Start PSQL:

brew services start postgresql@14

Create the database:

psql postgres
$ CREATE database ledger;
$ CREATE extension pgcrypto;
$ quit

Stop PSQL:

brew services stop postgresql@14

Building

Instructions for building the project locally.

Clone the project

git clone git@github.com:kai-niemi/ledger.git && cd ledger

Build the artifact

chmod +x mvnw
./mvnw clean install

Ledger is one single server component (executable jar) now available undertarget/.

Configuration

All configuration properties can be specified in config/application-default.yml that
overrides the baseline configuration in src/main/resources/application.yml
(see this file for a reference on all options available).

You can also override all parameters through the command line, which is the easiest approach.

For example:

java -jar ledger.jar \
--spring.datasource.url="jdbc:postgresql://localhost:26257/ledger?ssl=true&sslmode=require&allow_unsafe_internals=true" \
--spring.datasource.username=craig \
--spring.datasource.password=cockroach \
--spring.profiles.active="default"

Alternatively, you can create a new YAML file with a custom name suffix and then pass that
name with the --profiles argument:

cp src/main/resources/application.yml config/application-craig.yml
java -jar ledger.jar --profiles craig

Running

Run with an interactive shell

Start the server with:

java -jar target/ledger.jar <args>

(or use ./run-server.sh).

Now you can access the application via http://localhost:9090.

Start in the background

Add the shell commands you would like to run into a plain text file:

echo "version" > cmd.txt
echo "workload start transfer-funds" >> cmd.txt
echo "workload start read-balance" >> cmd.txt

Start the server in the background by passing a command file name
as argument with a @ prefix:

nohup java -jar target/ledger.jar @cmd.txt > ledger-stdout.log 2>&1 &

(or use ./start-server.sh @cmd.txt)

The server will run all commands in the text file and then wait to be closed.
Notice that you can't use quit in the end since all commands are run in sequence
but executed in parallel.

Use PostgreSQL

PostgreSQL as the underlying database is used by activating the psql profile:

java -jar ledger.jar \
--spring.datasource.url="jdbc:postgresql://localhost:5432/ledger \
--spring.datasource.username=craig \
--spring.datasource.password=cockroach \
--spring.profiles.active="psql"

Ledger automatically adjusts to the dialect of PostgreSQL and lack of multi-region
and follower read features. There's also no concept of gateway nodes, or primary
and secondary regions, so you will need to specify the region(s) to use on most
workload commands.

Single-region Tutorial

Usage tutorial for running a basic demo or load test towards a single-region
or single-host CockroachDB cluster.

Basics

Ledger is operated through its built-in shell, or by a command file passed at startup
time, in which case the shell is disabled (see running instructions).
For a complete list of commands, run the help command in the shell. There's also
a reactive web UI available at http://localhost:9090 (by default) to display
account activities and different charts.

Ledger binds monetary accounts to named cities within regions. A region maps to an
actual CockroachDB --locality region tier like aws-us-east-1. One region has one or
more cities with accounts, and each city has a country and currency code. This allows
for optimal data distribution and simple data partitioning for multi-region load
test scenarios.

The default configuration includes all current CockroachDB Cloud regions for AWS, GCP
and Azure. You can easily add custom region ot city mappings through a
config/application-<profile> YAML.

For example:

  ...
  application:
      regions:
        - name: us-east-1
          country: USA
          currency: USD
          cities:
            - name: new york
            - name: boston
            - name: washington dc
            - name: miami
            - name: charlotte
            - name: atlanta
        - name: us-east-2
          country: USA
          currency: USD
          cities:
            - name: st louis
            - name: indianapolis
            - name: nashville
            - name: dallas
            - name: houston
            - name: detroit
        - name: us-west-1
          country: USA
          currency: USD
          cities:
            - name: san francisco
            - name: los angeles
            - name: san diego
            - name: las vegas
            - name: salt lake city
        ...
      region-mappings:
        aws-us-east-1: us-east-1 
        aws-us-east-2: us-east-2 
        aws-us-west-1: us-west-1 
        gcp-us-east1: us-east-1
        gcp-us-east4: us-east-2
        gcp-us-west1: us-west-1
        ...

Ledger initially attempts to match the cluster's --locality region tier to an application
region name in application.regions.name. If no region is found, it checks the region-mappings
section for a matching key. If none is found there either, it logs a warning that the database
cluster's locality tiers doesnt match any application region. For a multi-region use case / demo,
its required to use at least 3 separate regions preferably with disjoint cities for best data
distribution.

For more guidance, see the Multi-region Tutorial.

Create an account plan

Ledger operates towards a pre-defined account plan, which is simply a collection of accounts of
different types with an initial balance and a total balance of zero per currency. Negative balances
are used to simplify the concepts within accounting called credit and debit, which are never of
negative value.

This command will create one liability account and 5,000 asset accounts per city, by default:

db build accountplan

The account plan is organized in such a way that the total balance of all accounts for a given
city and currency amounts to zero. Thus, if a non-zero total balance is ever observed it means
money has either been invented or destroyed and we can't have that. Under serializable (default)
isolation this is impossible. You can however allow such anomalies just for fun by running
in read committed isolation without locks. That would make this ledger exposed to P4
lost updates with non-zero balance sums as a result.

Transfer funds

This is a read-write intensive workload that transfer funds between accounts picked by random.

workload start transfer-funds

One transfer leg equals to one account balance update, thus the minimum number of
transfer legs is two. A balance update can hold a positive or negative value for simplicity,
rather than a credit or debit in real accounting. By default, all workloads select the cities
in the gateway node's region (local region to the client). You can however pick any region,
or all of them, affectively starting a transfer workload for every city, like:

workload start transfer-funds --region all

Read balance

This is a purely read-only command that executes point lookups on accounts to retrieve the current balance.

workload start read-balance

Another variant is to use historical follower reads, which, if you are familiar with CockroachDB, means
that any node receiving a request hosting a range replica for the requested key can service the request,
at the expense of the returned value being potentially stale with up to ~5s
(called a bounded staleness read). Normally only the lease holder replica can service reads which
are authoritative.

workload start read-balance --stale true

Create accounts

This is a write-only command to create new asset accounts in batches. It can be used to
populate the database with more accounts than included in the account plan.

workload start create-accounts

Notice that these accounts will have a zero balance and are not allowed to go negative.
To fund these accounts, you need to run the workload start transfer-grants command to grant funds
from liability accounts.

Transfer grants

This command will move money from liability accounts to asset accounts withing
a specified balance range. It's useful to run this after creating new accounts to allow
these to become part of workload selections.

workload start transfer-grants

Workload Overview

A quick overview of the main workload commands and the type of SQL transactions they perform.

Workload Reads Writes Explicit Implicit Locks Duration
transfer-funds 20% 80% x x 120m
transfer-grants 10% 90% x x Finite
create-accounts 100% x 120m
read-balance 100% x 120m
read-balance-historical 100& x 120m

Multi-region Tutorial

For the complete multi-region guide, see tutorial.


-- That is all, enjoy your accounting experience!