The much-sought-after yet elusive cash register system is now finally available as a micro-monolith.
Can't stand reading long texts? Don't you dare swipe back to TikTok or YouTube
Shorts!
You want to be inspired and see for yourseld how ka-ching-backend and
ka-ching-client (both in Ruby) could be used in a real world project?
β‘οΈ Visit ka-ching-demo and fire
up that docker-compose
π³π₯
it's still WIP, but you will get to use most of the features by now already! πͺ
π«¨
"Credit card goes brrrrrrr,
but real money goes ka-ching!"- Lambda Ferrari
AND ALSO
"Money can't buy happiness, but it's a lot more comfortable to cry in a Mercedes than on a bicycle."
- John Doe
- multi-tenant database architecture
- relies on database locks for critical operations
- json columns in most tables, to pass your own context
- fast, cheap, and reliable
- short and concise codebase
- lean on dependencies
- with containerization in mind
To learn how to create tenant databases, create bookings, create lockings, and
access audit logs, please read the API V1 documentation
here after a good old
$ rackup
.
Alternatively, when having Node.js installed, you can also run the following command to start a local server that serves the API V1 documentation:
$ npx http-server -p 9292
# open browser at http://localhost:9292/docs/api/v1/index.html
Checkout the Client, too π¦
You can find it on GitHub:
ka-ching-client.
- Quickstart for local testing/development
- Quickstart for local experiments with Docker
- What this repo is
- Setup and Dependencies
- Database Setup (the initial setup)
- Run
- Abstractions
- Diagrams
- Code of Conduct
- Contributing
- Todos
you need a default postgres instance running or configure the connection via environment variables.
- clone the repo
- create the databases
$ bin/setup
- run the tests
$ bundle install && rake
- start the server
$ rackup
from another terminal, while the server is running
- create a sample tenant
$ http POST ':9292/ka-ching/api/v1/admin' \
tenant_account_id=testuser_1
- create a sample booking with value of 100 cents
$ http POST ':9292/ka-ching/api/v1/testuser_1/bookings' \
action="deposit" \
amount_cents:=100 \
year:=2023 \
month:=1 \
day:=1 \
'context[foo]="bar"'
- create a sample withdrawal with value of 100 cents
$ http POST ':9292/ka-ching/api/v1/testuser_1/bookings' \
action="withdrawal" \
amount_cents:=100 \
year:=2023 \
month:=1 \
day:=1 \
'context[foo]="bar"'
- create a sample locking
$ http POST ':9292/ka-ching/api/v1/testuser_1/lockings' \
action="lock" \
amount_cents_saldo_user_counted:=100 \
year:=2023 \
month:=1 \
day:=1 \
'context[foo]="bar"'
- create a sample unlocking
$ http DELETE ':9292/ka-ching/api/v1/testuser_1/lockings'
- check auditlogs
$ http ':9292/ka-ching/api/v1/testuser_1/auditlogs/2023'
I recommend digging into the API V1 documentation to learn more about the whole feature set of the API.
- clone the repo
- in terminal "A"
$ docker-compose up
- in terminal "B"
$ docker-compose exec -ti api bin/setup
- stop the server in terminal "A" with
Ctrl+C
$ docker-compose up
and run call against the API (see above)
A fully functional multi-tenant REST-API application that acts as an abstraction layer for register cash transactions.
This aims to be an uncomplicated and effortless tool for you to track/book cash
register transactions, designed with a multitenant database in mind.
Small footprint, user-friendly REST-API.
Easy to incorporate it into your infrastructure as a service, secured via
firewalls and proxies on infrastructure level.
This application does not include a frontend or authentication capabilities. It is not designed to be your standalone backend system.
When deciding to implement a micro-monolith for accounting or booking logic, it is important to consider separation of concerns, adherence to SOLID principles, and keeping the main service's code base clean. The following arguments support these principles:
-
Separation of Concerns: Implementing accounting or booking logic in a separate service helps to reduce complexity and ensure that each service has a single responsibility. This separation of concerns makes it easier to maintain the application and to make changes to the logic without affecting other parts of the system.
-
SOLID Principles: Implementing accounting or booking logic as a micro-monolith encourages adherence to the SOLID principles of object-oriented programming, particularly the Single Responsibility Principle (SRP) and the Interface Segregation Principle (ISP). This ensures that each product has a clear responsibility and that it interacts with other services through well-defined interfaces.
-
Easier Testing: Implementing accounting or booking logic as a micro-monolith makes it easier to test this functionality in isolation, without the need to test the entire application. This approach makes it easier to write comprehensive tests that cover all aspects of the functionality and ensures that any bugs are caught early in the development cycle.
-
Maintainability: Implementing accounting or booking logic as a micro-monolith makes it easier to maintain the application over time, as changes to the logic can be made independently of other parts of the system. This approach reduces the risk of introducing bugs or breaking existing functionality when making changes.
-
Flexibility: Implementing the cash booking logic as a product of its own allows to tune or version the API independently of the main service. This approach makes it easier to make changes to the API without affecting the main service, and it allows to implement new features without breaking existing functionality.
This project aims to provide a simple and easy to use REST-API for cash register transactions. By providing JSON columns for storing arbitrary data, it is possible to extend the API with custom fields and data structures.
I've been tossing around the term "micro-monolith" for some time. Not sure if it's officially recognized, but I dig it. It's a compact monolith that can effortlessly serve as a foundation for something larger or function independently. As a micro-monolith, it's indivisible. It's a monolith, just pint-sized.
The following environment variables are used to configure the application:
Sequel is used to connect to the database. The following environment variables are used to configure the connection:
DATABASE_USER
the database user, defaults to:postgres
DATABASE_PASSWORD
the database password, defaults to:postgres
DATABASE_URL
the database url, defaults to:localhost
DATASBASE_PORT
the database port, defaults to:5432
The connection to the database is established using Sequel's connection string format (see details here).
"postgres://#{DATABASE_URL}:#{DATABASE_PORT}/#{database}?user=#{DATABASE_USER}&password=#{DATABASE_PASSWORD}"
This project relies on database locking and how it works specifically in PostgreSQL. As a result, the implementation of this program is limited to using PostgreSQL.
DATABASE_TENANT_DATABASE_NAMESPACE
- the database namespace, defaults to:kaching
DATABASE_NAME_SHARED
- the database name, defaults to:kaching_shared
DATABASE_NAME_BLANK
- the database name, defaults to:"#{DATABASE_TENANT_DATABASE_NAMESPACE}blank"
RACK_ENV
- the environment, defaults to:development
KACHING_RESET_PROTECTION
- the demo mode, defaults to:false
, if set totrue
, the application will allow resets of a tenant's data
WEB_CONCURRENCY
- the number of puma workers, defaults to:2
WEB_MAX_THREADS
- the number of max puma threads, defaults to:2
WEB_MIN_THREADS
- the number of min puma threads, defaults to:1
- Setup databases via
$ bin/setup
- check
Rakefile
Make sure you have the described environment variables set or match the defaults described above.
$ bin/setup
$ bundle install
$ bundle exec rackup
- drop all databases for the current environment (kaching_*) and/or whatever
you have set in
DATABASE_TENANT_DATABASE_NAMESPACE
$ bin/setup
$ bundle exec rake test
Make sure you have the described environment variables set or match the defaults described above.
Add this environment variable
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
to your Docker startup script (that might be a customized docker-compose.yml).
The part regarding the architecture might be different on your machine.
x86_64-linux-gnu
should be replaced with aarch64-linux-gnu
on an M1 Mac.
As an example:
Toggle me!
version: "3.1"
services:
db:
image: postgres:alpine
restart: always
environment:
POSTGRES_USER: kaching
POSTGRES_PASSWORD: kaching
LC_ALL: C.UTF-8
LANG: en_US.UTF-8
api:
image: ghcr.io/simonneutert/ka-ching-backend:main
# build:
# context: .
# dockerfile: Dockerfile
restart: always
environment:
- DATABASE_URL=db
- DATABASE_USER=kaching
- DATABASE_PASSWORD=kaching
# - RACK_ENV=development
- KACHING_RESET_PROTECTION=false
- LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 # Your architecture (x86_64-linux-gnu) might be different on an M1 Mac it would be aarch64-linux-gnu
depends_on:
- db
ports:
- 9292:9292
expose:
- 9292
- Postgres Wiki on Performance Optimization.
- dbForge Studio for PostgreSQL helps to identify productivity bottlenecks, and provides PostgreSQL performance tuning.
- The postgresqltuner.pl script on GitHub can analyze the configuration and make tuning recommendations.
- PgBadger analyse PostgreSQL logs to generate performance reports.
- pgMustard provides tuning advice based on EXPLAIN ANALYZE output.
Currently, no ORM is used. The database is accessed directly via Sequel, and services are implemented as a set of classes. This approach aims to ensure that the logic of the application is not tightly coupled to models (and their behaviour), but is rather based on tasks. Some critical tasks need to be backed by a database/table lock, in a multitenant it was more expressive this way.
bookings
- a booking is a transaction that is booked in the database
- A
locking
is a collection of bookings that are locked in the database for a period of time. Unlocking a locking
: This action deactivates a locking and unlocks all bookings related to the locking. An entry will be created in theaudit_logs
table. You should make an effort to guide your users so that this action is run as seldom as possible.
- An
audit_logs
is a log entry in the database of certain actions and their state, such as unlocking a locking.
The two main flows of the application are described in the following diagrams.
They aren't meant to be a complete description of the application, but rather to give an overview of the main flows.
graph TD;
A[Start] --> ValidateTransaction(Enter transaction details);
ValidateTransaction(Validate transaction details);
ValidateType{What is the type of Booking?};
ValidateType -- Withdraw --> ValidatePositiveSaldo;
ValidateType -- Deposit --> SuccessBooking;
ValidatePositiveSaldo(Check current saldo);
ValidatePositiveSaldo --> BookingSaldoPositive{Current Saldo positive?}
BookingSaldoPositive -- Negative --> InvalidBooking
InvalidBooking[Invalid Booking];
InvalidBooking --> Failure;
BookingSaldoPositive -- Positive --> SuccessBooking;
ValidateTransaction --> ValidLockStatus{"is a booking for that date\nin an unlocked period?"};
ValidLockStatus --- No --> InvalidBooking;
ValidLockStatus --- Yes --> ValidateType;
SuccessBooking[(Database)];
Failure;
graph TD;
A[Start] --> LockPeriod(Lock a Period);
LockPeriod --> ValidatePeriod;
ValidatePeriod{Is a lock already in place?};
ValidatePeriod -- Yes --> Yes;
Yes --> Failure;
Failure;
ValidatePeriod -- No --> No;
No --> CollectBookings(Collect Bookings since last locking);
CollectBookings --> Sum(Sum up bookings);
CollectBookings --> SumUser(Sum up bookings);
SumUser(Require user to count its cash balance);
SumUser --> SumTotal(Sum up total of what the books say and what's really in the user's pockets);
Sum --> SumTotal;
SumTotal --> SuccessLocking;
SuccessLocking[(Database)];
graph TD;
A[Start] --> UnlockPeriod(Unlock a Period);
UnlockPeriod --> ValidatePeriod;
ValidatePeriod{Is a lock already in place?};
ValidatePeriod --> Yes;
Yes --> NoOtherBookings{Was a booking created since last locking?};
NoOtherBookings --> Yes2("Yes (you could delete all bookings to then unlock)");
Yes2 --> Failure;
NoOtherBookings --> No2(No newer Bookings);
No2 --> CreateAuditLog
CreateAuditLog -- "Collect all Bookings for the locking period, persist current state/env" --> SuccessLocking;
Failure;
ValidatePeriod --> No;
No --> Failure;
SuccessLocking[(Database)];
There are a few simple rules to follow when using this tool.
- be nice
- be patient
see CONTRIBUTING.md
How to contribute tldr
- Fork the repo
- Create a new branch
- Make your changes
- Write tests
- Make sure all tests pass
- Make sure your code is formatted with
rubocop
- Make sure your code (the public parts) is documented with
yard
- Make sure to add an entry to the CHANGELOG.md
- Update the documentation if necessary (README.md (with its ToC), etc.)
- Create a pull request
- Wait for the CI to run
- Wait for a review
- π π π
I use Visual Studio Code with this extension: https://markdown-all-in-one.github.io/docs/guide/table-of-contents.html#creating-a-toc
Maybe you should, too?
Our test suite is written with Minitest.
ruby -w -Ilib:test test/api/v1/integration/bookings_test.rb --name test_no_negative_saldo_in_between