service
Software architecture
airbar-finance
A dedicated Go finance service — ledger, escrow, wallet, and PSP callbacks — with dependency direction, idempotency, and a real promotion path.
Go · PostgreSQL · Redis · gRPC · Docker Compose
Airbar needed ledger entries, escrow, wallet balances, withdrawals, and payment-provider callbacks as their own service — not a module buried in the main application.
This repository is that service. It has its own schema (finance.*), its own image, and its own deploy path.
Why a separate finance service?
Financial writes have different failure modes from ordinary HTTP handlers. A PSP may retry the same callback. Ledger rows should not be an afterthought of the product API.
Splitting the service isolates:
- the public HTTP surface Zibal needs for browser redirects
- gRPC contracts consumed by
airbar-core - migrations and readiness probes that can fail without taking the rest of the product down
The cost is operational: another compose overlay, another health endpoint, another deploy.
Architecture
Dependency direction is the architecture:
delivery
↓
usecase
↓
domain
↑
infrastructure
domain does not import gRPC, pgx, Redis, or Zibal. Infrastructure implements the ports the domain declares. cmd/server is the composition root.
Inbound adapters:
| Surface | Role |
|---|---|
| gRPC | Escrow, payments, wallet, withdrawal, treasury, reconciliation, credit |
| HTTP | GET /health/ready, Zibal callback |
Health on both surfaces pings Postgres and Redis before calling the process ready.
Idempotency
A payment provider may send the same callback more than once. That is not a transport quirk to handle in one handler.
Mutating gRPC methods go through a unary interceptor. The key comes from metadata or the request body. The guard:
- looks in Redis (
idempotency:{key}, 24h TTL) - on miss,
INSERT … ON CONFLICT DO NOTHINGinfinance.idempotency_records - replays a stored snapshot, or runs the handler and completes
- on handler failure, deletes the in-flight row
Operational note
Postgres is the source of truth. Redis is a hot cache of snapshots, not the authorization to run the side effect twice.
Read RPCs and the HTTP Zibal callback are not behind that interceptor.
Delivery
Compose overlays share a base file. Dev builds locally and exposes ports. Staging and production pull GHCR and join a shared airbar-net; they do not bundle Postgres or Redis.
CI on the self-hosted runner: go mod verify, go vet, go test, go build, goose-backed integration tests, govulncheck. Staging deploy runs migrations and probes /health/ready.
What this demonstrates
Software architecture that survives contact with money: ports and adapters, a split transport surface, idempotency as a use case, and promotion via compose overlays rather than a single “works here” file.