Exact settlement. Immutable ledger. Operational trust.
Overview | Console | Metrics | Quickstart | Demo Guide | OpenAPI
When a debt investment matures, someone must answer a hard question with money on the line: how much is owed, to whom, and has it been paid exactly once?
Most demo backends stop at CRUD. Production finance cannot. A duplicate payout is not a bug report. It is a reconciliation crisis. A missing ledger line is not a log gap. It is an audit failure. Arrakin exists in that gap between tutorial apps and real settlement infrastructure.
Arrakin is a reliability first settlement engine for matured investments. It detects due maturities, calculates principal, return, fees, and withholding in integer cents, enqueues payout jobs, processes them with concurrent workers under row level locking, writes append only double entry ledger records in the same database transaction as payout completion, retries transient failures with backoff, routes terminal failures to dead letter, and gives operators reconciliation and audit visibility through a typed REST API and internal console.
No duplicate completion under concurrency. No floating point money paths. No silent state changes.
- Preview
- What is Arrakin?
- The Problem
- How Settlement Works
- Operations Console
- By the Numbers
- Technology Stack
- Architecture
- Settlement Lifecycle
- Scheduler and Worker Flow
- API Surface
- Getting Started
- Demo and Verification
- CI Pipeline
- Repository Structure
- Documentation
- License
Arrakin is backend financial infrastructure for a debt investment platform at the moment of maturity.
| Stage | What happens |
|---|---|
| Detect | Scheduler scans due maturity_schedules and enqueues settlement jobs |
| Calculate | Engine computes gross return, platform fee, withholding tax, net payout |
| Process | Worker pool claims jobs with FOR UPDATE SKIP LOCKED, simulates payout gateway |
| Record | Four balanced ledger entries posted atomically with successful payout |
| Recover | Exponential backoff on transient errors; dead letter on terminal failure |
| Verify | Reconciliation snapshots compare expected vs settled totals |
| Observe | Audit events and admin API for every meaningful state transition |
The included React operations console is a client of the same API an SRE or finance ops team would use. The product is the engine, not a landing page.
Debt platforms must settle matured positions exactly once while remaining auditable under failure, retries, and concurrent workers.
| Failure mode | Arrakin response |
|---|---|
| Duplicate scheduler tick | Unique constraint + idempotent enqueue key per maturity |
| Two workers claim same job | SKIP LOCKED + transactional status transitions |
| Duplicate payout completion | Unique payout_reference per completed payout |
| Duplicate admin POST | Idempotency-Key header with stored response replay |
| Transient gateway error | Retry with backoff, capped attempts |
| Terminal gateway error | Dead letter until explicit admin replay |
| Amount drift | Reconciliation flags: amount_mismatch, missing_ledger, orphan_ledger, stale_pending |
All amounts are stored as BIGINT USD cents. The calculator uses basis points for fee and tax. Every successful job produces four ledger lines (debits and credits balance). Processing runs inside a single Postgres transaction spanning job status, payout attempt row, and ledger insert.
Demo seed (make seed) ships five investments with deliberate outcomes:
| Investment | Simulation profile | Expected result |
|---|---|---|
| INV DEMO 001 | success |
Succeeds on first attempt |
| INV DEMO 002 | transient_then_success |
Retries, then succeeds |
| INV DEMO 003 | terminal_failure |
Ends in dead letter |
| INV DEMO 004 | success |
Additional succeeded volume |
| INV DEMO 005 | (none) | Production style success path |
Internal ops UI wired to the same REST API. Built with React, TypeScript, and typed API clients.
| Operations Overview | Settlement Jobs |
|---|---|
|
|
| Ledger Entries | Reconciliation |
|
|
| Audit Log | |
|
|
| Console route | Purpose |
|---|---|
/ |
Pipeline health, reconciliation cards, scheduler trigger |
/jobs |
Filterable job queue with status and pagination |
/jobs/:id |
Amounts, payout attempts, replay and requeue |
/ledger |
Entry list with job and account filters |
/reconciliation |
Latest snapshot, run snapshot, history |
/audit |
Lifecycle and admin action timeline |
| Metric | Value | Context |
|---|---|---|
| Concurrent workers | 4 | Configurable WORKER_COUNT; tested with parallel job completion |
| Jobs per concurrency test | 5 | Zero duplicate ledger postings under contention |
| Ledger lines per settlement | 4 | Balanced double entry per succeeded job |
| Max payout retries | 5 | Exponential backoff before dead letter |
| Demo investments seeded | 5 | Success, retry, and terminal failure paths |
| Integration test scenarios | 15+ | Scheduler, ledger, retry, dead letter, HTTP idempotency |
| HTTP API version | v1 | Settlement, ledger, reconciliation, audit, admin |
| Money representation | BIGINT cents | No floating point in settlement paths |
| Idempotency layers | 3 | HTTP keys, maturity enqueue keys, payout reference |
Resume style impact (backend):
| Accomplishment (X) | Measurement (Y) | Method (Z) |
|---|---|---|
| Go settlement engine | 4 workers, 5 parallel jobs, 0 duplicate completions | Postgres SKIP LOCKED, transactional sqlc |
| REST ops API + CI | 15+ integration tests, automated pipeline on every push | Gin, idempotency store, GitHub Actions |
| Layer | Choices | Role |
|---|---|---|
| Language | Go 1.24+ | Single binary: API + scheduler + workers |
| HTTP | Gin, OpenAPI (swag) | Versioned REST, Swagger UI at /swagger |
| Database | PostgreSQL 16 | Queue, ledger, idempotency, audit (source of truth) |
| Cache / lock | Redis 7 | Scheduler leader lock, health checks |
| Data access | sqlc, golang migrate | Typed SQL, schema migrations |
| Observability | log/slog JSON | Structured logs with request_id, job_id |
| Admin UI | React, TypeScript, Vite | Ops console consuming /api/v1 |
| Testing | go test, Docker Compose | Unit + tagged integration suite |
| API tooling | Bruno collection | Runnable requests under api/bruno/ |
flowchart TB
subgraph Clients
UI[React Ops Console]
CLI[curl / Bruno]
end
subgraph Arrakin["Arrakin monolith (Go)"]
API[Gin HTTP API]
SCH[Scheduler]
WRK[Worker pool x4]
CALC[Settlement calculator]
GW[Payout gateway simulator]
LED[Ledger posting]
REC[Reconciliation]
end
subgraph Data
PG[(PostgreSQL 16)]
RD[(Redis 7)]
end
UI --> API
CLI --> API
API --> PG
SCH --> PG
SCH --> RD
WRK --> PG
WRK --> CALC
WRK --> GW
WRK --> LED
REC --> PG
LED --> PG
| Design choice | Rationale |
|---|---|
| Postgres backed queue | ACID with ledger; no queue drift |
| Monolith first | Faster local dev; modules extractable later |
| Integer cents | Eliminates float rounding in money paths |
| Append only ledger | Corrections are new entries, not updates |
| Simulated payout gateway | Interface ready for ACH/wire adapter |
stateDiagram-v2
[*] --> pending: Scheduler enqueues job
pending --> processing: Worker claims (SKIP LOCKED)
processing --> succeeded: Payout OK + ledger posted
processing --> failed: Transient error
failed --> processing: Backoff elapsed, requeue
failed --> dead_letter: Max retries exceeded
processing --> dead_letter: Terminal payout error
dead_letter --> pending: Admin replay
succeeded --> [*]
| Status | Meaning |
|---|---|
pending |
Job created, awaiting worker |
processing |
Worker holds lease |
succeeded |
Payout complete, ledger written |
failed |
Retry scheduled |
dead_letter |
Requires operator intervention |
sequenceDiagram
participant S as Scheduler
participant DB as PostgreSQL
participant W as Worker
participant G as Payout gateway
participant L as Ledger
S->>DB: Scan due maturities
S->>DB: Create settlement job (idempotent)
W->>DB: Claim next job FOR UPDATE SKIP LOCKED
W->>DB: Record payout attempt
W->>G: Execute payout
alt success
G-->>W: payout_reference
W->>L: Post 4 balanced entries
W->>DB: Mark succeeded (single TX)
else transient failure
G-->>W: retryable error
W->>DB: Increment retry, set next_retry_at
else terminal failure
G-->>W: terminal error
W->>DB: Move to dead_letter
end
Base URL: http://localhost:8080/api/v1
| Method | Path | Description |
|---|---|---|
GET |
/settlement-jobs |
List jobs (status, investment, cursor) |
GET |
/settlement-jobs/{id} |
Job detail |
GET |
/settlement-jobs/{id}/attempts |
Payout attempt history |
POST |
/settlement-jobs/{id}/replay |
Dead letter → pending |
POST |
/settlement-jobs/{id}/requeue |
Failed → pending |
GET |
/ledger/entries |
Ledger lines with filters |
GET |
/ledger/accounts |
Account catalog |
GET |
/reconciliation/latest |
Latest snapshot |
GET |
/reconciliation/snapshots |
Snapshot history |
POST |
/reconciliation/run |
On demand snapshot |
GET |
/audit/events |
Audit timeline |
POST |
/admin/scheduler/tick |
Manual maturity scan |
Health: GET /healthz, GET /readyz · Docs: GET /swagger/index.html
Mutating POST routes accept Idempotency-Key. Admin routes accept X-API-Key (bypassed in APP_ENV=development).
| Tool | Version |
|---|---|
| Go | 1.24+ |
| Docker + Compose | latest |
| golang migrate | CLI |
| sqlc | CLI (after schema changes) |
| Node.js | 18+ (admin UI only) |
cp .env.example .env
make docker-up
make migrate-up
make seed
make runVerify:
curl -s http://localhost:8080/healthz | jq .
curl -s http://localhost:8080/readyz | jq .Server listens on port 8080.
cd web/admin
cp .env.example .env
npm install
npm run devOpen http://localhost:5173 · API proxied to :8080
make demoBootstraps Docker, migrates, seeds, starts API, runs curl walkthrough. See DEMO.md for the full narrative.
| Command | What it does |
|---|---|
make test |
Unit tests (no database required) |
make test-integration |
15+ Postgres integration scenarios |
make verify |
Unit + integration + admin production build |
make demo |
End to end API demonstration |
make admin-build |
Typecheck and bundle admin UI |
make swagger |
Regenerate OpenAPI from annotations |
Suggested first demo path
make seedthenmake runPOST /api/v1/admin/scheduler/tickwith an idempotency key- Filter jobs: succeeded vs dead letter
- Open job detail → payout attempts
POST /reconciliation/run→ inspect discrepancy flags- Replay dead letter job → observe audit log
Import api/bruno/ into Bruno for a clickable collection.
flowchart LR
A[Push to main] --> B[Lint + unit tests]
B --> C[Integration tests]
C --> D[Build API binary]
E[Admin job] --> F[npm ci + vite build]
GitHub Actions (.github/workflows/ci.yml):
| Job | Steps |
|---|---|
| backend | Postgres 16 + Redis 7 services → migrate → unit tests → integration tests → make build |
| admin | npm ci → make admin-build |
cmd/arrakin/ Application entrypoint
internal/
api/ Gin routes, handlers, DTOs
scheduler/ Maturity scan and enqueue
worker/ Job processor and pool
settlement/ Calculator, payout gateway, retry
ledger/ Double entry posting
reconciliation/ Snapshot builder
audit/ Event publisher
integration/ End to end flow tests
store/ sqlc repositories
web/admin/ React operations console
api/bruno/ API request collection
migrations/ SQL schema history
seeds/ Demo investors and investments
specs/ Engineering specification
docs/ OpenAPI + production notes
scripts/demo.sh Automated demo script
| Document | Description |
|---|---|
| DEMO.md | Step by step demo walkthrough |
| docs/PRODUCTION.md | Locking, idempotency, audit, deferred items |
| specs/implementation-spec.md | Full technical specification |
| web/admin/README.md | Admin UI setup |
See LICENSE.
Built for engineers who care what happens to money after the HTTP response returns.





