Java Notes
Build a Banking System with Java covering account management, transactions, fund transfers, and statement generation with ACID compliance.
Project Overview
The Banking System is a comprehensive Java-based application that simulates real-world banking operations including account creation, deposits, withdrawals, fund transfers, mini-statement generation, and loan management. This project demonstrates core Java concepts like OOP, exception handling, JDBC/JPA, and transaction management with ACID compliance.
This project is ideal for:
- Computer Science students building portfolio projects
- Java developers learning Spring Boot and REST APIs
- Anyone preparing for Java developer interviews
- Developers wanting to understand transaction management and ACID properties
The system supports multiple account types (Savings, Current, Fixed Deposit), implements role-based access control (Admin, Manager, Customer), and provides comprehensive reporting with PDF statement generation.
Features
- Account Creation — Create savings, current, or fixed deposit accounts with KYC verification
- Deposit Money — Deposit funds with real-time balance update and transaction logging
- Withdraw Money — Withdraw with minimum balance validation and overdraft protection
- Fund Transfer — Transfer between accounts (NEFT/IMPS/RTGS simulation) with ACID compliance
- Mini Statement — View last 10 transactions with date, type, and running balance
- Full Statement — Generate PDF bank statements for any date range
- Balance Enquiry — Check current balance with account holder verification
- Account Types — Savings (4% interest), Current (no interest, overdraft), FD (6.5% interest)
- Interest Calculation — Automatic monthly interest calculation and credit for savings accounts
- Loan Management — Apply for personal/home loans with EMI calculation
- Role-Based Access — Admin (full access), Manager (approve/reject), Customer (own account only)
- PIN Management — Set and change 4-digit transaction PIN with encryption
- Account Freeze/Unfreeze — Admin can freeze suspicious accounts
- Transaction History — Complete audit trail with filtering and pagination
- Notifications — Email alerts for transactions above threshold amount
- KYC Verification — Document upload and verification workflow
- Branch Management — Multiple branches with branch-level reporting
- Daily/Monthly Reports — Automated report generation for management
- Exception Handling — Custom exceptions with proper HTTP status codes
- Input Validation — Bean validation with custom validators for account numbers, IFSC codes
Project Structure
Database Schema
accounts Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier |
| account_number | VARCHAR(16) | UNIQUE, NOT NULL | 16-digit account number |
| account_holder_name | VARCHAR(100) | NOT NULL | Full name of holder |
| account_type | ENUM | NOT NULL | SAVINGS, CURRENT, FIXED_DEPOSIT |
| balance | DECIMAL(15,2) | DEFAULT 0.00 | Current balance |
| pin | VARCHAR(255) | NOT NULL | Encrypted 4-digit PIN |
| status | ENUM | DEFAULT 'ACTIVE' | ACTIVE, FROZEN, CLOSED |
| branch_id | BIGINT | FOREIGN KEY | Reference to branch |
| user_id | BIGINT | FOREIGN KEY | Reference to user |
| interest_rate | DECIMAL(4,2) | NOT NULL | Annual interest rate |
| min_balance | DECIMAL(10,2) | NOT NULL | Minimum required balance |
| created_at | TIMESTAMP | NOT NULL | Account creation date |
| updated_at | TIMESTAMP | NOT NULL | Last modification date |
transactions Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier |
| transaction_id | VARCHAR(20) | UNIQUE, NOT NULL | TXN reference number |
| from_account | VARCHAR(16) | FOREIGN KEY | Source account |
| to_account | VARCHAR(16) | FOREIGN KEY | Destination account |
| amount | DECIMAL(15,2) | NOT NULL | Transaction amount |
| transaction_type | ENUM | NOT NULL | DEPOSIT, WITHDRAWAL, TRANSFER |
| description | VARCHAR(255) | Transaction description | |
| balance_after | DECIMAL(15,2) | NOT NULL | Balance after transaction |
| status | ENUM | DEFAULT 'SUCCESS' | SUCCESS, FAILED, PENDING |
| timestamp | TIMESTAMP | NOT NULL | Transaction time |
users Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier |
| VARCHAR(100) | UNIQUE, NOT NULL | Login email | |
| password | VARCHAR(255) | NOT NULL | BCrypt encrypted password |
| full_name | VARCHAR(100) | NOT NULL | User full name |
| phone | VARCHAR(15) | UNIQUE | Mobile number |
| role | ENUM | NOT NULL | ADMIN, MANAGER, CUSTOMER |
| kyc_verified | BOOLEAN | DEFAULT FALSE | KYC status |
| created_at | TIMESTAMP | NOT NULL | Registration date |
loans Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier |
| account_id | BIGINT | FOREIGN KEY | Borrower account |
| loan_type | ENUM | NOT NULL | PERSONAL, HOME, EDUCATION |
| principal_amount | DECIMAL(15,2) | NOT NULL | Loan amount |
| interest_rate | DECIMAL(4,2) | NOT NULL | Annual interest rate |
| tenure_months | INT | NOT NULL | Loan duration |
| emi_amount | DECIMAL(10,2) | NOT NULL | Monthly EMI |
| status | ENUM | DEFAULT 'PENDING' | PENDING, APPROVED, REJECTED, CLOSED |
| applied_date | TIMESTAMP | NOT NULL | Application date |
| approved_date | TIMESTAMP | Approval date |
Entity Relationships
Key Code Snippets
Account Entity (Model Class)
package com.wohotech.banking.entity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "accounts")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
@Builder
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "account_number", unique = true, nullable = false, length = 16)
private String accountNumber;
@Column(name = "account_holder_name", nullable = false, length = 100)
private String accountHolderName;
@Enumerated(EnumType.STRING)
@Column(name = "account_type", nullable = false)
private AccountType accountType;
@Column(name = "balance", precision = 15, scale = 2)
private BigDecimal balance = BigDecimal.ZERO;
@Column(name = "pin", nullable = false)
private String pin; // BCrypt encrypted
@Enumerated(EnumType.STRING)
@Column(name = "status")
private AccountStatus status = AccountStatus.ACTIVE;
@Column(name = "interest_rate", precision = 4, scale = 2)
private BigDecimal interestRate;
@Column(name = "min_balance", precision = 10, scale = 2)
private BigDecimal minBalance;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "branch_id")
private Branch branch;
@OneToMany(mappedBy = "fromAccount", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Transaction> transactions;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}Account Repository
package com.wohotech.banking.repository;
import com.wohotech.banking.entity.Account;
import com.wohotech.banking.enums.AccountStatus;
import com.wohotech.banking.enums.AccountType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
Optional<Account> findByAccountNumber(String accountNumber);
List<Account> findByUserId(Long userId);
List<Account> findByAccountType(AccountType accountType);
List<Account> findByStatus(AccountStatus status);
@Query("SELECT a FROM Account a WHERE a.balance < a.minBalance AND a.status = 'ACTIVE'")
List<Account> findAccountsBelowMinBalance();
@Query("SELECT SUM(a.balance) FROM Account a WHERE a.branch.id = :branchId")
BigDecimal getTotalDepositsByBranch(@Param("branchId") Long branchId);
@Query("SELECT a FROM Account a WHERE a.accountType = 'SAVINGS' AND a.status = 'ACTIVE'")
List<Account> findActiveAccountsForInterestCalculation();
boolean existsByAccountNumber(String accountNumber);
}Account Service Implementation
Account Controller
package com.wohotech.banking.controller;
import com.wohotech.banking.dto.request.*;
import com.wohotech.banking.dto.response.*;
import com.wohotech.banking.service.AccountService;
import com.wohotech.banking.service.StatementService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/accounts")
@RequiredArgsConstructor
@Tag(name = "Account Management", description = "APIs for banking account operations")
public class AccountController {
private final AccountService accountService;
private final StatementService statementService;
@PostMapping
@Operation(summary = "Create a new bank account")
public ResponseEntity<AccountResponse> createAccount(
@Valid @RequestBody AccountCreateRequest request) {
AccountResponse response = accountService.createAccount(request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@GetMapping("/{accountNumber}")
@Operation(summary = "Get account details")
public ResponseEntity<AccountResponse> getAccount(
@PathVariable String accountNumber) {
return ResponseEntity.ok(accountService.getAccountDetails(accountNumber));
}
@PostMapping("/deposit")
@Operation(summary = "Deposit money into account")
public ResponseEntity<BalanceResponse> deposit(
@Valid @RequestBody DepositRequest request) {
return ResponseEntity.ok(accountService.deposit(request));
}
@PostMapping("/withdraw")
@Operation(summary = "Withdraw money from account")
public ResponseEntity<BalanceResponse> withdraw(
@Valid @RequestBody WithdrawRequest request) {
return ResponseEntity.ok(accountService.withdraw(request));
}
@PostMapping("/transfer")
@Operation(summary = "Transfer funds between accounts")
public ResponseEntity<String> transfer(
@Valid @RequestBody TransferRequest request) {
return ResponseEntity.ok(accountService.transferFunds(request));
}
@GetMapping("/{accountNumber}/balance")
@Operation(summary = "Check account balance")
public ResponseEntity<BalanceResponse> checkBalance(
@PathVariable String accountNumber) {
return ResponseEntity.ok(accountService.checkBalance(accountNumber));
}
@GetMapping("/{accountNumber}/statement")
@Operation(summary = "Get account mini statement (last 10 transactions)")
public ResponseEntity<List<TransactionResponse>> getMiniStatement(
@PathVariable String accountNumber) {
return ResponseEntity.ok(statementService.getMiniStatement(accountNumber));
}
@PutMapping("/{accountNumber}/freeze")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Freeze a bank account (Admin only)")
public ResponseEntity<String> freezeAccount(
@PathVariable String accountNumber) {
accountService.freezeAccount(accountNumber);
return ResponseEntity.ok("Account frozen successfully");
}
@GetMapping
@PreAuthorize("hasRole('ADMIN') or hasRole('MANAGER')")
@Operation(summary = "Get all accounts (Admin/Manager)")
public ResponseEntity<List<AccountResponse>> getAllAccounts() {
return ResponseEntity.ok(accountService.getAllAccounts());
}
}API Endpoints
| Method | URL | Description | Auth Required |
|---|---|---|---|
| POST | /api/v1/accounts | Create new bank account | Yes (Any) |
| GET | /api/v1/accounts/{accountNumber} | Get account details | Yes (Owner/Admin) |
| POST | /api/v1/accounts/deposit | Deposit money | Yes (Owner) |
| POST | /api/v1/accounts/withdraw | Withdraw money (PIN required) | Yes (Owner) |
| POST | /api/v1/accounts/transfer | Fund transfer (PIN required) | Yes (Owner) |
| GET | /api/v1/accounts/{accountNumber}/balance | Check balance | Yes (Owner) |
| GET | /api/v1/accounts/{accountNumber}/statement | Mini statement | Yes (Owner) |
| GET | /api/v1/accounts/{accountNumber}/statement/full | Full PDF statement | Yes (Owner) |
| PUT | /api/v1/accounts/{accountNumber}/freeze | Freeze account | Yes (Admin) |
| PUT | /api/v1/accounts/{accountNumber}/unfreeze | Unfreeze account | Yes (Admin) |
| PUT | /api/v1/accounts/{accountNumber}/pin | Change PIN | Yes (Owner) |
| GET | /api/v1/accounts | List all accounts | Yes (Admin/Manager) |
| POST | /api/v1/loans/apply | Apply for loan | Yes (Customer) |
| GET | /api/v1/loans/{loanId} | Get loan details | Yes (Owner/Admin) |
| PUT | /api/v1/loans/{loanId}/approve | Approve loan | Yes (Manager) |
| GET | /api/v1/transactions/{accountNumber} | Transaction history | Yes (Owner) |
| POST | /api/v1/auth/register | User registration | No |
| POST | /api/v1/auth/login | User login (JWT) | No |
Sample API Request & Response
POST /api/v1/accounts/transfer
Request Body:
{
"fromAccount": "1234567890123456",
"toAccount": "6543210987654321",
"amount": 5000.00,
"pin": "1234",
"description": "Rent payment for June"
}Response (200 OK):
{
"message": "Transfer successful. Transaction ID: TXN1718192345678",
"fromAccount": "1234567890123456",
"newBalance": 45000.00,
"timestamp": "2024-06-12T14:30:00"
}Error Response (400 Bad Request):
{
"status": 400,
"error": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance. Minimum balance required: 1000.00",
"timestamp": "2024-06-12T14:30:00",
"path": "/api/v1/accounts/transfer"
}How to Run
Prerequisites
- Java 17 or higher installed
- MySQL 8.0 running
- Maven 3.9+ installed
- Git installed
Step-by-Step Setup
Step 1: Clone the repository
git clone https://github.com/wohotech/banking-system.git
cd banking-systemStep 2: Create MySQL Database
CREATE DATABASE banking_db;
CREATE USER 'banking_user'@'localhost' IDENTIFIED BY 'banking@123';
GRANT ALL PRIVILEGES ON banking_db.* TO 'banking_user'@'localhost';
FLUSH PRIVILEGES;Step 3: Configure application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/banking_db
username: banking_user
password: banking@123
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQLDialect
jwt:
secret: mySecretKey123456789012345678901234567890
expiration: 86400000
server:
port: 8080Step 4: Build and run the application
mvn clean install
mvn spring-boot:runStep 5: Access the application
- API Base URL:
http://localhost:8080/api/v1 - Swagger UI:
http://localhost:8080/swagger-ui.html
Step 6: Test with sample data
# Register a user
curl -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"john@example.com","password":"Pass@123","fullName":"John Doe","phone":"9876543210"}'
# Create an account
curl -X POST http://localhost:8080/api/v1/accounts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"fullName":"John Doe","accountType":"SAVINGS","initialDeposit":10000,"pin":"1234"}'Screenshots Description
- Swagger UI Dashboard — Shows all available API endpoints grouped by controller with request/response schemas
- Account Creation — POST request creating a savings account with initial deposit
- Deposit Transaction — Successful deposit showing updated balance
- Fund Transfer — Transfer between two accounts with transaction ID confirmation
- Mini Statement — Last 10 transactions with date, type, amount, and running balance
- Error Handling — Insufficient balance error with proper HTTP status and message
- Admin Panel — List of all accounts with freeze/unfreeze controls
- PDF Statement — Generated bank statement PDF with header, transactions table, and summary
Future Enhancements
- Multi-Currency Support — Handle USD, EUR, GBP with real-time exchange rates
- UPI Integration — Add UPI-based payment support with QR code generation
- Two-Factor Authentication — OTP-based verification for high-value transactions
- Scheduled Payments — Recurring payments (EMI, bills) with auto-debit
- Investment Module — Mutual funds and FD booking within the app
- Fraud Detection — ML-based anomaly detection for suspicious transactions
- Mobile App — React Native frontend for mobile banking
- Microservices Architecture — Split into Account, Transaction, Notification services
- Event Sourcing — Implement event-driven architecture with Kafka
- Credit Score Integration — Third-party API integration for loan eligibility
Interview Questions
Q1: How do you ensure ACID compliance in fund transfers?
Answer: We use Spring's @Transactional annotation on the transfer method. This ensures that both the debit from sender and credit to receiver happen atomically. If any step fails (e.g., receiver account not found after debit), the entire transaction rolls back. We use Isolation.SERIALIZABLE for critical operations and Propagation.REQUIRED (default) to join existing transactions.
Q2: How do you handle concurrent transactions on the same account?
Answer: We use optimistic locking with @Version annotation on the Account entity. If two transactions try to modify the same account simultaneously, one will get an OptimisticLockException. We also use @Lock(LockModeType.PESSIMISTIC_WRITE) on the repository method for critical operations like withdrawals to prevent dirty reads.
Q3: Why did you use BigDecimal instead of double for balance?
Answer: double has floating-point precision issues (e.g., 0.1 + 0.2 ≠ 0.3 in binary). In financial applications, even a fraction of a cent matters. BigDecimal provides arbitrary-precision decimal arithmetic, ensuring exact calculations. We always use BigDecimal.ZERO comparisons with compareTo() instead of equals().
Q4: How is the PIN stored securely?
Answer: The PIN is never stored in plain text. We use Spring Security's BCryptPasswordEncoder to hash the PIN before storage. During verification, we use passwordEncoder.matches(rawPin, encodedPin). BCrypt includes a random salt in each hash, so the same PIN produces different hashes, preventing rainbow table attacks.
Q5: How would you scale this system for millions of users?
Answer: (1) Database sharding by account number range, (2) Read replicas for balance enquiry and statement generation, (3) Redis caching for frequently accessed account data, (4) Message queues (Kafka) for async transaction processing, (5) Microservices decomposition with separate databases per service, (6) Connection pooling with HikariCP, (7) API rate limiting with Spring Cloud Gateway.
Q6: Explain the layered architecture used in this project.
Answer: The project follows a 4-layer architecture: Controller Layer (handles HTTP requests, input validation, response formatting), Service Layer (business logic, transaction management, orchestration), Repository Layer (data access, custom queries, pagination), Entity Layer (database mapping, relationships, constraints). This separation ensures testability, maintainability, and adherence to Single Responsibility Principle.
Q7: How do you handle exceptions in this project?
Answer: We use a @RestControllerAdvice class (GlobalExceptionHandler) that catches all exceptions and returns standardized error responses. Custom exceptions like InsufficientBalanceException, AccountNotFoundException extend RuntimeException and carry specific HTTP status codes. This approach provides consistent error format across all APIs and keeps controller code clean.
Key Learnings
- Transaction management with
@Transactionaland rollback scenarios - Securing financial data with encryption and hashing
- Implementing proper audit trails for compliance
- Building RESTful APIs with proper HTTP status codes
- Database design for financial systems (ACID, normalization)
- Input validation and custom validators
- PDF generation for reports and statements
- Role-based access control with Spring Security
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Banking System — Java Programming.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, projects, banking, system
Related Java Master Course Topics