Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributing to go-ledger
# Contributing to ledger

Thank you for your interest in contributing to `go-ledger`! As a high-consistency financial service, we mastertain strict code quality standards.
Thank you for your interest in contributing to `ledger`! As a high-consistency financial service, we mastertain strict code quality standards.

## 🌿 Branching Strategy

Expand Down
49 changes: 31 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# go-ledger
# ledger

A bank-grade financial transaction service built in Go. Designed to be the central financial brain that microservices talk to when they need to move value safely.

Expand All @@ -22,7 +22,7 @@ graph TD

### Why hexagonal architecture

go-ledger has three input transports: gRPC, HTTP Gateway, and a future event consumer. Hexagonal architecture isolates the domain from all of them — the service layer never imports gRPC, HTTP, or Postgres packages. Each transport is a plug-in adapter.
ledger has three input transports: gRPC, HTTP Gateway, and a future event consumer. Hexagonal architecture isolates the domain from all of them — the service layer never imports gRPC, HTTP, or Postgres packages. Each transport is a plug-in adapter.

### The dependency flow (strictly one-directional, no cycles)

Expand Down Expand Up @@ -115,8 +115,8 @@ Error messages are translated based on the client's `Accept-Language` header. Su

```bash
# Clone and configure
git clone https://github.com/mohammad-farrokhnia/go-ledger.git
cd go-ledger
git clone https://github.com/mohammad-farrokhnia/ledger.git
cd ledger
cp configs/config.example.yaml configs/config.yaml
# Edit configs/config.yaml — at minimum set database.dsn

Expand Down Expand Up @@ -157,8 +157,9 @@ grpcurl -plaintext -d '{
### Top up a user wallet (mint money from SYSTEM)

```bash
# idempotency_key must be a UUID — generate one before the call
grpcurl -plaintext -d '{
"idempotency_key": "topup-alice-001",
"idempotency_key": "550e8400-e29b-41d4-a716-446655440001",
"from_account_id": "<SYSTEM_ID>",
"to_account_id": "<ALICE_ID>",
"amount": 10000,
Expand All @@ -175,9 +176,9 @@ curl http://localhost:8080/v1/wallets/<ALICE_ID>/balance | jq .
### Retry with same idempotency key (safe — returns original)

```bash
# Sending the same request again returns the original transaction, money moves once
# Sending the same UUID again returns the original transaction, money moves once
grpcurl -plaintext -d '{
"idempotency_key": "topup-alice-001",
"idempotency_key": "550e8400-e29b-41d4-a716-446655440001",
"from_account_id": "<SYSTEM_ID>",
"to_account_id": "<ALICE_ID>",
"amount": 10000,
Expand All @@ -190,8 +191,8 @@ grpcurl -plaintext -d '{
```bash
curl http://localhost:8080/health | jq .
# {
# "data": {"status": "ok", "version": "1.0.0", "app": "go-ledger", "checks": {"postgres": "ok"}},
# "meta": {"appName": "go-ledger", "version": "1.0.0", "timestamp": "...", "messageCode": "HEALTH_OK", "message": "healthy"}
# "data": {"status": "ok", "version": "1.0.0", "app": "ledger", "checks": {"postgres": "ok"}},
# "meta": {"appName": "ledger", "version": "1.0.0", "timestamp": "...", "messageCode": "HEALTH_OK", "message": "healthy"}
# }
```

Expand All @@ -212,16 +213,24 @@ curl http://localhost:8080/metrics | grep ledger
make up # start Postgres + pgAdmin (localhost:5050)
make down # stop containers, keep data
make down-volumes # stop containers, delete all data
make migrate # run migrations on dev DB
make migrate-test # run migrations on test DB
make gen # regenerate proto → Go + Swagger
make migrate # run migrations on dev DB (requires DB_DSN in .env or env)
make migrate-test # run migrations on test DB (requires DB_DSN_TEST)
make gen # regenerate proto → Go + Swagger (requires buf + protoc plugins)
make lint # run golangci-lint
make test # run all tests with -race flag
make run # run the service (reads .env)
make run # run the service (reads configs/config.yaml)
make db # open psql shell
make help # list all targets
```

`make gen` requires `buf` and the Go protoc plugins:
```bash
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
```

## Configuration

The service loads `configs/config.yaml` first, then overlays any matching environment variables. Create your config by copying the template:
Expand All @@ -244,8 +253,11 @@ The Makefile (`make migrate`, `make db`) reads a `.env` file for convenience —

