Project Write-up · Full-Stack

Building a bank you can actually trust.

Rahul Bhushan Vemula· ~6 min read· JavaSpring BootReactMySQL
MySQL

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.

The key principle

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

1

Authenticated request

The React dashboard sends the transfer request with the user's JWT attached.

2

Verify & authorize

Spring Security validates the token and confirms the user owns the source account.

3

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.

4

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.

Want to see it running?

There's a live demo, or I'm happy to walk through the security model.