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.