## Running Tests

Integration tests (postgres package) require a running Postgres instance and `DB_DSN_TEST`:

```bash
# Ensure test DB is migrated
# Set DB_DSN_TEST in your environment or .env, then migrate the test DB
export DB_DSN_TEST="postgres://postgres:postgres@localhost:5432/ledger_test?sslmode=disable"
make migrate-test

# Run all tests including concurrency tests
Expand All @@ -257,10 +269,12 @@ make test
# TestCreateTransaction_Idempotency — same key twice, money moves once
```

Unit tests (ledger package) use a mock store and run without Postgres. Integration tests skip automatically when `DB_DSN_TEST` is unset.

## Building the Docker Image

```bash
docker build -f deployments/docker/Dockerfile -t go-ledger:latest .
docker build -f deployments/docker/Dockerfile -t ledger:latest .

# Run with docker compose (includes Postgres)
docker compose -f deployments/docker/docker-compose.yml up
Expand All @@ -284,16 +298,15 @@ docker compose -f deployments/docker/docker-compose.yml up
## Project Structure

```
api/proto/ledger/v1/ proto definition + generated Go code
api/proto/ledger/v1/ ledger.proto generated .pb.go files are gitignored (make gen)
cmd/ledger/ entrypoint — composition root only
configs/ config.yaml template
deployments/docker/ Dockerfile + docker-compose
docs/ API documentation
internal/
audit/ outbox worker — polls DB, POSTs events to webhook
config/ koanf config loader
i18n/ error message translation (English + Farsi)
ledger/ domain: types, errors, Store/Auditor ports, Service
ledger/ domain: types, errors, Store/OutboxStore ports, Service
metrics/ Prometheus metric definitions
store/postgres/ Postgres implementation of ledger.Store
transport/grpc/ gRPC server, handler, interceptors, mappers
Expand Down
2 changes: 1 addition & 1 deletion api/openapi/ledger.swagger.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"title": "go-ledger API",
"title": "ledger API",
"description": "A high-consistency, bank-grade financial transaction service.",
"version": "1.0"
},
Expand Down
4 changes: 2 additions & 2 deletions api/proto/ledger/v1/ledger.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";

option go_package = "github.com/mohammad-farrokhnia/go-ledger/api/proto/ledger/v1;ledgerv1";
option go_package = "github.com/mohammad-farrokhnia/ledger/api/proto/ledger/v1;ledgerv1";

Check failure on line 9 in api/proto/ledger/v1/ledger.proto

View workflow job for this annotation

GitHub Actions / Buf lint

File option "go_package" changed from "github.com/mohammad-farrokhnia/go-ledger/api/proto/ledger/v1;ledgerv1" to "github.com/mohammad-farrokhnia/ledger/api/proto/ledger/v1;ledgerv1".
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "go-ledger API"
title: "ledger API"
version: "1.0"
description: "A high-consistency, bank-grade financial transaction service."
}
Expand Down
14 changes: 7 additions & 7 deletions cmd/ledger/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import (

"golang.org/x/sync/errgroup"

"github.com/mohammad-farrokhnia/go-ledger/internal/audit"
"github.com/mohammad-farrokhnia/go-ledger/internal/config"
"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
_ "github.com/mohammad-farrokhnia/go-ledger/internal/metrics"
"github.com/mohammad-farrokhnia/go-ledger/internal/store/postgres"
transportgrpc "github.com/mohammad-farrokhnia/go-ledger/internal/transport/grpc"
transporthttp "github.com/mohammad-farrokhnia/go-ledger/internal/transport/http"
"github.com/mohammad-farrokhnia/ledger/internal/audit"
"github.com/mohammad-farrokhnia/ledger/internal/config"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
_ "github.com/mohammad-farrokhnia/ledger/internal/metrics"
"github.com/mohammad-farrokhnia/ledger/internal/store/postgres"
transportgrpc "github.com/mohammad-farrokhnia/ledger/internal/transport/grpc"
transporthttp "github.com/mohammad-farrokhnia/ledger/internal/transport/http"
)

func main() {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/mohammad-farrokhnia/go-ledger
module github.com/mohammad-farrokhnia/ledger

go 1.26.3

Expand Down
2 changes: 1 addition & 1 deletion internal/audit/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"net/http"
"time"

"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
)

type Worker struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/i18n/messages.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//nolint:staticcheck // U+200C (ZWNJ) is intentional Persian typography
package i18n

import "github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
import "github.com/mohammad-farrokhnia/ledger/internal/ledger"

