A production-minded REST API for user registration, wallet management, deposits, authenticated transfers, and transaction history. The service is packaged as a Spring Boot application and runs with PostgreSQL through Docker Compose.
The application follows a layered architecture with explicit boundaries:
HTTP clients
|
v
Controllers -> request validation and HTTP responses
|
v
Services -> business rules and transaction boundaries
|
v
Repositories -> Spring Data JPA persistence and locking
|
v
PostgreSQL
controller: REST endpoints under/api/v1.dto: validated request models and response models, keeping the API contract separate from persistence entities.service: registration, authentication, wallet, and transfer workflows.repository: database access, filtered transaction queries, and wallet locking.entity: JPA domain model for users, wallets, and transactions.security: JWT creation, parsing, and request authentication.mapper: conversion between entities and API DTOs.exception: centralized API error handling.
- Java 17
- Spring Boot 4.1
- Spring MVC
- Spring Data JPA and Hibernate
- Spring Security with JWT
- PostgreSQL 16
- Jakarta Bean Validation
- springdoc OpenAPI and Swagger UI
- Maven Wrapper
- Docker and Docker Compose
- JUnit, Spring Boot Test, Mockito, and H2 for tests
- User registration with a securely encoded password and an initial USD wallet.
- JWT-based login and stateless request authentication.
- Authenticated wallet lookup and deposits with ownership checks.
- Wallet-to-wallet transfers with self-transfer and balance validation.
- Pessimistic wallet locking to protect concurrent balance updates.
- Paginated and filtered transaction history by date, type, and status.
- Consistent validation and REST error responses.
- Interactive OpenAPI documentation generated from controller annotations.
- Reproducible multi-stage Docker image with a JDK build stage and a lightweight JRE runtime stage.
Install Docker Desktop with Docker Compose support. No local Java, Maven, or PostgreSQL installation is required for the containerized workflow.
From the repository root:
docker compose up --buildThe application starts on http://localhost:8080. PostgreSQL runs inside the Compose network and is available to the application as postgres:5432. The database data is retained in the postgres-data named volume.
To run the stack in the background:
docker compose up --build -dTo stop the stack:
docker compose downPostgreSQL is also published on localhost:5432 so the application can be started directly from IntelliJ using the default datasource settings. Stop any other PostgreSQL service using port 5432 before starting Compose.
For a local Java 17 or 21 environment, start the database first:
docker compose up -d postgresThen start the application from IntelliJ or the Maven wrapper. The default local connection is postgres/secret on localhost:5432:
./mvnw test
./mvnw spring-boot:runOn Windows, use mvnw.cmd in place of ./mvnw.
Database credentials and JWT settings can be overridden with SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD, JWT_SECRET, and JWT_EXPIRATION_MS.
Once the application is running, open the interactive Swagger UI:
The generated OpenAPI document is available at:
Authentication endpoints are public. To test protected endpoints:
- Use
POST /api/v1/usersto register a user. - Use
POST /api/v1/auth/loginand copy the returned JWT. - Select Authorize in Swagger UI and enter the token (without the
Bearerprefix). - Execute the protected operations; Swagger UI adds the bearer header automatically.
The remaining API endpoints require the bearer token returned by POST /api/v1/auth/login.
| Method | Endpoint | Purpose | Access |
|---|---|---|---|
POST |
/api/v1/users |
Register a user and create a wallet | Public |
POST |
/api/v1/auth/login |
Authenticate and issue a JWT | Public |
GET |
/api/v1/wallets/me |
Read the current user's wallet | Authenticated |
POST |
/api/v1/wallets/deposit |
Deposit into the current user's wallet | Authenticated |
POST |
/api/v1/transactions/transfer |
Transfer funds between wallets | Authenticated |
GET |
/api/v1/transactions |
Query the current user's transactions | Authenticated |
GET |
/api/v1/users |
List all users | Admin role |
State-changing workflows use @Transactional so wallet changes and transaction records commit atomically. Read-only queries use @Transactional(readOnly = true) where appropriate. Registration creates the user and initial wallet in one unit of work.
Spring Security runs in stateless mode. A JWT authentication filter extracts and validates bearer tokens before protected controllers execute. Passwords are stored using BCrypt, and service-level ownership checks prevent a user from depositing to or transferring from another user's wallet.
Transfer operations load both wallets with a repository method using @Lock(LockModeType.PESSIMISTIC_WRITE). The database locks the rows for the duration of the transaction, preventing concurrent transfers from observing and spending the same balance.
Request DTOs use Jakarta Bean Validation and controllers apply @Valid. Validation failures return HTTP 400 Bad Request with structured field-level errors.
A validation error response has the following shape:
{
"timestamp": "2026-09-08T16:40:00",
"status": 400,
"error": "Validation Failed",
"message": "Validation failed",
"path": "/api/v1/users",
"fieldErrors": {
"email": "Email is required"
}
}
The fieldErrors object maps each request field to its validation error message. For example:
{
"fieldErrors": {
"email": "Invalid email format",
"password": "Password should be at least 8 characters"
}
}
The API uses the following status codes for common outcomes:
| Status | Meaning |
|---|---|
200 OK |
Request completed successfully |
400 Bad Request |
Request validation failed or the request violates a business rule |
401 Unauthorized |
Authentication is required or the supplied credentials/token are invalid |
403 Forbidden |
The authenticated user does not have permission to access the resource |
404 Not Found |
The requested resource does not exist |
500 Internal Server Error |
An unexpected server-side error occurred |
OpenAPI annotations describe controller operations and their response outcomes, including validation failures, keeping the interactive documentation aligned with the API behavior.
Run the automated test suite with:
./mvnw testBuild the production artifact without tests with:
./mvnw -DskipTests package