A RESTful bookstore backend structured using Clean Architecture principles. built with Java 25, Spring Boot 4, and PostgreSQL 18. Designed as a portfolio project demonstrating clean architecture principles, JWT-based authentication, and containerized deployment via Docker.
- JWT authentication
- Role-based authorization (CUSTOMER - ADMIN)
- Book CRUD operations
- Pagination
- Rate limiting
- BCrypt password hashing
- RFC 9457 error responses
- Docker deployment
src/main/java/com/bookstore/javabookstore/
├── domain/
│ ├── exception/
│ ├── model/
│ ├── repository/
│ └── service/
├── application/
│ └── usecase/
│ ├── book/
│ └── user/
├── infrastructure/
│ ├── config/
│ ├── persistence/
│ │ ├── entity/
│ │ ├── mapper/
│ │ └── repository/
│ └── security/
└── presentation/
├── controller/
└── dto/
├── request/
└── response/
The project enforces clean architecture with four layers. Dependencies flow inward; infrastructure depends on application, application depends on domain, domain depends on nothing outside itself and is the main foundation.
| Layer | Responsibility |
|---|---|
| domain | Business models and rules. No framework dependencies. Just pure Java. |
| application | Use cases. Orchestrates domain objects. No HTTP, no JPA. |
| infrastructure | Framework and technology concerns and implementations. |
| presentation | HTTP boundary. |
| Package | Description |
|---|---|
| domain.model | Book, User (rich domain objects with validation) |
| domain.repository | Repository interfaces (contracts, not implementations) |
| domain.service | Service interfaces (PasswordHasher, TokenService, LoginAttemptService) |
| domain.exception | Domain exceptions mapped to HTTP status codes |
| application.book | AddBookUseCase, GetBooksUseCase, UpdateBookUseCase, DeleteBookUseCase. |
| application.user | RegisterUserUseCase, LoginUseCase. |
| infrastructure.config | Bean wiring for use cases. |
| infrastructure.persistence | JPA entities, Spring Data repositories, mappers. |
| infrastructure.security | JWT, Bcrypt, rate limiting, Spring Security config. |
| presentation.controller | AuthController, BookController. |
| presentation.dto | Request and response records. |
| presentation.GlobalExceptionHandler | Maps domain exceptions to RFC 9457 ProblemDetail responses. |
| Technology | Stack |
|---|---|
| Language | Java 25 |
| Framework | Spring Boot 4 / Spring Framework 7 |
| Security | Spring Security , jjwt |
| Persistence | Spring Data JPA, Hibernate, PostgreSQL 18 |
| Password hashing | BCrypt |
| Rate limiting | Bucket4j + Caffeine cache |
| Containerization | Docker, Docker Compose |
| Version Control | Git |
Authentication uses stateless JWT (HS256). Tokens are issued on login and contain userId, email, username, and role claims. No server-side session is maintained.
Password handling: raw passwords never enter the domain layer; this keeps cryptographic concerns outside the business domain. The infrastructure layer hashes with Bcrypt before passing the hash to User.create().
Rate limiting:
- Signup: 10 requests per IP per 2 hours (Caffeine Cache-backed per-IP bucket, evicts after 2 hours of inactivity, max 10,000 tracked IPs)
- Login: 10 failed attempts per email triggers a 2-hour lockout (in-memory, resets on server restart, doesn't allow actual user to get punished)
- Bucket4j implements the Token Bucket algorithm for request throttling
Design tradeoffs:
- JWT logout is client-side only (token deletion). Server-side invalidation would require a denylist (Redis), which is out of scope.
- Login lockout resets on server restart. Persistent lockout would require a DB-backed store like Redis.
ddl-auto=updateis used for development convenience. Production should use Flyway migrations.- Role-based route enforcement is not yet implemented at the API level.
All endpoints return application/json. Errors follow RFC 9457 ProblemDetail format to provide detailed error breakdown.
POST /api/auth/register Register a new user
POST /api/auth/login Login and receive JWT
Register request: Request Body
{
"username": "user",
"email": "username@example.com",
"password": "securepassword"
}Register response (201): Response
{
"id": "uuid",
"username": "user",
"email": "username@example.com",
"role": "CUSTOMER"
}Login request: Request Body
{
"email": "username@example.com",
"password": "securepassword"
}Login response (200): Response
{
"token": "abXY..."
}GET /api/books?page=0&pageSize=10 List books (paginated format)
POST /api/books Add a book
PUT /api/books/{id} Update a book
DELETE /api/books/{id} Delete a book
Create/Update request: Request Body
{
"name": "Clean Code",
"author": "Robert C. Martin",
"pages": 431,
"description": "A handbook of agile software craftsmanship."
}Book response: Response
{
"id": "uuid",
"name": "Clean Code",
"author": "Robert C. Martin",
"pages": 431,
"description": "A handbook of agile software craftsmanship."
}| Status | Thrown when |
|---|---|
400 - Bad Request |
Validation failure |
401 - Unauthorized |
Invalid credentials or locked account |
404 - Not Found |
Book ID does not exist |
409 - Conflict |
Email or username already registered |
429 - Too Many Requests |
Signup rate limit exceeded |
500 - Internal Server Error |
Unexpected failure |
Prerequisites: Docker, Docker Compose.
- Clone the repository
- Create
.envin the project root:
DB_PASSWORD=your_password
JWT_SECRET=<base64-encoded 32-byte key — generate with: openssl rand -base64 32> - Run:
docker compose up --build- API is available at
http://localhost:8085
Promoting a user to ADMIN (admin must be set manually):
UPDATE users SET role = 'ADMIN' WHERE email = 'your@email.com';- Ensure PostgreSQL is running locally on port 5432 with a
bookstoredatabase - Copy
application.properties.exampletosrc/main/resources/application.propertiesand fill in values - Run:
./mvnw spring-boot:run
Kindly refer to application.properties.example inside src/main/java/com/bookstore/javabookstore/ for a premade key file.