Architecture Overview
The Library Management API follows a layered architecture pattern with clear separation of concerns. Each layer has a specific responsibility and communicates with adjacent layers through well-defined interfaces.
Presentation Controllers and DTOs
Persistence Data access layer
Directory Structure
jc-java-training/
├── src/
│ ├── main/
│ │ ├── java/com/raven/training/
│ │ │ ├── TrainingApplication.java # Main application class
│ │ │ ├── config/ # Configuration classes
│ │ │ │ ├── AppConfig.java
│ │ │ │ ├── filter/
│ │ │ │ │ └── JwtTokenValidator.java
│ │ │ │ └── security/
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ └── SwaggerConfig.java
│ │ │ ├── exception/ # Exception handling
│ │ │ │ ├── error/
│ │ │ │ │ ├── BookNotFoundException.java
│ │ │ │ │ ├── BookAlreadyInCollectionException.java
│ │ │ │ │ ├── BookNotInCollectionException.java
│ │ │ │ │ ├── UserNotFoundException.java
│ │ │ │ │ ├── EmailAlreadyExistsException.java
│ │ │ │ │ └── UsernameAlreadyExistsException.java
│ │ │ │ └── handler/
│ │ │ │ └── GlobalExceptionHandler.java
│ │ │ ├── mapper/ # Object mappers
│ │ │ │ ├── IBookMapper.java
│ │ │ │ └── IUserMapper.java
│ │ │ ├── persistence/ # Data layer
│ │ │ │ ├── entity/
│ │ │ │ │ ├── Book.java
│ │ │ │ │ ├── User.java
│ │ │ │ │ └── AuthUser.java
│ │ │ │ ├── model/
│ │ │ │ │ ├── ApiError.java
│ │ │ │ │ └── ErrorResponse.java
│ │ │ │ └── repository/
│ │ │ │ ├── IBookRepository.java
│ │ │ │ ├── IUserRepository.java
│ │ │ │ └── IAuthUserRepository.java
│ │ │ ├── presentation/ # API layer
│ │ │ │ ├── controller/
│ │ │ │ │ ├── BookController.java
│ │ │ │ │ ├── UserController.java
│ │ │ │ │ └── AuthUserController.java
│ │ │ │ └── dto/
│ │ │ │ ├── book/
│ │ │ │ │ ├── BookRequest.java
│ │ │ │ │ ├── BookResponse.java
│ │ │ │ │ └── bookexternal/
│ │ │ │ │ ├── BookResponseDTO.java
│ │ │ │ │ └── OpenLibraryBookDTO.java
│ │ │ │ ├── user/
│ │ │ │ │ ├── UserRequest.java
│ │ │ │ │ └── UserResponse.java
│ │ │ │ ├── login/
│ │ │ │ │ └── LoginRequest.java
│ │ │ │ ├── register/
│ │ │ │ │ └── RegisterRequest.java
│ │ │ │ └── pagination/
│ │ │ │ └── CustomPageableResponse.java
│ │ │ ├── service/ # Business logic
│ │ │ │ ├── interfaces/
│ │ │ │ │ ├── IBookService.java
│ │ │ │ │ └── IUserService.java
│ │ │ │ └── implementation/
│ │ │ │ ├── BookServiceImpl.java
│ │ │ │ ├── UserServiceImpl.java
│ │ │ │ ├── UserDetailServiceImpl.java
│ │ │ │ └── OpenLibraryService.java
│ │ │ └── util/ # Utility classes
│ │ │ ├── JwtUtils.java
│ │ │ └── deserializer/
│ │ │ └── IdentifierDeserializer.java
│ │ └── resources/
│ │ ├── application.properties
│ │ └── static/
│ └── test/
│ └── java/com/raven/training/ # Test classes (mirrors main structure)
├── pom.xml # Maven configuration
├── mvnw # Maven wrapper (Unix)
├── mvnw.cmd # Maven wrapper (Windows)
└── README.md
Package Structure
Main Application Class
@ SpringBootApplication
public class TrainingApplication {
public static void main ( String [] args ) {
SpringApplication . run ( TrainingApplication . class , args);
}
}
The @SpringBootApplication annotation combines @Configuration, @EnableAutoConfiguration, and @ComponentScan.
1. Config Package (config/)
Purpose : Application configuration, security, and filters
Key Components:
Configures Spring Security with JWT authentication:
Implements stateless authentication
Defines public endpoints (Swagger, auth)
Protects API endpoints with JWT
Configures BCrypt password encoding
Sets up CORS and CSRF policies
JwtTokenValidator.java (filter/)
Custom filter for validating JWT tokens:
Extracts JWT from Authorization header
Validates token signature and expiration
Sets authentication in SecurityContext
Runs on every request to protected endpoints
SwaggerConfig.java (security/)
Configures OpenAPI/Swagger documentation:
Defines API information and metadata
Configures JWT security scheme for Swagger UI
Enables interactive API testing
General application configuration:
Bean definitions (RestTemplate, ObjectMapper, etc.)
Custom serializers/deserializers
Application-wide settings
Usage Example :
@ Configuration
@ EnableWebSecurity
public class SecurityConfig {
@ Bean
public SecurityFilterChain securityFilterChain ( HttpSecurity http ) {
// Security configuration
}
}
2. Exception Package (exception/)
Purpose : Centralized exception handling and custom error types
Structure:
error/ - Custom exception classes:
BookNotFoundException - Thrown when a book is not found
BookAlreadyInCollectionException - Book already in user’s collection
BookNotInCollectionException - Book not in user’s collection
UserNotFoundException - User not found
EmailAlreadyExistsException - Email already registered
UsernameAlreadyExistsException - Username already taken
handler/ - Exception handlers:
GlobalExceptionHandler - Centralized exception handling using @ControllerAdvice
Example Custom Exception :
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException ( UUID id ) {
super ( "Book not found with id: " + id);
}
}
Global Exception Handler :
@ RestControllerAdvice
public class GlobalExceptionHandler {
@ ExceptionHandler ( BookNotFoundException . class )
public ResponseEntity < ErrorResponse > handleBookNotFound ( BookNotFoundException ex ) {
ErrorResponse error = new ErrorResponse (
HttpStatus . NOT_FOUND . value (),
ex . getMessage (),
LocalDateTime . now ()
);
return new ResponseEntity <>(error, HttpStatus . NOT_FOUND );
}
}
3. Mapper Package (mapper/)
Purpose : Object mapping between layers using MapStruct
Key Mappers :
@ Mapper ( componentModel = "spring" ,
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy . IGNORE )
public interface IBookMapper {
@ Mapping ( target = "id" , ignore = true )
@ Mapping ( target = "users" , ignore = true )
Book toEntity ( BookRequest bookRequest );
BookResponse toResponse ( Book book );
List < BookResponse > toResponseList ( List < Book > bookList );
}
@ Mapper ( componentModel = "spring" ,
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy . IGNORE )
public interface IUserMapper {
@ Mapping ( target = "id" , ignore = true )
@ Mapping ( target = "books" , ignore = true )
User toEntity ( UserRequest userRequest );
UserResponse toResponse ( User user );
List < UserResponse > toResponseList ( List < User > userList );
}
MapStruct generates implementation classes at compile time. The generated classes are in target/generated-sources/annotations/.
Why MapStruct?
Type-safe object mapping
Compile-time code generation (no reflection overhead)
Integration with Lombok
Automatic null handling
Custom mapping rules support
4. Persistence Package (persistence/)
Purpose : Data access layer, entities, and repositories
Subpackages:
entity/ JPA entities mapped to database tables
model/ Domain models and value objects
repository/ Spring Data JPA repositories
Entities
Book Entity :
@ Entity
@ Table ( name = "books" )
@ Data
@ Builder
@ NoArgsConstructor
@ AllArgsConstructor
public class Book {
@ Id
@ GeneratedValue ( strategy = GenerationType . UUID )
private UUID id ;
private String title ;
private String author ;
private String gender ; // Genre
private String image ;
private String subtitle ;
private String publisher ;
private String year ;
private Integer pages ;
@ Column ( unique = true )
private String isbn ;
@ ManyToMany ( mappedBy = "books" )
private Set < User > users = new HashSet <>();
}
User Entity :
@ Entity
@ Table ( name = "users" )
@ Data
@ Builder
@ NoArgsConstructor
@ AllArgsConstructor
public class User {
@ Id
@ GeneratedValue ( strategy = GenerationType . UUID )
private UUID id ;
private String name ;
private String email ;
@ ManyToMany
@ JoinTable (
name = "user_books" ,
joinColumns = @ JoinColumn ( name = "user_id" ),
inverseJoinColumns = @ JoinColumn ( name = "book_id" )
)
private Set < Book > books = new HashSet <>();
}
Repositories
IBookRepository :
@ Repository
public interface IBookRepository extends JpaRepository < Book , UUID > {
@ Query ( "SELECT b FROM Book b WHERE " +
"(:title IS NULL OR :title = '' OR LOWER(b.title) LIKE LOWER(CONCAT('%', :title, '%'))) AND " +
"(:author IS NULL OR :author = '' OR LOWER(b.author) LIKE LOWER(CONCAT('%', :author, '%'))) AND " +
"(:gender IS NULL OR :gender = '' OR LOWER(b.gender) LIKE LOWER(CONCAT('%', :gender, '%')))" )
Page < Book > findAllWithFilters (
@ Param ( "title" ) String title ,
@ Param ( "author" ) String author ,
@ Param ( "gender" ) String gender ,
Pageable pageable
);
boolean existsByIsbn ( String isbn );
}
Spring Data JPA provides automatic implementation of repository interfaces.
5. Presentation Package (presentation/)
Purpose : REST API controllers and data transfer objects
Subpackages:
controller/ - REST endpoints:
BookController - Book management endpoints
UserController - User management endpoints
AuthUserController - Authentication endpoints (login, register)
dto/ - Data Transfer Objects organized by domain:
book/ - Book DTOs
user/ - User DTOs
login/ - Login DTOs
register/ - Registration DTOs
pagination/ - Pagination response wrapper
Controller Example
@ RestController
@ RequestMapping ( "/api/v1/books" )
@ AllArgsConstructor
public class BookController {
private final IBookService bookService ;
private final OpenLibraryService openLibraryService ;
private final IBookRepository bookRepository ;
/**
* Retrieves a paginated list of books with optional filtering.
*/
@ GetMapping ( "/findAll" )
public ResponseEntity < CustomPageableResponse < BookResponse >> findAll (
@ RequestParam ( defaultValue = "0" ) int page ,
@ RequestParam ( defaultValue = "10" ) int size ,
@ RequestParam ( required = false ) String title ,
@ RequestParam ( required = false ) String author ,
@ RequestParam ( required = false ) String gender ) {
Pageable pageable = PageRequest . of (page, size, Sort . by ( "id" ). ascending ());
Page < BookResponse > booksPage = bookService . findAll (title, author, gender, pageable);
CustomPageableResponse < BookResponse > response = new CustomPageableResponse <>(
booksPage . getContent (),
booksPage . getNumberOfElements (),
booksPage . getSize (),
booksPage . getNumber () * booksPage . getSize (),
booksPage . getTotalPages (),
booksPage . getTotalElements (),
booksPage . hasPrevious () ? booksPage . getNumber () : null ,
booksPage . getNumber () + 1 ,
booksPage . hasNext () ? booksPage . getNumber () + 2 : null
);
return new ResponseEntity <>(response, HttpStatus . OK );
}
@ GetMapping ( "/findById/{id}" )
public ResponseEntity < BookResponse > findById (@ PathVariable UUID id ) {
return new ResponseEntity <>( bookService . findById (id), HttpStatus . OK );
}
@ PostMapping ( "/create" )
public ResponseEntity < BookResponse > create (@ RequestBody BookRequest bookRequest ) {
return new ResponseEntity <>( bookService . save (bookRequest), HttpStatus . CREATED );
}
@ PutMapping ( "/update/{id}" )
public ResponseEntity < BookResponse > update (
@ PathVariable UUID id ,
@ RequestBody BookRequest bookRequest ) {
return new ResponseEntity <>( bookService . update (id, bookRequest), HttpStatus . OK );
}
@ DeleteMapping ( "/delete/{id}" )
public void delete (@ PathVariable UUID id ) {
bookService . delete (id);
}
}
DTO Structure
Request DTO (Record) :
public record BookRequest (
String gender,
String author,
String image,
String title,
String subtitle,
String publisher,
String year,
Integer pages,
String isbn
) {}
Response DTO (Record) :
public record BookResponse (
UUID id,
String gender,
String author,
String image,
String title,
String subtitle,
String publisher,
String year,
Integer pages,
String isbn
) {}
The project uses Java Records for immutable DTOs, providing concise syntax and built-in equals/hashCode/toString.
6. Service Package (service/)
Purpose : Business logic layer
Structure:
interfaces/ - Service contracts:
IBookService - Book business operations
IUserService - User business operations
implementation/ - Service implementations:
BookServiceImpl - Book business logic
UserServiceImpl - User business logic
UserDetailServiceImpl - Spring Security user details
OpenLibraryService - External API integration
Service Pattern
public interface IBookService {
Page < BookResponse > findAll ( String title , String author , String gender , Pageable pageable );
BookResponse findById ( UUID id );
BookResponse save ( BookRequest bookRequest );
BookResponse update ( UUID id , BookRequest bookRequest );
void delete ( UUID id );
}
@ Service
@ AllArgsConstructor
public class BookServiceImpl implements IBookService {
private final IBookRepository bookRepository ;
private final IBookMapper bookMapper ;
@ Override
public Page < BookResponse > findAll ( String title , String author , String gender , Pageable pageable ) {
// Check if any filter is provided
boolean hasFilters = (title != null && ! title . isEmpty ()) ||
(author != null && ! author . isEmpty ()) ||
(gender != null && ! gender . isEmpty ());
Page < Book > books ;
if (hasFilters) {
books = bookRepository . findAllWithFilters (
title != null ? title : "" ,
author != null ? author : "" ,
gender != null ? gender : "" ,
pageable
);
} else {
books = bookRepository . findAll (pageable);
}
return books . map (bookMapper :: toResponse);
}
@ Override
public BookResponse findById ( UUID id ) {
Book book = bookRepository . findById (id)
. orElseThrow (() -> new BookNotFoundException (id));
return bookMapper . toResponse (book);
}
@ Override
@ Transactional
public BookResponse save ( BookRequest bookRequest ) {
Book book = bookMapper . toEntity (bookRequest);
Book savedBook = bookRepository . save (book);
return bookMapper . toResponse (savedBook);
}
@ Override
@ Transactional
public BookResponse update ( UUID id , BookRequest bookRequest ) {
Book existingBook = bookRepository . findById (id)
. orElseThrow (() -> new BookNotFoundException (id));
// Update only non-null fields
if ( bookRequest . title () != null && ! bookRequest . title (). isBlank ()) {
existingBook . setTitle ( bookRequest . title ());
}
if ( bookRequest . author () != null && ! bookRequest . author (). isBlank ()) {
existingBook . setAuthor ( bookRequest . author ());
}
// ... update other fields
Book updatedBook = bookRepository . save (existingBook);
return bookMapper . toResponse (updatedBook);
}
@ Override
@ Transactional
public void delete ( UUID id ) {
Book book = bookRepository . findById (id)
. orElseThrow (() -> new BookNotFoundException (id));
bookRepository . delete (book);
}
}
Always use @Transactional on service methods that modify data to ensure database consistency.
7. Util Package (util/)
Purpose : Utility classes and helpers
Key Components:
JwtUtils
IdentifierDeserializer
Encapsulates JWT token management: @ Component
public class JwtUtils {
public String generateToken ( String username , Collection < ? extends GrantedAuthority > authorities ) {
// Generate JWT token
}
public String validateToken ( String token ) {
// Validate and extract username
}
public boolean isTokenValid ( String token ) {
// Check token validity
}
public String extractUsername ( String token ) {
// Extract username from token
}
}
Custom Jackson deserializer for handling variable identifier formats: public class IdentifierDeserializer extends JsonDeserializer < String > {
@ Override
public String deserialize ( JsonParser p , DeserializationContext ctxt )
throws IOException {
JsonNode node = p . getCodec (). readTree (p);
// Handle string identifier
if ( node . isTextual ()) {
return node . asText ();
}
// Handle array identifier (take first element)
if ( node . isArray () && node . size () > 0 ) {
return node . get ( 0 ). asText ();
}
return null ;
}
}
Used for OpenLibrary API responses where identifiers can be strings or arrays.
Design Patterns
1. Layered Architecture
Clear separation between presentation, business logic, and data access layers.
2. Repository Pattern
Abstraction of data access logic through Spring Data JPA repositories.
3. Service Pattern
Business logic encapsulated in service classes, separated from controllers.
4. DTO Pattern
Data transfer objects for API communication, separate from entities.
5. Dependency Injection
Constructor-based dependency injection using Spring’s @AllArgsConstructor (Lombok) or @Autowired.
6. Exception Handling Pattern
Centralized exception handling with @ControllerAdvice and custom exceptions.
How to Add New Features
Adding a New Entity
Create the entity
Add entity class in persistence/entity/ @ Entity
@ Table ( name = "reviews" )
@ Data
@ Builder
public class Review {
@ Id
@ GeneratedValue ( strategy = GenerationType . UUID )
private UUID id ;
private String content ;
private Integer rating ;
}
Create the repository
Add repository interface in persistence/repository/ public interface IReviewRepository extends JpaRepository < Review , UUID > {
List < Review > findByRatingGreaterThan ( Integer rating );
}
Create DTOs
Add request/response DTOs in presentation/dto/review/ public record ReviewRequest ( String content, Integer rating) {}
public record ReviewResponse ( UUID id, String content, Integer rating) {}
Create mapper
Add mapper interface in mapper/ @ Mapper ( componentModel = "spring" )
public interface IReviewMapper {
Review toEntity ( ReviewRequest request );
ReviewResponse toResponse ( Review review );
}
Create service
Add service interface in service/interfaces/ and implementation in service/implementation/
Create controller
Add controller in presentation/controller/ @ RestController
@ RequestMapping ( "/api/v1/reviews" )
public class ReviewController {
// Controller methods
}
Add tests
Create test classes mirroring the main structure in src/test/
Adding a New API Endpoint
Add method to service interface
Define the business operation signature
Implement in service class
Add business logic implementation
Add controller method
Expose the endpoint with proper HTTP method and path
Document in Swagger
Add OpenAPI annotations for documentation
Write tests
Create unit tests for service and controller
API Documentation
The API is documented using Swagger/OpenAPI . Access the interactive documentation at:
http://localhost:8081/swagger-ui/index.html
Interactive Testing Test endpoints directly from the browser
JWT Authentication Authenticate with JWT tokens in Swagger UI
Schema Definitions View all request/response schemas
Try It Out Execute real API calls with sample data
Configuration Files
application.properties
spring.application.name =training
# PostgreSQL Database Configuration
spring.datasource.url =${db_url}
spring.datasource.username =${db_username}
spring.datasource.password =${db_password}
spring.datasource.driver-class-name =org.postgresql.Driver
# Hibernate/JPA Configurations
spring.jpa.database-platform =org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto =create-drop
# Server Configuration
server.port =8081
# JWT Security
security.jwt.user.generator =${user_jwt}
security.jwt.key.private =${key_jwt}
pom.xml Highlights
Java Version : 21
Spring Boot : 3.5.3
MapStruct : 1.6.3
Lombok : 1.18.32
JaCoCo : 0.8.13 (80% coverage requirement)
Best Practices
Use Records for DTOs Immutable, concise, and type-safe
Constructor Injection Prefer constructor injection over field injection
Interface-Based Services Define service contracts with interfaces
Transactional Services Use @Transactional for data modifications
Custom Exceptions Create domain-specific exceptions
Global Exception Handling Centralize error handling with @ControllerAdvice
Pagination Support Use Spring Data’s Pageable for list endpoints
Comprehensive Testing Maintain 80%+ code coverage
Next Steps
Setup Guide Configure your development environment
Testing Learn about testing strategy and coverage
API Reference Explore all available endpoints
Authentication Implement JWT authentication