Skip to main content

Overview

The Library Management API implements a three-tier layered architecture that separates concerns into distinct layers: Presentation, Service, and Persistence. This design promotes maintainability, testability, and scalability by enforcing clear boundaries between different aspects of the application.

The Three Layers

┌───────────────────────────────────────────────────────────┐
│                   PRESENTATION LAYER                       │
│  • REST Controllers                                        │
│  • Request/Response DTOs                                   │
│  • Input Validation                                        │
│  • HTTP Status Mapping                                     │
└───────────────────────┬───────────────────────────────────┘

                        │ Delegates business logic

┌───────────────────────────────────────────────────────────┐
│                     SERVICE LAYER                          │
│  • Business Logic                                          │
│  • Transaction Management                                  │
│  • Service Interfaces                                      │
│  • Service Implementations                                 │
└───────────────────────┬───────────────────────────────────┘

                        │ Performs data operations

┌───────────────────────────────────────────────────────────┐
│                   PERSISTENCE LAYER                        │
│  • JPA Repositories                                        │
│  • Entity Classes                                          │
│  • Database Queries                                        │
│  • Data Models                                             │
└───────────────────────────────────────────────────────────┘

Presentation Layer

The presentation layer handles all incoming HTTP requests and outgoing responses. It’s responsible for exposing REST API endpoints and managing the contract between clients and the application.

Components

Controllers are annotated with @RestController and handle HTTP requests:
  • BookController - Book management endpoints
  • UserController - User management and book collections
  • AuthUserController - Authentication and registration
Location: src/main/java/com/raven/training/presentation/controller/
DTOs define the shape of request and response payloads:
  • BookRequest / BookResponse
  • UserRequest / UserResponse
  • AuthLoginRequest / AuthLoginResponse
  • AuthRegisterRequest / AuthRegisterResponse
Location: src/main/java/com/raven/training/presentation/dto/

Example: BookController

Let’s examine how the BookController handles a typical request:
@RestController
@RequestMapping("/api/v1/books")
@AllArgsConstructor
public class BookController {
    
    private final IBookService bookService;
    
    @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);
    }
}
Full source: src/main/java/com/raven/training/presentation/controller/BookController.java:30-148

Key Characteristics

Thin Controllers

Controllers contain minimal logic - they delegate to services

HTTP Semantics

Proper use of HTTP methods (GET, POST, PUT, DELETE) and status codes

DTO Isolation

Internal entities are never exposed directly to clients

Dependency Injection

Constructor injection for all dependencies using @AllArgsConstructor

Service Layer

The service layer contains the business logic of the application. It acts as a bridge between controllers and repositories, orchestrating complex operations and enforcing business rules.

Service Interfaces

Service contracts are defined through interfaces, promoting loose coupling:
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);
}
Location: src/main/java/com/raven/training/service/interfaces/IBookService.java:19-66

Service Implementations

Implementations contain the actual business logic:
@Service
@AllArgsConstructor
public class BookServiceImpl implements IBookService {
    
    private IBookRepository bookRepository;
    private IBookMapper bookMapper;
    
    @Override
    @Transactional(readOnly = true)
    public BookResponse findById(UUID id) {
        Book book = bookRepository.findById(id)
                .orElseThrow(BookNotFoundException::new);
        
        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) {
        return bookRepository.findById(id)
                .map(existingBook -> {
                    // Update only non-null fields
                    if (bookRequest.title() != null && !bookRequest.title().trim().isEmpty()){
                        existingBook.setTitle(bookRequest.title());
                    }
                    if (bookRequest.author() != null && !bookRequest.author().trim().isEmpty()){
                        existingBook.setAuthor(bookRequest.author());
                    }
                    // ... more field updates
                    
                    Book bookUpdated = bookRepository.save(existingBook);
                    return bookMapper.toResponse(bookUpdated);
                })
                .orElseThrow(BookNotFoundException::new);
    }
}
Full source: src/main/java/com/raven/training/service/implementation/BookServiceImpl.java:27-156

Service Layer Responsibilities

1

Business Logic Execution

Implements all business rules and validations
2

Transaction Management

Defines transaction boundaries with @Transactional
3

Entity-DTO Transformation

Coordinates with mappers to convert between entities and DTOs
4

Exception Handling

Throws domain-specific exceptions (e.g., BookNotFoundException)
5

Orchestration

Coordinates multiple repository calls when needed

Transaction Management

The service layer uses Spring’s @Transactional annotation for declarative transaction management:
@Transactional(readOnly = true)
public BookResponse findById(UUID id) {
    // Read-only optimization
}
Read-only transactions (readOnly = true) are optimized by the database and prevent accidental writes.

Persistence Layer

The persistence layer manages all database interactions using Spring Data JPA. It provides an abstraction over the database, allowing the service layer to work with domain objects without SQL knowledge.

JPA Repositories

Repositories extend JpaRepository to inherit CRUD operations:
@Repository
public interface IBookRepository extends JpaRepository<Book, UUID> {
    
