Bank Account Management System
A console-based banking system exploring the concepts of OOP and separation of concerns.
Bank Account Management System
I wrote a console-based banking application to understand OOP principles and separation of concerns at a practical level. The system handles account creation, transaction processing, and role-based access control using a layered architecture that separates models, controllers, and repositories.
Having come from an SRC Architecture pattern, I knew to separate application concerns even though this was a small console application.
What it does
The application supports:
- Account creation for Savings and Checking accounts
- Customer types with different privileges (Regular vs Premium)
- Transaction processing with deposit and withdrawal operations
- Role-based access (Customer vs Manager)
- Transaction confirmation flow with preview
- Account viewing with proper authorization
- Dynamic array management with auto-resizing
Key features:
- Premium customers pay $10,000 upfront but get no minimum balance requirements, no monthly fees, and higher transaction limits ($50,000 vs $10,000)
- Savings accounts enforce a $500 minimum balance for regular customers and apply 3.5% annual interest
- Checking accounts allow $1,000 overdraft and charge $10 monthly fees (waived for premium)
- Managers can view all accounts and transactions; customers can only see their own
- Every transaction shows a confirmation preview before execution
Architecture
The system uses a three-layer architecture adapted from MVC for console applications:
Models layer (com/bank/models/)
Represents domain entities and business logic. Models define data structure, encapsulate business rules, and implement core operations. They contain no knowledge of storage or presentation.
Key models:
Account.java- Abstract base class with common properties and operationsSavingsAccount.java/CheckingAccount.java- Concrete implementations with specific rulesCustomer.java- Customer and manager entities with role-based attributesTransaction.java- Transaction records with status tracking
Business logic example from SavingsAccount:
@Override
public void withdraw(double amount) {
double currentBalance = checkBalance();
if (getAccountHolder().getCustomerType() == CustomerType.PREMIUM) {
super.withdraw(amount);
} else {
if ((currentBalance - amount) >= MIN_BALANCE) {
super.withdraw(amount);
} else {
System.out.println("Cannot withdraw: Would fall below minimum balance");
}
}
}
Controllers layer (com/bank/controllers/)
Handles user interaction and application flow. Controllers receive input, validate data, coordinate between repositories and models, and format output. They do not contain business logic.
Key controllers:
MenuController.java- Main application flow and navigationAccountController.java- Account creation and viewing workflowsTransactionController.java- Transaction processing workflows
Controller coordination example:
public void createAccount() {
// Get user input
System.out.print("Enter initial deposit: ");
double deposit = scanner.nextDouble();
// Create model (business logic here)
SavingsAccount account = new SavingsAccount(accountNumber, customer, deposit);
// Delegate storage to repository
accountManager.addAccount(account);
// Display result
System.out.println(account.getCreationMessage());
}
Repository layer (com/bank/repository/)
Manages data storage and retrieval. Repositories abstract storage implementation, provide CRUD operations, and handle data access logic. They do not contain business logic.
Key repositories:
AccountManager.java- Stores and retrieves accountsCustomerManager.java- Manages customer recordsTransactionManager.java- Maintains transaction history
Repository storage example:
public void addAccount(Account account) {
if (accountCount >= accounts.length) {
resizeArray(); // Infrastructure concern
}
accounts[accountCount++] = account; // Storage operation
}
Why this separation matters
Each layer has one responsibility:
- Models handle business logic and domain rules
- Controllers handle user interaction and flow control
- Repositories handle data storage and retrieval
Benefits:
- Changing storage (arrays to database) only affects repositories
- Changing business rules only affects models
- Changing UI (console to GUI) only affects controllers
- Testing business logic independently of UI or storage
- Adding features without scattered changes across the codebase
OOP principles applied
Abstraction
Account is abstract because there is no generic account in real banking. You have savings accounts or checking accounts, never just an account. The abstract class forces all concrete implementations to define their own withdrawal logic.
Inheritance
Both SavingsAccount and CheckingAccount extend Account. They inherit common properties like account number, balance, and customer, but each implements its own rules.
Polymorphism
Runtime behavior selection based on object type:
Account account = accountManager.findAccount(accountNumber);
account.withdraw(500); // Which withdraw() runs?
The JVM determines at runtime whether to call SavingsAccount.withdraw() or CheckingAccount.withdraw(). Same method call, different behavior.
Encapsulation
All fields are private. You cannot directly modify an account's balance. You must use deposit() and withdraw(), which enforce business rules.
private double balance; // Cannot access directly
public void deposit(double amount) {
if (amount > 0) {
balance += amount; // Controlled access
}
}
Implementation details
Static vs instance variables
Early on I initialized arrays inside methods. Every method call created a new array, wiping all stored data. The fix was understanding object lifecycle:
// Wrong
public void addAccount(Account account) {
Account[] accounts = new Account[50]; // New array every call
// ...
}
// Correct
public class AccountManager {
private Account[] accounts; // Instance variable
public AccountManager() {
this.accounts = new Account[50]; // Initialize once
}
}
Key insight:
- Static fields are shared across all class instances (customer ID counter)
- Instance fields are unique to each object but persist across method calls
- Initialize collections in constructors, not in methods
Dynamic array resizing
Repositories use arrays with automatic capacity doubling:
private void resizeArray() {
Account[] newAccounts = new Account[accounts.length * 2];
System.arraycopy(accounts, 0, newAccounts, 0, accounts.length);
accounts = newAccounts;
}
Transaction confirmation flow
Every transaction displays a preview before execution:
TRANSACTION CONFIRMATION
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Transaction ID: TXN1733001234567
Account: C414
Type: WITHDRAWAL
Amount: $500.00
Current Balance: $25,000.00
New Balance: $24,500.00
Date: 2025-11-30
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Confirm Transaction? (Y/N):
This preview-confirm pattern prevents accidental operations and improves user experience even in console applications.
Role-based access control
Simple but effective authorization using ID prefixes:
if (!userId.startsWith("MGR")) {
System.out.println("ā Access Denied: Only managers can view all accounts.");
return;
}
Managers get IDs like MGR00001, customers get CUST00001. Not production-grade security, but demonstrates the authorization concept.
Challenges
Volume not persisting data
Initializing arrays inside methods created new instances on every call. All previous data was lost. Fixed by moving array initialization to constructors and using instance variables.
Transaction ID bug
Originally made transactionId static, which caused all transactions to share the same ID. The most recent transaction ID would overwrite previous ones. Fixed by making it an instance variable so each transaction has its own unique ID.
Separation of validation logic
Business rules belong in models, not controllers. Minimum balance checks go in SavingsAccount, not in AccountController. This separation makes the code testable and maintainable.
What I would add next
- Database integration to replace in-memory arrays
- Proper authentication instead of simple ID checks
- Account transfer functionality between accounts
- Unit tests for business logic
- REST API to turn it into a backend service
- Proper auto-incrementing ID generation
- Extract
TransactionStatusenum into its own file for consistency
Closing thoughts
This project covers the fundamentals: inheritance, polymorphism, encapsulation, abstraction, and architectural patterns. The layered approach makes the code maintainable and testable. The business logic is isolated in models, the UI flow is handled in controllers, and the storage is abstracted in repositories.
If you are learning OOP, build something like this. Make the mistakes. Debug the issues. Understanding comes from implementation, not from reading about principles.