Building a Desktop Banking App With Java Swing
How I built a full-featured banking application with Java Swing, AWT, and MySQL — account management, transactions, and a clean MVC architecture, all without a framework.
title: "Building a Desktop Banking App With Java Swing" date: "2024-01-15" description: "How I built a full-featured banking application with Java Swing, AWT, and MySQL — account management, transactions, and a clean MVC architecture, all without a framework." tags: ["java", "backend", "mysql"] published: true image: ""
Desktop GUI development feels like a relic in a world of React and Flutter, but when I decided to build a banking application as part of my academic work, I chose Java Swing deliberately. Not because it's trendy — it very much isn't — but because it forces you to think clearly about separation of concerns without a framework doing the heavy lifting for you.
This post is a retrospective on what I built, what broke, and what I'd do differently.
Architecture Overview
The application follows a three-layer architecture. I didn't call it MVC at the time — I just tried not to make a mess — but looking back, that's essentially what it is:
The key discipline was making sure my Swing panels never touched the database directly. Every UI action — a button click, a form submit — fired a method on a service class. The service handled business rules, then delegated to a DAO. It sounds obvious on paper, but when you're tired and Swing's event dispatch thread is already confusing you, the temptation to just write ResultSet rs = conn.executeQuery(...) inside an ActionListener is real.
Database Design
The schema ended up being four core tables:
One design decision I debated for a while: should transfers be one transaction record or two? I went with one row per transfer with both from_account and to_account populated. This made querying a single account's history slightly more complex (you need to WHERE from_account = ? OR to_account = ?) but it kept the data clean and prevented orphaned half-records.
Core Features Implementation
The authentication screen was the entry point. Password storage used SHA-256 hashing — not bcrypt, which I'd do differently today, but at least I wasn't storing plaintext:
The PreparedStatement usage here was non-negotiable. Early in development I saw a classmate using string concatenation to build SQL queries and nearly had a breakdown explaining SQL injection to them.
The dashboard screen used a JTabbedPane to separate concerns visually — one tab for account overview, one for transaction history, one for transfers. Each tab was its own JPanel subclass with its own refresh logic. This kept things manageable.
Handling Concurrent Transactions
This is where it got genuinely tricky. In a real banking system, concurrent transactions on the same account are a serious correctness problem. My app wasn't going to see real concurrency, but I wanted to model it correctly anyway.
The solution was database-level locking with a transaction:
The FOR UPDATE lock ensures that if two transfers from the same account somehow fire simultaneously, one waits for the other to complete before reading the balance. The full try-catch-finally for connection cleanup is verbose but necessary — connection leaks in a pool are silent killers.
Swing Layout: The Pain Points
Swing's layout managers deserve their own post, but briefly: GridBagLayout is powerful and maddening. I ended up using a combination of BorderLayout for top-level panels and GridLayout or FlowLayout for forms. The login dialog used a custom GridBagLayout setup that took me an embarrassingly long time to get centered properly on the screen:
The insets are what make it look like an actual UI instead of components jammed against each other. I learned this the hard way.
What I Learned
A few things stuck with me from this project:
Always use BigDecimal for money. Never float or double. Floating-point rounding errors in financial calculations are not hypothetical — they are guaranteed.
Connection pooling matters even in small apps. Opening a new Connection for every query in Swing's event dispatch thread blocks the UI. I wrote a bare-minimum pool using a LinkedList<Connection> with synchronized getConnection() / releaseConnection() methods, and it made a noticeable difference.
Swing runs on the EDT (Event Dispatch Thread). Any long-running work — database queries, file I/O — must happen off the EDT. I used SwingWorker for this, which has a slightly awkward API but does the job.
The discipline of "never touch the database from a UI class" saved me from a huge mess when I later needed to add input validation. All validation logic lived in the service layer already — I just had to call it.
What I'd Do Differently
If I built this today: bcrypt for passwords (not SHA-256), a proper connection pool like HikariCP instead of my handwritten one, and I'd probably swap Swing for JavaFX which has a more modern approach to declarative UI. I'd also add unit tests for the service layer — completely untested code in a banking app feels wrong in retrospect, even for an academic project.
The project gave me a strong foundation in JDBC, SQL transactions, and the discipline of layered architecture. Those things transfer cleanly to Spring Boot, which I used in later projects.