Skip to content

Repository files navigation

ledger-service

A double-entry ledger and transfer API in FastAPI, containerised and running on AWS Fargate.

The premise is simple: money never appears or disappears, it only moves between accounts. Every transfer is recorded as a balanced journal entry, so the answer to "where did this money go" is always in the data rather than in someone's memory. Around that sit the things that decide whether a financial service is trustworthy in practice: idempotent writes, an append-only audit trail, reversals that never delete history, and a reconciliation endpoint that compares the ledger against an external statement and reports the difference.

What it does

  • Double-entry core. Accounts, journal entries, and postings, with debits and credits forced to balance at the database level. A transfer that would unbalance the books cannot be committed.
  • Idempotent transfers. Clients send an Idempotency-Key; a retry returns the original result instead of moving money twice. Keys are stored with the request fingerprint, so the same key with a different body is rejected rather than silently accepted.
  • Exact amounts. Money is stored as integer minor units with an explicit currency. No floats anywhere in the money path.
  • Lifecycle, not deletion. Transfers move pending -> posted -> settled, and a mistake is undone with a compensating reversal entry. Nothing is ever deleted or edited in place.
  • Balances. Derived from postings, with point-in-time queries ("what was this balance on this date") rather than a mutable balance column that can drift.
  • Audit trail. Append-only record of who did what and when, attached to every state change.
  • Screening hook. A small rule-based check runs before a transfer posts (limits, blocked counterparties), returning an allow/hold decision that is recorded with the entry. It is a hook and a worked example, not a compliance product.
  • Reconciliation. Feed in an external statement (CSV or JSON) and get back a diff: matched, missing on either side, and amount mismatches, located to the specific entry.

Stack

FastAPI (async), SQLAlchemy with async sessions, PostgreSQL, Alembic migrations, pytest, Docker.

Running on AWS

The deployment is the point as much as the code, so it is checked in rather than described:

  • Container: multi-stage Dockerfile, non-root user, health endpoint at /healthz.
  • ECS Fargate: task definition (CPU/memory sizing, log configuration), a service behind an Application Load Balancer with the target group health check pointed at /healthz.
  • Data: RDS PostgreSQL; database credentials come from Secrets Manager and are injected as task secrets, never baked into the image.
  • Logs and metrics: CloudWatch log group per service, structured JSON log lines.
  • Scaling: service autoscaling on CPU and request count, with a sane minimum for availability.
  • Pipeline: GitHub Actions builds the image, pushes to ECR, and updates the ECS service; Alembic migrations run as a one-off task before the new revision goes live.
# local
docker compose up          # api + postgres
alembic upgrade head
pytest

# deploy
make image                 # build + push to ECR
make deploy                # register task definition revision + update service

Every AWS identifier in this repo is a placeholder. Account IDs, ARNs, registry URLs and secret names come from environment variables or Secrets Manager at deploy time.

Tests

Cover the parts that actually cost money when wrong: balanced-entry enforcement, idempotent replay (same key twice moves money once), concurrent transfers on the same account, reversal correctness, point-in-time balances, and reconciliation against a statement with deliberately injected breaks.

Evidence (to be filled with real captured output)

Filled in by deploying and running it. Real captures, not edited. Nothing in this table is filled in yet.

Item Result
ECS service running (task count, health) (fill: console screenshot)
Idempotent replay (same key x2) (fill: one posting, second returns original)
Concurrent transfer test (fill: final balance correct under N parallel writes)
Reconciliation on a statement with 3 injected breaks (fill: all 3 located)
Autoscaling under load (fill: task count vs request rate)

Repository layout

ledger-service/
  app/
    main.py            FastAPI app and routes
    ledger.py          double-entry core (entries, postings, balancing)
    transfers.py       transfer lifecycle + idempotency
    screening.py       pre-post rule hook
    reconcile.py       statement diff
    models.py          SQLAlchemy models
    db.py              async session/engine
  migrations/          Alembic
  deploy/
    Dockerfile
    task-definition.json
    service.tf         (or CloudFormation) ALB, service, autoscaling
  .github/workflows/   build, test, push to ECR, update service
  tests/

Notes

  • Synthetic data only. No real account data, no real money, no bank-specific logic.
  • The interesting parts are the invariants (books always balance, retries never double-apply, history is append-only), not the endpoint count.

MIT licensed.

About

Double-entry ledger API where the books always balance, retries never double-apply, and history is append-only. FastAPI, Postgres, ECS Fargate.

Resources

Stars

Watchers

Forks

Contributors

Languages