A production-ready full-stack banking application designed to demonstrate secure backend architecture, database-driven RBAC authorization, and real-world financial operations.
The system supports account management, deposits, withdrawals, transfers, transaction auditing, and role-based access control with JWT authentication.
- Project Highlights
- System Architecture
- Technical Overview
- Tech Stack
- Prerequisites
- Running Locally
- Project Structure
- Permissions Table
- API Documentation
- JWT-based authentication with token invalidation (logout support)
- Database-driven RBAC (Module → Operation → Role → Permission)
- Feature-based layered backend architecture
- Fully containerized with Docker
- REST API built with Spring Boot
- Proper HTTP status code usage (200, 400, 401, 403, 404, 500)
- Modern frontend built with React and Redux Toolkit
- Stateless JWT authentication
- Token returned after login
- Required in ´Authorization: Bearer ´
- Logout invalidates stored token
The system implements a database-driven Role-Based Access Control model
This provides:
- Fine-grained endpoints protection
- No hardcoded role chekcs
- Fully scalable permission management
- Clean separation between security rules and business logic
The project uses Flyway for version-controlled database schema management.
All database changes are applied automatically during application startup through migration scripts located in:
src/main/resources/db/migration
This approach provides:
-
Version-controlled database evolution
-
Repeatable and consistent schema creation
-
Safe deployments across environments
-
Automatic schema initialization when running the project locally
Flyway runs automatically when the backend starts, ensuring the database schema is always up to date.
- Java 17
- Spring Boot 3.4.2
- Spring Web
- Spring Data JPA
- Spring Security
- Validation
- Hibernate
- MySQL
- Maven
- Lombok
- MapStruct
- JJWT
- TypeScript
- React 18
- Redux Toolkit
- Vite
- Axios
- React Hook Form + Yup
- Material UI
- Recharts
- Docker
- Docker Compose
- GitHub Actions
- EC2
- RDS
Before running the project locally, ensure the following tools are installed:
- Java 17
- Node.js 18+
- Docker
- Docker Compose
- Git
Optional but recommended:
- Postman or Insomnia for API testing
- TablePlus or DBeaver for database inspection
The entire application stack (MySQL, backend API, and frontend) is containerized and can be started with Docker Compose.
git clone https://github.com/your-username/banking-system.git cd banking-system
Create a .env file in the root directory of the project.
Example configuration:
SPRING_PROFILES_ACTIVE=dev
DB_HOST=mysql
DB_PORT=3306
DB_NAME=banking_system_db
DB_USERNAME=banking_user
DB_PASSWORD=banking_pass
MYSQL_ROOT_PASSWORD=root
JWT_SECRET=your-secret-key
JWT_EXPIRATION_MINUTES=30
VITE_API_BASE_URL=http://localhost:8080/api/v1Run the following command from the root directory:
docker compose up --build -d
This will start three containers:
-
MySQL Database
-
Spring Boot Backend API
-
React Frontend
Once the containers are running, the services will be available at:
| Service | URL |
|---|---|
| Frontend | http://localhost:5173 |
| Backend API | http://localhost:8080/api/v1 |
To stop all containers:
docker compose down
To also remove the database volume:
docker compose down -v
-
The backend waits for MySQL to become available using a Docker health check.
-
The database schema is automatically managed with Flyway migrations.
-
The React application communicates with the backend using the environment variable
VITE_API_BASE_URL.
The Backend is organized by domain features, not only technical layers.
Each feature contains:
- controller
- service
- dto (request/response)
- persistence (entity + repository)
- exception (if needed)
accounts/
controller/
dto/
persistence/
service/
exception/
auth/
transactions/
users/
admin/This improves:
- Maintainability
- Scalability
- Clear domain boundaries
- Easier onboarding for new developers
The system implements database-driven Role-Based Access Control (RBAC). Permissions are defined dynamically through the modules, operations, and permissions tables instead of static annotations.
- ADMINISTRATOR – Full access to all operations.
- EMPLOYEE – Operational banking access.
- CUSTOMER – Limited access to their own banking resources.
- Note: Some restrictions such as "only their own accounts" are enforced at the service layer, not directly in the RBAC table.
| Module | Operation | Administrator | Employee | Customer |
|---|---|---|---|---|
| Account | Create account | ✅ | ✅ | ✅ |
| Get one account | ✅ | ✅ | ✅ | |
| Get all accounts | ✅ | ✅ | ✅ (only their own accounts) | |
| Update account | ✅ | ✅ | ❌ | |
| Update account status | ✅ | ✅ | ✅ | |
| Delete account | ✅ | ❌ | ❌ | |
| Deposit into account | ✅ | ✅ | ✅ (only to their own accounts) | |
| Withdraw from account | ✅ | ✅ | ✅ (only from their own accounts with sufficient balance) | |
| Transfer between accounts | ✅ | ✅ | ✅ (only from their own accounts to other existing accounts) | |
| Check account balance | ✅ | ✅ | ✅ (only their own accounts) | |
| Transaction | Get one transaction | ✅ | ✅ | ✅ |
| Get all transactions | ✅ | ✅ | ✅ (only transactions related to their accounts) | |
| User | Register user | ✅ | ✅ | ✅ (public endpoint) |
| Auth | Authenticate | ✅ | ✅ | ✅ (public endpoint) |
| Validate token | ✅ | ✅ | ✅ (public endpoint) | |
| Get my profile | ✅ | ✅ | ✅ | |
| Logout | ✅ | ✅ | ✅ (public endpoint) | |
| Module | Create module | ✅ | ❌ | ❌ |
| Get one module | ✅ | ❌ | ❌ | |
| Get all modules | ✅ | ❌ | ❌ | |
| Update module | ✅ | ❌ | ❌ | |
| Delete module | ✅ | ❌ | ❌ | |
| Operation | Create operation | ✅ | ❌ | ❌ |
| Get one operation | ✅ | ❌ | ❌ | |
| Get all operations | ✅ | ❌ | ❌ | |
| Update operation | ✅ | ❌ | ❌ | |
| Delete operation | ✅ | ❌ | ❌ | |
| Permission | Create permission | ✅ | ❌ | ❌ |
| Get one permission | ✅ | ❌ | ❌ | |
| Get all permissions | ✅ | ❌ | ❌ | |
| Delete permission | ✅ | ❌ | ❌ | |
| Role | Create role | ✅ | ❌ | ❌ |
| Get one role | ✅ | ❌ | ❌ | |
| Get all roles | ✅ | ❌ | ❌ | |
| Update role | ✅ | ❌ | ❌ | |
| Delete role | ✅ | ❌ | ❌ | |
| Statistics | Get statistics | ✅ | ❌ | ❌ |
Below are three practical examples using the initial data.
- URL:
{BASE_URL}/api/v1/auth/authenticate - Description: Authenticates a user and returns a JWT token for subsequent requests.
- Method: POST
- Headers:
- Content-Type:
application/json. Mandatory
- Content-Type:
- Request Body:
- username: The user's unique username. Mandatory. Cannot be blank.
- password: The user's password. Mandatory. Cannot be blank.
- Example Usage (CURL):
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"username": "sakura",
"password": "Sakura_123456"
}' \
"http://localhost:8080/api/v1/auth/authenticate"-
Response:
-
200 (Ok): Returns a JWT token for the authenticated user.
- Example:
{ "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }
- Example:
-
500 (Internal Server Error): If the the username or password is incorrect.
- Example:
{ "frontendMessage": "An unexpected error occurred. Please try again.", "backendMessage": "User with username 'narutor' was not found.", "status": 500, "path": "/api/v1/auth/authenticate", "timestamp": "2025/03/25 13:24:12" }
- Example:
-
Note: Use the returned token in the
Authorizationheader asBearer <token>.
-
- URL:
{BASE_URL}/api/v1/accounts/transfer - Description: Transfers money between accounts using the initial data from
data.sql. This operation persists aTransactionentity in the database to log the activity. - Method: POST
- Headers:
- Content-Type:
application/json. Mandatory. - Authorization:
Bearer <JWT_TOKEN>. Mandatory (obntained from the authentication endpoint)
- Content-Type:
- Request Body:
- sourceAccountNumber: Source account number. Mandatory. Cannot be blank.
- targetAccountNumber: Target account number. Mandatory. Cannot be blank.
- amount: Amount to transfer. Mandatory. Cannot be blank.
- comment: Optional comment for the transaction.
- Example Usage (CURL):
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-d '{
"sourceAccountNumber": "76591142607308616612",
"targetAccountNumber": "114087875602508931",
"amount": 1000.00,
"comment": "Transfer between Sakura accounts"
}' \
"http://localhost:8080/api/v1/accounts/transfer"- Response:
- 200 (Ok): Transfer successful, with the generated transaction details.
- Example:
{ "transactionNumber": "123456789012", "sourceAccount": { "id": 3, "accountNumber": "76591142607308616612", "balance": 9000.0 }, "targetAccount": { "id": 2, "accountNumber": "114087875602508931", "balance": 6000.0 }, "amount": 1000.0, "transactionDate": "2025-03-30T13:24:12", "type": "TRANSFER", "comment": "Transfer between Sakura accounts" }
- Example:
- 400 (Bad Request): If the amount exceeds the balance or is invalid.
- Example:
{ "frontendMessage": "An unexpected error occurred. Please try again.", "backendMessage": "Insufficient founds.", "status": 400, "path": "/api/v1/accounts/transfer", "timestamp": "2025/03/25 13:24:12" }
- Example:
- 401 (Unauthorized): If the token is missing or invalid.
- Example:
{ "frontendMessage": "No authentication credentials were found. Please log in.", "backendMessage": "Bad credentials.", "status": 401, "path": "/api/v1/accounts/transfer", "timestamp": "2025/03/25 13:24:12" }
- Example:
- 403 (Forbidden): If the user has no authority.
- Example:
{ "frontendMessage": "No authentication credentials were found. Please log in.", "backendMessage": "Forbidden.", "status": 403, "path": "/api/v1/accounts/transfer", "timestamp": "2025/03/25 13:24:12" }
- Example:
- 404 (Not Found): If an account does not exist.
- Example:
{ "frontendMessage": "The requested resource was not found.", "backendMessage": "Source account not found.", "status": 404, "path": "/api/v1/accounts/transfer", "timestamp": "2025/03/25 13:24:12" }
- Example:
- 500 (Internal Server Error): If there are any error.
- Example:
{ "frontendMessage": "An unexpected error occurred. Please try again.", "backendMessage": "Bad credentials.", "status": 500, "path": "/api/v1/accounts/transfer", "timestamp": "2025/03/25 13:24:12" }
- Example:
- 200 (Ok): Transfer successful, with the generated transaction details.
- URL:
{BASE_URL}/api/v1/auth/logout - Description: Logs out the user by invalidating the JWT Token.
- Method: POST
- Headers:
- Authorization:
Bearer <JWT_TOKEN>. No mandatory (obtained from the authentication endpoint)
- Authorization:
- Example Usage (CURL):
curl -X POST \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
"http://localhost:8080/api/v1/auth/logout"- Response:
- 200 (Ok): Logout successful.
- Example:
{ "message": "Logout successful." }
- Example:
- 200 (Ok): Logout successful.