Banking apps are a good test of an architecture diagram, because every box has a rule attached. Accounts must never go negative without a rule allowing it, a transfer must happen once and only once, and customers must see only their own data. This article draws one reference layout for a retail banking app and shows how TDK CLI could run its services locally.
This is an illustrative design. It is not taken from a real bank, and the repository on my machine has no bank application. The only banking service in the ERP example is banking-integration-api, which is a finance-side bank connection, not a customer-facing app.
Do not use this for real money without a security review and the compliance work your jurisdiction requires. A local stack with fake accounts is a teaching tool, not a bank.
The diagram
flowchart TD
CUST["Customer on a phone or browser"]
OPS["Bank operator console"]
subgraph EDGE["Edge: frontends and gateway"]
WEB["web-banking-app: frontend"]
OPSAPP["ops-console-app: frontend"]
GW["Traefik gateway with TLS in production"]
end
subgraph IDENT["Identity and access"]
AUTH["auth-api: login, sessions, MFA hooks"]
CUSTDATA["customer-profile-api"]
end
subgraph CORE["Core banking services"]
ACCTS["accounts-api: balances and account types"]
LEDGER["ledger-api: double-entry postings"]
TRANS["transfers-api: idempotent transfers"]
CARDS["cards-api: card status and limits"]
end
subgraph RISK["Risk and rules"]
FRAUD["fraud-rules-api: scoring and holds"]
LIMITS["limits-api: daily and per-transfer limits"]
end
subgraph INTEG["Integrations"]
PAYS["payments-gateway-api: external rails"]
NOTIFY["notifications-api: email and SMS"]
end
subgraph JOBS["Background work"]
STMT["statements-worker: monthly statements"]
RECON["reconciliation-worker: match external settlements"]
end
subgraph DATA["Data and messaging"]
PG[("PostgreSQL: one database per domain")]
NATS["NATS: transfer and posting events"]
end
subgraph AUDIT["Audit and observability"]
AUDLOG["audit-log-api: append-only record"]
TRACE["traces and logs with correlation IDs"]
end
CUST --> WEB
OPS --> OPSAPP
WEB --> GW
OPSAPP --> GW
GW --> AUTH
GW --> CUSTDATA
GW --> ACCTS
GW --> TRANS
GW --> CARDS
AUTH --> CUSTDATA
TRANS --> FRAUD
TRANS --> LIMITS
TRANS --> LEDGER
TRANS --> PAYS
LEDGER --> ACCTS
CARDS --> LIMITS
TRANS --> NATS
LEDGER --> NATS
NATS --> NOTIFY
NATS --> AUDLOG
NATS --> RECON
STMT --> LEDGER
ACCTS --> PG
LEDGER --> PG
TRANS --> PG
CUSTDATA --> PG
AUDLOG --> PG
CORE --> AUDLOG
CORE -.-> TRACE
AUTH -.-> TRACE
classDef money fill:#e8fbef,stroke:#15803d,color:#0f172a
classDef risk fill:#fdecec,stroke:#b91c1c,color:#0f172a
classDef audit fill:#fff4e5,stroke:#c2410c,color:#0f172a
class LEDGER,TRANS,ACCTS,CARDS money
class FRAUD,LIMITS risk
class AUDLOG,TRACE audit
How to read it
Customers and operators reach the system through two frontends. Both pass through one gateway, which in production would terminate TLS and handle authentication before a request reaches a service.
The core services are the money path. The ledger records every movement as a double-entry posting, so each transfer debits one account and credits another, and the books always balance. Transfers go through the fraud and limit checks before they post. Idempotency matters here: a retried transfer must not move money twice, so the transfers service keys each request.
Events on NATS carry transfers and postings to the services that react to them: notifications, the audit log, and reconciliation. Statement generation reads from the ledger on a schedule, as a background worker.
The audit log is append-only. Nothing in the diagram writes to it except the event flow, and nothing edits or deletes its rows. Traces and logs carry a correlation ID, so a single transfer can be followed across services.
Mapping it to TDK CLI
In a TDK project, each box in the diagram becomes a resource. The frontends are frontend resources, the APIs are backend resources, and the statements and reconciliation jobs are worker resources. Each gets a service.json, and dependsOn declares the order: the database first, then the ledger, then the transfers service that posts to it.
TDK runs the local stack and checks it before startup. The drift check stops a hand edit to generated files, the dry run prints the start order, and readiness output shows which services are up. Those checks help a team keep the local system consistent. They do not make the design safe.
tdk up main --dry-run
tdk status --json
What the diagram does not cover
It leaves out the controls a real bank needs: customer identity verification, regulatory reporting, key management, backup and disaster recovery, penetration testing, and the rules for storing account data. Each of those needs a specialist, and none is solved by a diagram.
It also says nothing about performance. A transfer service under load is a different problem from a transfer service on a laptop.
Summary
A retail banking app is a set of services around a double-entry ledger, with transfers, limits, fraud checks, an append-only audit log, and events that connect them. Drawn as one diagram, it shows where the money moves and where the checks sit. Use it to teach the layout and to build a local sandbox with fake accounts. Anything closer to production needs a security review and the compliance work that applies where you operate.
Top comments (1)
cool!