Click Here To See The Project Codebase
Book library with Onion (Hexagonal + DDD) Architecture, CQRS, Java Swing, Spring boot, Spring data, Spring security, H2, MYSQL, Openapi, Spring MVC, Thymeleaf & JDBC. Approaches such as password encryption, thread per request model and use of containerization with docker for the infrastructure has been realized.



A library manager built with hexagonal architecture principles and clean architecture domain-driven design (DDD), including patterns such as CQRS.
It now supports the original Swing user interface, Spring MVC and the Spring Boot REST API, with Spring Data JPA + H2 configured for the Spring run profile while retaining the same application/domain core. For Swing usage, Docker has been used to use MySQL as the infrastructure DB.
application.ports.input.services interfaces. This keeps UI logic interchangeable while preserving domain rules.application.ports.output. Infrastructure implements them with JDBC, JPA adapter repositories, password encryption, and system time utilities, enabling replacement without touching domain code.
BookCommandApplicationService + BookQueryApplicationService) and work alongside UserApplicationService and other services to orchestrate repositories, mappers, and security ports while enforcing high-level workflows.Book, Author, User, and UserBookState encapsulate business rules. Validation methods (validate()) protect invariants such as rating ranges, sensible publication years, release date ordering, and mandatory author names. Violations raise domain exceptions to keep rule enforcement close to the model.JdbcUnitOfWork scopes transactional work; CypherPasswordEncryptor fulfills the password hashing/verification port; CurrentUserHolder (thread-local) implements the security context port; DatabaseConfig centralizes connection lifecycle.
PasswordEncryptor output port abstracts credential hashing/verification. CypherPasswordEncryptor encrypts and matches passwords before persistence or authentication, isolating crypto details from the UI and application layers.CurrentUser holder. Application services read this context to authorize admin-only actions (create/update/delete books, orphan author cleanup) without passing session state through every method. UI classes only trigger login; they never cache credentials or user objects directly.
Book.validate() constrains publication year ranges and required fields; UserBookState.validate() bounds ratings, restricts release dates to logical windows, and ensures status transitions remain consistent.User aggregate and the target Book.
JdbcUnitOfWork to keep cross-aggregate updates atomic.
src/main/java/com/finalproject
├── presentation
│ ├── spring # Spring API starter + REST controllers (inbound adapters)
│ └── swing # Swing starter, views, and swing bootstrap wiring
├── application # Use cases, ports, mappers, and application DTOs
├── domain # Entities, value objects, domain exceptions (core model)
└── infrastructure
├── configuration # Spring security/openapi/bean configuration
├── persistence
│ ├── jdbc # JDBC adapters for Swing runtime
│ └── jpa # JPA adapters/entities/repositories for Spring runtime
└── security # Shared security helpers/context
jdbc:h2:mem:mylibrary) and Spring Data JPA. Schema and seed data are loaded automatically from src/main/resources/db/h2/schema.sql and src/main/resources/db/h2/data.sql.DatabaseConfig is a lightweight singleton that loads the H2 driver, provides connections, and offers a shutdown helper.JdbcUnitOfWork wraps JDBC operations, handling commit/rollback around supplied actions to keep application service workflows consistent.
http://localhost:8080/swagger-ui/index.htmlhttp://localhost:8080/v3/api-docsBearer <your_token> (token from /api/auth/login)./api/auth/login is open; all other /api/** endpoints require JWT authentication.
validate() methods and domain-specific exception types.application.ports.input and application.ports.output decouple use cases from transport, storage, and security implementations.DependencyInjector wires concrete infrastructure implementations into application services without a container, keeping wiring explicit and testable.PasswordEncryptor abstraction to avoid leaking crypto details.DomainException instances to surface validation or permission errors in the UI.
Swing-Report.pdf for the original project write-up.src/main/resources/covers/.
| Category | Technology | Notes |
|---|---|---|
| Language | Java 17 | Records, sealed types, text blocks |
| Framework | Spring Boot 3.3.5 | Auto-configuration, embedded Tomcat |
| Web MVC | Spring MVC + Thymeleaf | Server-rendered pages, Thymeleaf Security extras |
| REST | Spring Web (Jackson) | JSON serialization/deserialization |
| API docs | springdoc-openapi 2.6 | Swagger UI, OpenAPI 3 spec |
| Security | Spring Security 6 | Stateless JWT chain (/api/**) + session chain (/mvc/**) |
| Auth tokens | JJWT 0.12.6 | HS256 signed JWTs; claims: userId, username, userType |
| ORM | Spring Data JPA / Hibernate | Spring runtime persistence |
| Raw JDBC | Custom JDBC adapters | Swing runtime persistence against MySQL |
| DB (Spring) | H2 in-memory | jdbc:h2:mem:mylibrary; schema + seed auto-applied |
| DB (Swing) | MySQL 8 (Docker) | Full relational schema, pre-seeded DML |
| Containers | Docker + docker-compose | MySQL service for Swing runtime |
| Desktop UI | Java Swing | NetBeans .form + .java pairs |
| Build | Maven 3 | Spring Boot Maven Plugin |
| Boilerplate | Lombok | @RequiredArgsConstructor, @Builder where applicable |
| Java versions | Java 17 | var, sealed interfaces, pattern matching |
╔══════════════════════════════════════════════════════════════════╗
║ PRESENTATION (UI) ║
║ ║
║ ┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐ ║
║ │ Spring REST │ │ Spring MVC │ │ Swing Desktop │ ║
║ │ /api/** │ │ /mvc/** │ │ (JFrame/JPanel) │ ║
║ │ JWT + OpenAPI│ │ Thymeleaf+Sec │ │ DependencyInjector│ ║
║ └──────┬───────┘ └────────┬─────────┘ └────────┬──────────┘ ║
╚═════════╪══════════════════╪═══════════════════════╪════════════╝
│ │ │
│ application.ports.input.services (interfaces only)
▼ ▼ ▼
╔══════════════════════════════════════════════════════════════════╗
║ APPLICATION LAYER ║
║ ║
║ BookCommandApplicationService BookQueryApplicationService ║
║ UserApplicationService AuthorApplicationService ║
║ UserBookStateApplicationService ║
║ ║
║ application.ports.output.* (interfaces — never implemented here)║
╚═════════════╪══════════════════════════════════╪════════════════╝
│ domain objects only │ port calls
▼ ▼
╔══════════════════╗ ╔═══════════════════════════════════╗
║ DOMAIN LAYER ║ ║ INFRASTRUCTURE LAYER ║
║ ║ ║ ║
║ Book ║ ║ Spring runtime: ║
║ Author ║ ║ ├─ JPA entity adapters ║
║ User ║ ║ ├─ Spring Data repositories ║
║ UserBookState ║ ║ ├─ JWT filter + BCrypt encoder ║
║ 14 Value Objects ║ ║ └─ SecurityContextHolder adapter ║
║ 9 Exceptions ║ ║ ║
║ validate() ║ ║ Swing runtime: ║
╚══════════════════╝ ║ ├─ JDBC adapters (MySQL) ║
║ ├─ JdbcUnitOfWork ║
║ ├─ DatabaseConfig singleton ║
║ └─ ThreadLocal CurrentUser holder║
╚═══════════════════════════════════╝
Dependency rule: every arrow points inward. Domain has zero external dependencies. Application depends only on Domain. Infrastructure and Presentation depend on Application and Domain — never the reverse.
BaseEntity<ID> abstract — holds typed ID, declares validate()
├── Book year [1000–currentYear], pages [0–10 000], cover, about
├── Author name, surname, optional website URL
├── User username, hashed password, UserType (READER | ADMIN)
└── Comment content string with CommentId
AggregateRoot<ID> extends BaseEntity — marks true consistency boundary
└── UserBookState userId FK + bookId FK + Read enum + Rating + comments + ReleaseDate
UserBookState is the primary aggregate: reading status, ratings (0–5, minimum 1 when READ), comments, and optional wishlist release dates are all validated together in a single validate() call before any persistence operation.
| Value Object | Type | Invariant enforced |
|---|---|---|
BookId |
int wrapper |
Typed identity, prevents accidental mixing with AuthorId |
AuthorId |
int wrapper |
Typed identity |
UserId |
int wrapper |
Typed identity |
CommentId |
int wrapper |
Typed identity |
UserBookStateId |
int wrapper |
Typed identity |
Year |
int wrapper |
Must be in [1000, LocalDate.now().getYear()] |
NumberOfPages |
int wrapper |
Must be in [0, 10 000] |
Cover |
String wrapper |
Path string; file-existence validation available (disabled for seed path compatibility) |
Rating |
int wrapper |
0–5; tightened to 1–5 when read == READ |
Read |
enum |
READ, NOT_READ, WISH_TO_BE_READ |
ReleaseDate |
LocalDate wrapper |
Must be present iff read == WISH_TO_BE_READ; must be null otherwise |
UserType |
enum |
READER, ADMIN |
Website |
String wrapper |
URL string for author portfolio |
BaseId<T> |
Generic abstract | Shared equals/hashCode on raw value for all typed IDs |
DomainException (RuntimeException)
├── BookDomainException — validation failures on Book
├── BookNotFoundException — no book found for a given BookId
├── AuthorDomainException — validation failures on Author
├── AuthorNotFoundException — no author found for BookId/AuthorId
├── UserDomainException — permission checks, role violations
├── UserNotFoundException — no user found for credentials/id
├── UserBookStateDomainException — state/rating/releaseDate inconsistencies
└── UserBookStateNotFoundException — no state record found for user+book
All exceptions are unchecked. Application services catch nothing — they propagate up to the presentation layer, where Thymeleaf templates or REST exception handlers translate them into user messages or HTTP status codes.
The book catalog has very different read vs. write profiles: reads are frequent, public, and paginated; writes are infrequent, admin-gated, and transactional. Separating the two services means:
UnitOfWork, always followed by validate().BookCommandApplicationService
| Method | Guard | Transaction | Side-effects |
|---|---|---|---|
createBook(CreateBookCommand) |
ADMIN |
UnitOfWork |
Creates Author on-demand if name+surname not found |
updateBook(UpdateBookCommand) |
ADMIN |
UnitOfWork |
Creates new Author on-demand; deletes old Author if it has no remaining books |
deleteBook(DeleteBookCommand) |
ADMIN |
UnitOfWork |
Deletes Author if it has no remaining books |
BookQueryApplicationService
| Method | Input | Output |
|---|---|---|
find(GetBookQuery) |
bookId: int |
FindBookResponse or BookNotFoundException |
findAll(SearchBooksQuery) |
title?: String, PageQuery(page, size) |
PageResult<FindBookResponse> |
PageResult<T> is a generic record:
(List<T> content, int page, int size, long totalElements, int totalPages, boolean first, boolean last)
with a map(Function<T,S>) utility for transforming content without rebuilding pagination metadata.
| Method | Permission | Description |
|---|---|---|
findUserBookOfCurrentUser(bookId) |
Any authenticated | Returns reading state for a book + defaults if no record exists |
findUserBookStatistics(bookId) |
Any authenticated | Aggregate: totalReads, averageRating, commentsCount |
findFavouriteBooksOfCurrentUser() |
Any authenticated | Books where rating >= 3 |
findNotReadBooksYetOfCurrentUser() |
Any authenticated | Books with no state record + records with NOT_READ combined |
findWishedBooksToReadThatWillBeDoneIn1WeekOfCurrentUser() |
ADMIN |
Wishlist entries with releaseDate within the next 7 days |
createUserBookForCurrentUser(request) |
Any authenticated | Calls UserBookState.validate() before save |
updateUserBookForCurrentUser(request) |
Owner only | Verifies userId == currentUser.id before update |
The project demonstrates that the same application/domain core can run against completely different infrastructure stacks:
| Concern | Swing (original) | Spring (extended) |
|---|---|---|
| Entry point | SwingDesktopApplication |
SpringApplication |
| DI container | Manual DependencyInjector class |
Spring IoC (@Bean, @Service) |
| Persistence | JDBC adapters — raw PreparedStatement/ResultSet |
JPA adapters — Spring Data JpaRepository |
| Database | MySQL 8 via Docker | H2 in-memory — zero external deps |
| Transaction | JdbcUnitOfWork (autoCommit=false, manual commit/rollback) |
Spring @Transactional (via JPA adapter) |
| User session | ThreadLocal CurrentUser holder |
SecurityContextHolder adapter |
| Auth | CypherPasswordEncryptor (BCrypt) |
Spring Security + BCrypt + JWT |
| UI | Java Swing (JFrame, JPanel, JTable, .form) |
Thymeleaf HTML templates + REST JSON |
Neither runtime knows about the other. The application and domain layers compile and run identically in both contexts — the only difference is which beans are wired in the DependencyInjector vs. Spring's context.
Client Spring Security Application layer
│ │ │
│ POST /api/auth/login │ │
│──────────────────────────► │ │
│ │ UserApplicationService │
│ │ ──────────────────────────── ►│
│ │ PasswordEncryptor.matches() │
│ │ ◄──────────────────────────── │
│ │ JwtTokenProvider.generate() │
│◄────────── {token} ─────── │ │
│ │ │
│ GET /api/books (Bearer) │ │
│──────────────────────────► │ │
│ JwtAuthenticationFilter │
│ validates token, sets SecurityContext │
│ │ CurrentUserHolderAdapter │
│ │ reads SecurityContextHolder │
│ │ ──────────────────────────── ►│
│ │ isAdmin() / getId() checks │
│◄────────── response ─────── │ ◄──────────────────────────── │
Two independent filter chains prevent them from interfering:
@Order(1) ApiSecurityConfig → securityMatcher("/api/**") — stateless, JWT
@Order(2) MvcSecurityConfig → securityMatcher("/mvc/**") — stateful, form login
CurrentUser and PasswordEncryptor are output port interfaces in the application layer. This means:
io.jsonwebtoken, BCryptPasswordEncoder, or SecurityContextHolder.
| Screen | Class | Access | Description |
|---|---|---|---|
| Login | Login.java |
Public | Username/password form; calls UserApplicationService; populates CurrentUser thread-local |
| Main Dashboard | MainPanel.java |
Authenticated | Navigation hub; shows user info and buttons to all sub-screens |
| Display Book | DisplayBook.java |
Authenticated | Full book detail: cover, metadata, reading state buttons |
| Book Operations | BookOperations.java |
Admin | Lists all books; entry to create/edit/delete flows |
| Book Creation | BookCreation.java |
Admin | Form to create a book + author; calls BookCommandApplicationService.createBook() |
| Book Edit | BookEdit.java |
Admin | Pre-filled edit form; calls updateBook() |
| Reading State Manager | BookStateManager.java |
Authenticated | Sets Read status, Rating, comments, optional releaseDate |
| Favourite Books | FavoriteBooks.java |
Authenticated | Books rated ≥ 3 via findFavouriteBooksOfCurrentUser() |
| Favourite Authors | FavoriteAuthors.java |
Authenticated | Authors of the user's favourite books |
| Unread Books | UnreadBooks.java |
Authenticated | Books with NOT_READ or no state record |
| Wishlist Notification | WishlistNotification.java |
Admin | Wishlist entries due within 7 days |
| Search Author | SearchAuthor.java |
Authenticated | Author search by name |
POST /api/auth/login
Content-Type: application/json
{ "username": "alice", "password": "yasar1" }
Response:
{ "id": 1, "username": "admin", "userType": "ADMIN", "token": "<JWT>" }
All other endpoints require Authorization: Bearer <JWT>.
# Paginated catalogue
GET /api/books?page=0&size=10
# Title search
GET /api/books?title=refactoring&page=0&size=5
# Single book
GET /api/books/{id}
# Create (ADMIN, 201 Created)
POST /api/books
{
"authorName": "Martin", "authorSurname": "Fowler",
"title": "Refactoring", "year": 2018,
"numberOfPages": 448, "about": "Improving existing code",
"coverPath": "src/main/resources/covers/Book1.jpg"
}
# Update (ADMIN, 200 OK)
PUT /api/books/{id} { same fields }
# Delete (ADMIN, 204 No Content)
DELETE /api/books/{id}
# Get my state for a book
GET /api/user-book-states?bookId={id}
# Create my state
POST /api/user-book-states
{
"bookId": 1,
"read": "READ",
"rating": 5,
"comments": ["Highly recommended"]
}
# Wishlist entry (WISH_TO_BE_READ requires releaseDate)
POST /api/user-book-states
{
"bookId": 2,
"read": "WISH_TO_BE_READ",
"rating": 0,
"releaseDate": "2025-03-01"
}
# Update
PUT /api/user-book-states/{id} { same fields }
Default test credentials: admin / 123 (ADMIN) · reader / 123 (READER)
| Pattern | Implementation | Location |
|---|---|---|
| Hexagonal / Ports & Adapters | Input/output port interfaces; all adapters implement ports, never extend domain | application.ports.* + all infrastructure.* |
| Domain-Driven Design | Entities, aggregate roots, value objects, domain exceptions, ubiquitous language, validate() |
domain.* |
| CQRS | Separate command and query application service interfaces and implementations | BookCommandApplicationService* / BookQueryApplicationService* |
| Repository | Port interface + JDBC adapter + JPA adapter per aggregate | ports.output.repository.* |
| Unit of Work | UnitOfWork @FunctionalInterface port; JdbcUnitOfWork wraps commit/rollback |
UnitOfWork.java, JdbcUnitOfWork.java |
| Builder | Every aggregate and entity exposes a nested Builder with fluent setters |
Book.Builder, Author.Builder, User.Builder, UserBookState.Builder |
| Factory Method | Book.Builder.newBuilder(), User.Builder.newBuilder() |
Domain entity builders |
| Adapter | JPA ↔ domain mappers; JDBC adapters; PasswordEncoderImpl; CurrentUserHolderAdapter |
infrastructure.* |
| Strategy | JDBC vs JPA persistence strategies injected behind the same port interface | Dual runtime — same port, two implementations |
| Template Method | BaseEntity.validate() declared abstract; each entity provides its own rules |
BaseEntity, Book, UserBookState |
| Facade | Application services present a simple interface over domain + UnitOfWork + multiple repos | All *ApplicationServiceImpl classes |
| Null Object | findUserBookStatistics returns a zero-value FindUserBookStatisticsResponse instead of null |
UserBookStateApplicationServiceImpl |
| Singleton | DatabaseConfig manages one shared JDBC connection lifecycle for Swing runtime |
DatabaseConfig.java |
| Dependency Injection (manual) | DependencyInjector wires concrete classes without a container — makes dependencies explicit |
presentation.swing.dependency |
# Clone git clone <repo-url> cd book-library-jdbc-swing # Run Spring (REST + MVC, H2 auto-starts) mvn spring-boot:run # Then open: open http://localhost:8080/mvc/books # Web UI open http://localhost:8080/swagger-ui/index.html # API docs
| URL | Description |
|---|---|
/mvc/books |
Thymeleaf home — hero section + trending carousel |
/mvc/books/{id} |
Book detail — stats, user rating & comments |
/mvc/login |
Form login |
/swagger-ui/index.html |
Interactive API explorer |
/v3/api-docs |
Raw OpenAPI 3 JSON |
| User | Password | Role |
|---|---|---|
admin |
123 |
ADMIN — full CRUD |
reader |
123 |
READER — reading states only |