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.
- 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.
FastAPI (async), SQLAlchemy with async sessions, PostgreSQL, Alembic migrations, pytest, Docker.
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 serviceEvery 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.
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.
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) |
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/
- 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.