- docker
- go
- air (if running without docker)
- git
- make
- node (integration tests)
- pnpm (integration tests)
| This repo mainly makes use of docker and docker compose
Docker
make docker-build
- Build the BLedger dockerfile
Go + Node
make update
-- installs requirementsmake build
-- builds binary to./bin/server
Warning
When deciding to run through docker or go directly, keep in mind that you have to change the
DB_DSN
env variable to maintain a proper connection. Docker Container <-> Docker Container uses an internal networkDOCKER:
- DB_DSN=postgres://postgres:postgres@host.docker.internal:5432/postgres?sslmode=disable
GO :
- DB_DSN=postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable
Docker Services
- Start all services with
docker-compose up
- Note: the healthcheck on the docker-compose.yml waits for the postgres instance to be fully healthy before starting the BLedger instance. This will take a few seconds to all go live.
- Note:
BLedger
is commented out but will work if uncommented. Fastest way to run is todocker-compose up -d
&make run-hot
to get a reloadable server calling a local postgres instance with a persistent volume without config changes.
API
-
Go + Air (preferred)
make run-hot
-
Go (bin)
make build
make run-direct
-- runs the server without hot reloading
-
Go (raw cmd/server.go)
make run-raw
API
Served at http://localhost:8080
HTTP Verb | Route | Handler |
---|---|---|
GET | /v1/transactions/:id | github.com/$user/bledger/internal/router.(*Manager).GetTransaction |
POST | /v1/transactions/ | github.com/$user/bledger/internal/router.(*Manager).CreatePendingTransaction |
PUT | /v1/transactions/:id | github.com/$user/bledger/internal/router.(*Manager).ExecutePendingTransaction |
POST | /v1/transactions/immediate | github.com/$user/bledger/internal/router.(*Manager).CreateTransaction |
DELETE | /v1/transactions/:id | github.com/$user/bledger/internal/router.(*Manager).ReverseTransaction |
GET | /v1/accounts/:id | github.com/$user/bledger/internal/router.(*Manager).GetAccount |
POST | /v1/accounts/ | github.com/$user/bledger/internal/router.(*Manager).CreateAccount |
GET | /health_check | github.com/$user/bledger/internal/router.(*Manager).InitRouter.func1 |
GET | / | github.com/$user/bledger/internal/router.(*Manager).InitRouter.func2 |
make lint
-- runs linter and security checker
make test
-- runs controller, and other misc tests
docker-compose up -d
-- to start local redis and postgresmake run-hot
-- to start a server instancemake integration_test
-- runs raw typescript integration tests (not using jest for sake of time)
| Fill in your .env
at your root with (this is currently included and not ignored in the .gitignore):
If using all docker containers
ENVIRONMENT=local
PORT=8080
DB_DSN=postgres://postgres:postgres@host.docker.internal:5432/postgres?sslmode=disable
CACHE_URI=redis://localhost:6379
CACHE_PASSWORD=eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81
If using docker container for redis and postgres but with a raw go server
ENVIRONMENT=local
PORT=8080
DB_DSN=postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable
CACHE_URI=localhost:6379
CACHE_PASSWORD=eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81
├── Dockerfile
├── Makefile
├── README.md
├── bin
├── cmd
│ └── server.go
├── docker-compose.yaml
├── go.mod
├── go.sum
├── integration_tests
│ ├── account.ts
│ ├── helpers.ts
│ ├── index.ts
│ ├── interfaces.ts
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── transactions.ts
│ └── tsconfig.json
├── internal
│ ├── cache
│ │ ├── cache.go
│ │ └── redis
│ │ └── redis.go
│ ├── common
│ │ ├── constant.go
│ │ ├── error.go
│ │ ├── error_test.go
│ │ ├── helper.go
│ │ ├── helper_test.go
│ │ ├── idempotency.go
│ │ ├── server.go
│ │ └── server_test.go
│ ├── config
│ │ └── config.go
│ ├── controller
│ │ ├── account.go
│ │ ├── controller.go
│ │ └── transaction.go
│ ├── db
│ │ └── db.go
│ ├── middleware
│ │ ├── idempotency.go
│ │ └── logger.go
│ ├── model
│ │ ├── account.go
│ │ ├── environment.go
│ │ ├── response.go
│ │ ├── transaction.go
│ │ └── version.go
│ └── router
│ ├── account.go
│ ├── router.go
│ └── transaction.go
└── pkg
└── version.go
The following project is a non-versioned transction ledger that allows for 1-step and 2-step transctions to be created on an account. You can create accounts, fetch their balances and create immediate and 2-step transactions using CREDIT or DEBIT.
Immediate transactions immediately move to a COMPLETED
state if they pass balance and currency checks, while a 2-step transaction lets the consumer create a transaction that moves to PENDING
state with a subsequent api call that moves it into COMPLETED
if it passes the checks. COMPLETED
transactions can be reversed, and will make balance changes on the associated account; the resulting balance is REVERSED
.
The transactions and account balance update management rely on DB transaction atomicity and mutexes. The transaction controller sets account and transaction locks to prevent multiple writes to the same row of data that could cause data loss.
For instance, if we are executing a 2-step transaction for Account A
we set a row level lock on the both the transaction and account rows to prevent corruption. If multiple other 1-step or 2-step transactions wanted to take place, they must wait for the row-level lock to end before accessing the database
There are no server-side mutexes or channel synchronizations because we are relying on the database layer and DB locks as our mutex. When using GORM and an ACID-compliant database, relying on database transactions should be sufficient to maintain consistency and performance in the transaction ledger.
There also exists, an idempotency middleware, that allows for an api consumer to prevent duplicate writes of the transaction. The idempotency middleware is commented out in the router, but can be simply uncommented and will work amongst all apis. The idempotency keys are set in redis for hot caching and faster duplicate-write prevention.
- Properly version the the transactions. For time's sake, went with a single-version transaction with an updatable
state
rather then multiple versions of transactions with a shared primary key. Ideally you would have debits and credits have some sort of version and a primary key that is generated from some components of the transaction and display it's lifecycle. Today, we simply revert the transaction and update the state and subsequent account balance changes - Allow for 2-sided transactions to take place. E.g. instead of only
DEBIT
orCREDIT
onAccount A
, we might have a transction ofDEBIT AccountA
andCREDIT AccountB
and their subsequent balances checks rather than making 2 different transactions using this api. - More unit tests, currently relying heavy on integration tests for brevity to avoid creating golang mocks
Create a small application that can track monetary balances for multiple accounts in real time.
Requirements:
[x] System should accept credits and debits, each starting in a pending state that may be moved to a completed state at some point in the future.
[x] Credits and debits may fail before succeeding.
[x] Credits and debits may be reversed after initially succeeding.
[x] Does not allow debits for more than what’s available.
[x] Multiple requests to create transactions for an individual account can be executed in parallel. The system should remain correct in such circumstances.
[x] Provides a way to query an account’s balance at any point in time.