DEV Community

tdk-cli
tdk-cli

Posted on

The Bank App Architecture Diagram: How a Retail Banking Stack Could Run Locally With TDK CLI

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
tdk-cli profile image
tdk-cli •

cool!