var catalog = map[string]map[Lang]string{
"account_not_found": {
Expand Down
2 changes: 1 addition & 1 deletion internal/ledger/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"errors"
"testing"

"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
)

type mockStore struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/store/postgres/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

"github.com/jackc/pgx/v5"

"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
)

func (s *Store) CreateAccount(ctx context.Context, params ledger.CreateAccountParams) (ledger.Account, error) {
Expand Down
2 changes: 1 addition & 1 deletion internal/store/postgres/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"context"
"fmt"

"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
)

func (s *Store) GetWalletHistory(ctx context.Context, params ledger.GetWalletHistoryParams) ([]ledger.Entry, error) {
Expand Down
2 changes: 1 addition & 1 deletion internal/store/postgres/outbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"encoding/json"
"fmt"

"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
)

func (s *Store) PollAuditOutbox(ctx context.Context, limit int) ([]ledger.AuditOutboxEntry, error) {
Expand Down
2 changes: 1 addition & 1 deletion internal/store/postgres/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"fmt"

"github.com/jackc/pgx/v5/pgxpool"
"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
)

type Store struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/store/postgres/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"sort"

"github.com/jackc/pgx/v5"
"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
)

type accountSnapshot struct {
Expand Down
4 changes: 2 additions & 2 deletions internal/store/postgres/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
"sync"
"testing"

"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
"github.com/mohammad-farrokhnia/go-ledger/internal/store/postgres"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
"github.com/mohammad-farrokhnia/ledger/internal/store/postgres"
)

func newKey() string {
Expand Down
2 changes: 1 addition & 1 deletion internal/transport/grpc/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
)

func domainErrorToGRPC(err error) error {
Expand Down
6 changes: 3 additions & 3 deletions internal/transport/grpc/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

ledgerv1 "github.com/mohammad-farrokhnia/go-ledger/api/proto/ledger/v1"
"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
"github.com/mohammad-farrokhnia/go-ledger/internal/metrics"
ledgerv1 "github.com/mohammad-farrokhnia/ledger/api/proto/ledger/v1"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
"github.com/mohammad-farrokhnia/ledger/internal/metrics"
)

type Handler struct {
Expand Down
4 changes: 2 additions & 2 deletions internal/transport/grpc/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package grpc
import (
"google.golang.org/protobuf/types/known/timestamppb"

ledgerv1 "github.com/mohammad-farrokhnia/go-ledger/api/proto/ledger/v1"
"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
ledgerv1 "github.com/mohammad-farrokhnia/ledger/api/proto/ledger/v1"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
)

func accountToProto(a ledger.Account) *ledgerv1.Wallet {
Expand Down
4 changes: 2 additions & 2 deletions internal/transport/grpc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"

ledgerv1 "github.com/mohammad-farrokhnia/go-ledger/api/proto/ledger/v1"
"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
ledgerv1 "github.com/mohammad-farrokhnia/ledger/api/proto/ledger/v1"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
)

type Server struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/transport/http/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/encoding/protojson"

ledgerv1 "github.com/mohammad-farrokhnia/go-ledger/api/proto/ledger/v1"
ledgerv1 "github.com/mohammad-farrokhnia/ledger/api/proto/ledger/v1"
)

//go:embed swagger.json
Expand Down
6 changes: 3 additions & 3 deletions internal/transport/http/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/mohammad-farrokhnia/go-ledger/internal/i18n"
"github.com/mohammad-farrokhnia/go-ledger/internal/ledger"
"github.com/mohammad-farrokhnia/ledger/internal/i18n"
"github.com/mohammad-farrokhnia/ledger/internal/ledger"
)

var grpcToHTTP = map[codes.Code]int{
Expand Down Expand Up @@ -57,7 +57,7 @@ func newMeta(acceptLang string, code i18n.MessageCode) Meta {
}

const (
appName = "go-ledger"
appName = "ledger"
appVersion = "1.0.0"
)

Expand Down
2 changes: 1 addition & 1 deletion internal/transport/http/swagger.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"title": "go-ledger API",
"title": "ledger API",
"description": "A high-consistency, bank-grade financial transaction service.",
"version": "1.0"
},
Expand Down
2 changes: 1 addition & 1 deletion internal/transport/http/swagger.swagger.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"title": "go-ledger API",
"title": "ledger API",
"description": "A high-consistency, bank-grade financial transaction service.",
"version": "1.0"
},
Expand Down
Loading
Loading