A Spring Boot application demonstrating Spring State Machine for managing book lifecycle in a library system. This project showcases event-driven architecture, state transitions with guards and actions, and async email notifications.
This project demonstrates how to use Spring State Machine to model a real-world domain problem: managing the lifecycle of books in a library. Instead of manually checking and updating book states with conditional logic scattered across the codebase, the state machine provides:
- Declarative transitions: Define valid state changes in one place
- Guards: Business rules that must pass before a transition occurs
- Actions: Side effects triggered on state entry/exit or during transitions
- Single Source of Truth: Book state is persisted in the database; state machine is reconstructed on each operation
| Concept | Implementation |
|---|---|
| State Machine Factory | Creates state machine instances per book |
| Guards | Business rule validation (e.g., borrow limits) |
| Entry/Exit Actions | Lifecycle hooks (e.g., set dates, log events) |
| Transition Actions | Side effects (e.g., send email on return) |
| Extended State | Contextual data storage (dates, user info) |
| Event-Driven | Async email via Spring Events |
| State | Description |
|---|---|
AVAILABLE |
Book is on shelf, can be borrowed |
BORROWED |
Book is checked out to a user |
OVERDUE |
Borrowed book past due date |
ISSUED |
Permanently assigned (terminal state) |
| Event | Description |
|---|---|
BORROW_BOOK |
User checks out a book |
RETURN_BOOK |
User returns a book |
MARK_OVERDUE |
System marks book as overdue |
ISSUE_BOOK |
Permanent assignment to user |
RETURN_BOOK [action: email]
ββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β RETURN_BOOK [action: email] β
β βββββββββββββββββββββββββββββββββββββββ β
β β β β
βΌ β β β
βββββββββββββββ β β
(start) βββΆβ AVAILABLE β β β
ββββββββ¬βββββββ β β
β β β
β BORROW_BOOK β β
β [guard: userBorrowLimitGuard] β β
β [guard: userHasNoOverdueGuard] β β
β β β
βΌ β β
βββββββββββββββ β β
β BORROWED βββββββββββββββββββββββββββββββββββββ β
β β β
β entry: β β
β - set borrowDate β
β - set dueDate β
β - set borrowedByUserId β
β β
β exit: β
β - calculate duration β
β - set returnDate β
ββββββββ¬βββββββ β
β β
βββββββββββββΌββββββββββββ β
β β β β
β β β β
β MARK_ β β ISSUE_BOOK β
β OVERDUE β β β
β β β β
βΌ β βΌ β
βββββββββββββββ β βββββββββββββββ β
β OVERDUE β β β ISSUED β (terminal) β
β ββββββΌββββΆβ β β
β entry: β β βββββββββββββββ β
β - set β β β
β overdueDate β β
β β β β
ββββββββ¬βββββββ β β
β β β
β RETURN_BOOK [action: email] β
β β β
βββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββββββ
| From | Event | To | Guard | Action |
|---|---|---|---|---|
| AVAILABLE | BORROW_BOOK | BORROWED | userBorrowLimitGuard, userHasNoOverdueGuard | - |
| BORROWED | RETURN_BOOK | AVAILABLE | - | sendEmailAction |
| BORROWED | MARK_OVERDUE | OVERDUE | - | - |
| BORROWED | ISSUE_BOOK | ISSUED | - | - |
| OVERDUE | RETURN_BOOK | AVAILABLE | - | sendEmailAction |
| Guard | Rule |
|---|---|
userBorrowLimitGuard |
User cannot have more than 3 borrowed books |
userHasNoOverdueGuard |
User cannot borrow if they have overdue books |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Request Flow β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. API Request (PATCH /api/v1/books/{id}/borrow) β
β β β
β βΌ β
β 2. BookStatusChangeService.doAction() β
β β β
β βΌ β
β 3. Read current state from BookEntity (DB) βββ Single β
β β Source of β
β βΌ Truth β
β 4. Create StateMachine, reset to DB state β
β β β
β βΌ β
β 5. Send event, guards validate, transition executes β
β β β
β βΌ β
β 6. If successful: persist new state to BookEntity (DB) β
β β β
β βΌ β
β 7. State machine discarded (transient) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Java 21 or higher
- Maven 3.6+
# Clone the repository
git clone <repository-url>
cd library-management
# Build the project
./mvnw clean install
# Run the application
./mvnw spring-boot:run| URL | Description |
|---|---|
| http://localhost:8080/swagger-ui.html | Swagger UI |
| http://localhost:8080/api-docs | OpenAPI spec |
| http://localhost:8080/h2-console | H2 Database console |
- JDBC URL:
jdbc:h2:mem:testdb - Username:
sa - Password: (empty)
-
Single Source of Truth: State machine context is not persisted separately. The
BookEntity.statecolumn is the authoritative source. State machines are reconstructed on each request. -
Blocking Reactive Calls: We use
blockLast()on state machine events to ensure transitions complete before reading the new state. This avoids race conditions. -
Business Logic in Guards: State-checking guards are unnecessary (the framework handles this). Guards should validate business rules like borrowing limits.
-
Async Email: Book return emails are sent asynchronously via Spring's
@AsyncandApplicationEventPublisherto not block the API response.
- Add state to
BookStates.java - Add transitions in
StateMachineConfig.configure(transitions) - Add entry/exit actions if needed
- Update tests
- Update this README
- Create a
@Beanmethod returningGuard<BookStates, BookEvents> - Add it to the relevant transition with
.guard(yourGuard()) - Document the business rule
- Add tests
This project is for educational purposes, demonstrating Spring State Machine patterns.