Building a bank you can actually trust.
A banking app has one job above all others: never let money move when it shouldn't. I built one end-to-end — accounts, transfers, deposits, transaction history — and most of the interesting work wasn't the features. It was the security model underneath them.
01Why banking is a security problem first
Most CRUD apps treat security as a layer you add at the end. A banking system can't. Every endpoint that touches a balance is a potential exploit, and the difference between a customer reading their own statement and reading someone else's is one missing authorization check. So I designed the app from the access model outward, not the features inward.
The core question I kept returning to: for every action, who is allowed to do this, and how do I prove it on the server? Never trusting the client to answer that is the whole game.
02The authentication model
I used Spring Security with JWT. When a user logs in, the server issues a signed
token carrying their identity and role. Every subsequent request presents that token, and the server
verifies the signature before doing anything else. Because the token is signed, the client can't tamper
with it to promote itself — flipping a customer claim to admin breaks the
signature and the request is rejected.
The reason JWT fits banking well is that it's stateless: the server doesn't have to store a session for every logged-in user, which keeps it simpler to scale. The trade-off is that tokens are valid until they expire, so the design has to keep their lifetime short and scope them carefully.
Authentication answers "who are you?" Authorization answers "what are you allowed to do?" Conflating the two is how data leaks happen. A logged-in customer is authenticated — that doesn't mean they're authorized to touch the admin dashboard.
03Role-scoped access
The app has two kinds of users — customers and admins — and they see fundamentally different systems. A customer manages their own accounts and transfers. An admin oversees the platform. Rather than hide admin features in the UI and hope nobody finds the endpoint, every protected route checks the role server-side. The UI hiding is a convenience; the server check is the actual security boundary.
04How a transfer actually flows
Authenticated request
The React dashboard sends the transfer request with the user's JWT attached.
Verify & authorize
Spring Security validates the token and confirms the user owns the source account.
Execute the transaction
The service debits one account and credits the other — as a single unit, so a partial transfer can't leave money in limbo.
Persist & record
MySQL stores the updated balances plus an immutable entry in the transaction history.
05Why MySQL for the money
Financial data is the textbook case for a relational database. Balances, transactions, and audit logs have strict relationships and demand consistency — you never want two transfers to race and corrupt a balance. A relational store with proper transactional guarantees is exactly the right tool, which is why I chose MySQL over anything looser. The transaction history doubles as an audit trail: every movement is recorded, nothing is silently overwritten.
06What I took away
The lesson that stuck: in a system that handles money, the boring parts are the important parts. Anyone can build a transfer form. Making sure that form can't be used to move money that isn't yours — that's the engineering. Designing from the access model outward, and never trusting the client to enforce its own permissions, is a habit I carry into everything I build now.