What's Actually Running in My Homemade Banking System — A Tour Through the Structure, Screens, and Core Code

Java Spring Boot 金融 アーキテクチャ 認証 API

Introduction

I’m building a banking-style system on my own, and actually running it, not just designing it. The whole flow — holding an account, checking balances, depositing, withdrawing, transferring — works end to end, from screen to API to database. The process from requirements through design is covered in Building a Banking API That Starts From Design.

This article follows on from that, and takes a tour of “what’s actually running” through the structure, screens, and core code. The security and implementation articles I write from here on assume this tour as a given. Laying the common groundwork here means I don’t have to re-explain the details every time.


Overall structure

Behind the user-facing screen, several services split up the responsibilities and run. The screen never touches the database directly; a layer in between (BFF) terminates authentication and bundles up the calls to each service.

   User (browser / phone)


   ┌──────────────────────────────────────────┐
   │ BFF: bundles the API calls behind the screen, terminates authentication │
   └──────────────────────────────────────────┘

          ├─▶ Auth service (login, identity verification)
          ├─▶ Account service (balance, deposits/withdrawals, transfers)
          └─▶ Notification service (transaction notifications)

               Each service has its own database

Rather than one monolith, auth, accounts, and notifications are split into separate services. Keeping the boundaries separate makes it harder for one side’s concerns to bleed into the other.


The screen

Customers use it from a browser (React). Login isn’t a password — it’s a passkey (FIDO/WebAuthn).

Login screen (passkey / FIDO — demo data for this project)

Once login succeeds, the home screen lists the user’s own accounts and balances. The security story I write after this one is set exactly at this point — “operating on your own account.”

Home screen (your own accounts and balances — demo data for this project. Not real names or balances)

The screen works cleanly, but what matters is what’s behind it. Confirming who is logged in, and making sure that person can only touch their own accounts — that’s the backbone of this system.


Authentication and the “caller”

Once login succeeds, the system carries that person forward as the “caller” (principal). From then on, every API call looks at this caller on every request and decides “is this person allowed to do this, to this target.”

   Login ─▶ establish the caller (principal)

                     ├─▶ Read (GET): return only the caller's own accounts
                     └─▶ Write (POST): let them operate only on their own accounts

                       (this is where the later security article is set)

What matters here is that “login succeeded” and “you may touch this account” are separate judgments. The former is authentication, the latter is authorization. Someone who logged in isn’t automatically allowed to touch someone else’s account.


Core code: the ownership check on the read API

For a read API (GET) like a balance inquiry, this “only your own accounts” rule was established early on. It checks whether the caller extracted from authentication matches the owner of the target account, and refuses if they don’t match.

@GetMapping("/accounts/{id}/balance")
public BalanceResponse balance(@PathVariable String id,
                               @AuthenticationPrincipal Jwt principal) {

    String me = principal.getSubject();          // the caller, established by authentication
    Account account = accounts.findById(id);

    // Ownership check: does the target account belong to the caller?
    if (!account.getCustomerId().equals(me)) {
        throw new AccessDeniedException("not your account");
    }
    return BalanceResponse.of(account);
}

Short, but the backbone is packed in here. principal is the caller established by authentication; account.getCustomerId() is the account’s owner. Match the two, and refuse if they don’t line up. The read side was protected in this shape.

The problem is that this “shape that was protected” doesn’t automatically carry over to the write side (deposits, withdrawals, transfers) — and that story continues in the next article.


Where this article sits

That’s the full tour of “what this system is”: the structure (auth, accounts, and notifications kept separate), the screens (login, operating on accounts), the backbone (establish the caller, protect with ownership checks). The articles from here on build on this foundation, handling one pitfall or design decision from the implementation at a time.


Feel free to send a message

Job offers, project referrals, feedback, questions — anything is welcome. I sincerely hope to connect with people who share high ambitions. I will keep taking on the challenges I have staked my life on. Thank you very much.