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.