A RESTful banking API built with Spring Boot and Java 21, simulating the core operations of a digital bank: customer onboarding, account management, deposits, withdrawals, transfers and JWT-based authentication.
- Overview
- Features
- Tech Stack
- Architecture
- Security
- API Endpoints
- Error Handling
- Getting Started
- Running Tests
Banking API exposes a set of REST resources that let a client register, authenticate, open bank accounts and move money between them, while keeping every sensitive rule — balance checks, account status, ownership — enforced consistently across the codebase.
Two roles are supported: CUSTOMER, who can only manage their own data and accounts, and ADMIN, who has full visibility across the system.
- Customer registration, lookup, update and removal
- Account creation, balance lookup and blocking
- Deposits, withdrawals and transfers between accounts
- Paginated transaction history per account
- JWT login and self-service registration
- Role-based and ownership-based authorization on every protected endpoint
- Java 21
- Spring Boot 4.1 — Web, Data JPA, Security, Validation, Cache
- PostgreSQL for persistence
- Flyway for schema versioning
- JJWT for token generation and validation
- Lombok
- JUnit 5, Mockito and AssertJ for testing
- Docker Compose for the local database
- Maven
The codebase is organized by business domain rather than by technical layer — each feature (accounts, customers, transactions, authentication) owns its entity, repository, service, controller and DTOs, instead of being scattered across generic controller/service/repository packages.
Business rules live inside the domain entities themselves (Account, Customer) rather than in the services: an account knows how to validate its own deposit, withdrawal and status transitions, which keeps that logic unit-testable in complete isolation from Spring and the database.
Ownership checks are centralized in a single CurrentCustomerProvider, so the "is this the resource owner, or an admin?" rule is written once and reused everywhere, instead of being duplicated across every service method.
- Stateless authentication via JWT, verified by a custom filter placed before Spring Security's default authentication filter
- Passwords hashed with BCrypt
/api/auth/**is public; every other endpoint requires a valid token- Two layers of authorization: role-based (
@PreAuthorize("hasRole('ADMIN')")) for admin-only actions, and ownership-based for everything a customer does with their own data - Custom
401/403handlers returning a consistent JSON error body instead of the default servlet error page
Authentication
POST /api/auth/login
POST /api/auth/register
Customers
# ADMIN only
POST /api/customers
GET /api/customers
# CUSTOMER only
GET /api/customers/{id}
PUT /api/customers/{id}
DELETE /api/customers/{id}
Accounts
POST /api/v1/accounts
GET /api/v1/accounts/{id}
POST /api/v1/accounts/{id}/deposit
POST /api/v1/accounts/{id}/withdraw
POST /api/v1/accounts/{id}/block
Transactions
POST /api/accounts/{accountId}/transactions/deposit
POST /api/accounts/{accountId}/transactions/withdraw
POST /api/accounts/{accountId}/transactions/transfer
GET /api/accounts/{accountId}/transactions
A global @RestControllerAdvice converts every domain exception into a standardized ProblemDetail (RFC 7807) response:
| Exception | HTTP Status |
|---|---|
AccountNotFoundException |
404 Not Found |
ResourceNotFoundException |
404 Not Found |
InsufficientBalanceException |
422 Unprocessable Entity |
AccountBlockedException |
403 Forbidden |
AccessDeniedException |
403 Forbidden |
AuthenticationException |
401 Unauthorized |
IllegalArgumentException |
400 Bad Request |
MethodArgumentNotValidException |
400 Bad Request |
| Any other exception | 500 Internal Server Error |
Requirements: Java 21, Maven 3.9+, Docker.
-
Start the database:
docker compose up -d
-
Optionally override the local defaults with environment variables:
Variable Default (dev) JWT_SECRETlocal-dev-only-insecure-default-secret-please-overrideJWT_EXPIRATION3600000(1 hour, in ms)DB_URLjdbc:postgresql://localhost:5433/bankingDB_USERNAMEbankingDB_PASSWORDbankingJWT_SECRETshould always be overridden with a strong, private value outside local development. -
Run the application:
./mvnw spring-boot:run
Flyway migrations run automatically on startup.
-
The API is available at
http://localhost:8080.
./mvnw testThe suite covers domain rules on Account and Customer, the service layer (including ownership and permission scenarios), JWT generation/validation, and the authentication flow, using JUnit 5, Mockito and AssertJ.