    @Query("""
        SELECT b FROM Book b 
        WHERE (COALESCE(:title, '') = '' OR LOWER(b.title) LIKE LOWER(CONCAT('%', :title, '%'))) 
        AND (COALESCE(:author, '') = '' OR LOWER(b.author) LIKE LOWER(CONCAT('%', :author, '%'))) 
        AND (COALESCE(:gender, '') = '' OR LOWER(b.gender) = LOWER(:gender))
    """)
    Page<Book> findAllWithFilters(
            @Param("title") String title,
            @Param("author") String author,
            @Param("gender") String gender,
            Pageable pageable
    );
    
    Optional<Book> findByIsbn(String isbn);
    
    boolean existsByIsbn(String isbn);
}
Location: src/main/java/com/raven/training/persistence/repository/IBookRepository.java:24-65

Entity Classes

JPA entities represent database tables:
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "book")
public class Book {
    
    @Id
    private UUID id;
    
    private String gender;
    private String author;
    private String image;
    private String title;
    private String subtitle;
    private String publisher;
    private String year;
    private Integer pages;
    private String isbn;
    
    @ManyToMany(mappedBy = "books", fetch = FetchType.LAZY)
    @Builder.Default
    private List<User> users = new ArrayList<>();
    
    @PrePersist
    public void prePersist() {
        if (id == null) {
            id = UUID.randomUUID();
        }
    }
}
Location: src/main/java/com/raven/training/persistence/entity/Book.java:27-61

Relationships

The application models a many-to-many relationship between Users and Books:
┌─────────────┐         ┌──────────────┐         ┌─────────────┐
│    User     │         │  user_books  │         │    Book     │
├─────────────┤         │  (join table)│         ├─────────────┤
│ id (PK)     │────────▶│ user_id (FK) │◀────────│ id (PK)     │
│ userName    │         │ book_id (FK) │         │ title       │
│ name        │         └──────────────┘         │ author      │
│ birthDate   │                                  │ isbn        │
│ books (List)│                                  │ users (List)│
└─────────────┘                                  └─────────────┘
User Entity:
@ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
    name = "user_books",
    joinColumns = @JoinColumn(name = "user_id"),
    inverseJoinColumns = @JoinColumn(name = "book_id")
)
private List<Book> books = new ArrayList<>();
Book Entity:
@ManyToMany(mappedBy = "books", fetch = FetchType.LAZY)
private List<User> users = new ArrayList<>();

The Mapping Layer

While not a traditional “layer”, the mapping layer plays a crucial role in converting between entities and DTOs.

MapStruct 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);
}
Location: src/main/java/com/raven/training/mapper/IBookMapper.java:22-53
MapStruct generates the implementation at compile time, not runtime, resulting in type-safe and performant mapping code.

Complete Request Flow Example

Let’s trace a complete request to create a new book:
1

1. HTTP Request Arrives

POST /api/v1/books/create
Authorization: Bearer <jwt-token>
Content-Type: application/json

{
  "title": "Clean Code",
  "author": "Robert C. Martin",
  "isbn": "978-0132350884",
  "pages": 464
}
2

2. Security Filter

JwtTokenValidator validates the JWT token and sets authentication context
3

3. BookController.create()

@PostMapping("/create")
public ResponseEntity<BookResponse> create(@RequestBody BookRequest bookRequest){
    return new ResponseEntity<>(bookService.save(bookRequest), HttpStatus.CREATED);
}
File: BookController.java:122-124
4

4. BookServiceImpl.save()

@Transactional
public BookResponse save(BookRequest bookRequest) {
    Book book = bookMapper.toEntity(bookRequest);  // DTO → Entity
    Book savedBook = bookRepository.save(book);     // Persist to DB
    return bookMapper.toResponse(savedBook);        // Entity → DTO
}
File: BookServiceImpl.java:84-91
5

5. IBookMapper.toEntity()

MapStruct converts BookRequest DTO to Book entity
6

6. IBookRepository.save()

Spring Data JPA persists the entity to the database
7

7. IBookMapper.toResponse()

MapStruct converts the saved Book entity to BookResponse DTO
8

8. HTTP Response

HTTP/1.1 201 Created
Content-Type: application/json

{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "title": "Clean Code",
  "author": "Robert C. Martin",
  "isbn": "978-0132350884",
  "pages": 464
}

Benefits of Layered Architecture

Separation of Concerns

Each layer has a single, well-defined responsibility

Testability

Layers can be tested independently with mocks

Maintainability

Changes in one layer don’t ripple to others

Reusability

Service logic can be reused by different controllers

Security

Internal entities never exposed to external clients

Scalability

Layers can be optimized or scaled independently

Best Practices

Anti-patterns to avoid:
  • Controllers calling repositories directly (bypassing service layer)
  • Business logic in controllers or repositories
  • Exposing entity classes directly as API responses
  • Service methods without proper transaction boundaries
Recommended practices:
  • Always use DTOs for API contracts
  • Keep controllers thin - delegate to services
  • Define service contracts through interfaces
  • Use @Transactional appropriately
  • Handle exceptions at the appropriate layer

Architecture Overview

High-level architecture and design principles

Security Architecture

JWT authentication and authorization flow

Build docs developers (and LLMs) love