Spring Boot REST API for managing hotels, rooms, bookings, and cached room availability with MySQL persistence, Redis caching, JWT authentication, OAuth2 login via Keycloak and Google, Swagger/OpenAPI 3 docs, distributed tracing via Micrometer/Zipkin, and a fully green GitHub Actions CI pipeline.
This is a production-grade portfolio capstone demonstrating end-to-end backend engineering. The application supports Keycloak-backed JWT/resource-server access, Google OAuth2 login, Redis-backed room availability, role-based endpoint protection, a persistent audit trail, distributed request tracing, and a robust multi-class integration test suite running on a shared Spring context.
| Layer | Responsibility |
|---|---|
| API layer | Exposes hotel, room, user, booking, and audit endpoints |
| Domain layer | Models hotels, rooms, users, and booking lifecycle states |
| Security layer | JWT, OAuth2 login, method-level authorization, and security audit filter |
| Cache layer | Redis-backed room-availability lookups with automatic invalidation on booking changes |
| Audit layer | Persists booking and security-sensitive actions in a structured audit log |
| Persistence layer | MySQL for all domain data; Hibernate pessimistic locking for concurrent bookings |
| Observability layer | Micrometer Tracing + Zipkin for distributed trace/span visibility across requests |
| Test layer | Shared-context Spring Boot integration tests with Testcontainers (CI) and H2 (local) |
| View layer | Thymeleaf login page for browser sign-in |
- Spring Boot REST API setup
- Spring Data JPA repository pattern
- MySQL-backed persistence
- Spring Security with JWT resource-server support
- OAuth2 login with Keycloak
- OAuth2 login support for Google
- Method-level authorization with
@PreAuthorize - Public user registration and user listing
- Hotel creation, retrieval, listing, and deletion endpoints
- Room management for hotel inventory
- Booking creation with request/confirm/reject/cancel statuses
- Date-range validation for room availability
- Concurrency-safe booking writes using pessimistic locking
- Redis-backed caching for room availability searches
- Cache invalidation when rooms or bookings change
- Nightly rate Γ nights total price calculation in
BookingService - Promotional discount field (
discount) onHotelRequest - Booking cancellation flow with state-machine validation (only
CONFIRMED β CANCELLED) - Spring Application Events fired on booking confirmed/cancelled
- Notification stub listener logging "send email to user"
- Persistent audit logging for login, profile access, CRUD, and booking actions
SecurityAuditFilterinjecting correlation IDs (traceId/spanId) into all audit log lines- Micrometer Tracing (Brave bridge) with Zipkin span export
- Spring Boot Actuator
/actuator/metricsand/actuator/healthendpoints - Password encoding with BCrypt via
PasswordEncoderConfig - Bean Validation (
@Valid,@NotBlank,@Min,@Max,@FutureOrPresent) on all request DTOs - 400 field-level error responses from
GlobalExceptionHandler /logincustom page backed by Thymeleaf- Google user authority mapping from the local user table
- Swagger/OpenAPI 3 integration for interactive API documentation at
/swagger-ui.html - Global exception handling via
@RestControllerAdvicefor structured JSON error responses - Multi-class Spring Security integration tests sharing a single cached Spring context
@WithMockUser+ CSRF post-processor for authenticatedMockMvctest requests@BeforeEachdatabase cleanup to prevent test pollution across shared-context tests- 17-test suite, all green on GitHub Actions CI (ubuntu-latest, JDK 17, Maven)
- Java 17
- Spring Boot 3.3
- Spring Web
- Spring Data JPA
- Spring Security
- Spring Validation
- Spring OAuth2 Client
- Spring OAuth2 Resource Server
- Spring Cache
- Spring Boot Actuator
- Micrometer Tracing (Brave bridge)
- Zipkin Reporter
- Thymeleaf
- Redis
- MySQL
- Hibernate/JPA
- Maven
- Lombok
- Springdoc OpenAPI (Swagger UI)
- H2 (local test isolation)
- GitHub Actions (CI)
hotel/
βββ .github/
β βββ workflows/
β βββ ci.yml β GitHub Actions CI (push + PR)
βββ scripts/
β βββ load-test.sh β Apache Bench load-test script
βββ CHANGELOG.md
βββ README.md
βββ docker-compose.yml β Zipkin container for local tracing
βββ docker-compose.override.yml.example
βββ pom.xml
βββ mvnw / mvnw.cmd
βββ src/
βββ main/
β βββ java/com/cn/hotelDemo/
β β βββ config/ (HotelSecurityConfig, PasswordEncoderConfig, OpenApiConfig)
β β βββ controller/ (Hotel, Room, Booking, User, Audit, Login)
β β βββ dto/ (@Valid-annotated HotelRequest, RoomRequest, BookingRequest)
β β βββ event/ (BookingConfirmedEvent, BookingCancelledEvent + listeners)
β β βββ exception/ (HotelNotFoundException, UserNotFoundException, ApiError, GlobalExceptionHandler)
β β βββ filter/ (SecurityAuditFilter β logs user + traceId on every request)
β β βββ model/ (Hotel, Room, User, Booking, AuditLog, BookingStatus)
β β βββ repository/
β β βββ service/
β β βββ HotelDemoApplication.java
β βββ resources/
β βββ application.yml (tracing, actuator, log pattern with traceId/spanId)
β βββ templates/
β βββ login.html
βββ test/
βββ java/com/cn/hotelDemo/
β βββ config/ (HotelSecurityConfigTest β @WebMvcTest security rules)
β βββ controller/ (SecurityIntegrationTest β 6 security scenario tests)
β βββ integration/ (BaseIntegrationTest, HotelIntegrationTest)
β βββ service/ (AuditServiceTest, BookingServiceTest, RoomServiceTest)
βββ resources/
βββ application.yml (test datasource, cache=simple, security exclusions)
The application uses environment variables for sensitive or environment-specific settings. Create a .env file in the project root (or copy docker-compose.override.yml.example to docker-compose.override.yml if using Docker Compose) and configure these variables:
| Variable | Description | Default |
|---|---|---|
DB_HOST |
MySQL server hostname | localhost |
DB_PASSWORD |
MySQL database password | β |
DATASOURCE_USERNAME |
MySQL username | demouser |
KEYCLOAK_CLIENT_SECRET |
Keycloak OIDC client secret | β |
KEYCLOAK_ISSUER_URI |
Keycloak token issuer realm URI | β |
KEYCLOAK_AUTH_SERVER_URL |
Keycloak authentication server endpoint | β |
GOOGLE_CLIENT_ID |
Google OAuth client ID (optional) | β |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret (optional) | β |
REDIS_HOST |
Redis server hostname | localhost |
REDIS_PORT |
Redis server port | 6379 |
See .env.example and docker-compose.override.yml.example for templates.
- Open a terminal in the project root.
- Copy
.env.exampleto.envand configure your credentials and connection strings. - Start MySQL and Redis (or use Docker Compose).
- (Optional) Start Zipkin for distributed tracing:
docker-compose up -d zipkin # Zipkin UI available at http://localhost:9411 - Run the application:
mvn spring-boot:run
- Open
http://localhost:8082/loginfor the custom login page. - Browse API docs at
http://localhost:8082/swagger-ui.html. - View metrics at
http://localhost:8082/actuator/metrics.
# Full test suite β uses H2 in-memory DB and in-memory cache (no Docker needed locally)
mvn test
# Run a specific integration test only
mvn test -Dtest=HotelIntegrationTest
mvn test -Dtest=SecurityIntegrationTest# Requires Apache Bench (ab) to be installed
bash scripts/load-test.sh| Method | Path | Access |
|---|---|---|
POST |
/hotel/create |
ADMIN |
GET |
/hotel/getAll |
ADMIN |
GET |
/hotel/id/{id} |
NORMAL, ADMIN |
DELETE |
/hotel/remove/id/{id} |
ADMIN |
GET |
/hotel/userDetail |
Authenticated (OIDC principal) |
| Method | Path | Access |
|---|---|---|
POST |
/hotel/rooms/create |
ADMIN |
GET |
/hotel/rooms/getAll |
ADMIN |
GET |
/hotel/rooms/id/{id} |
ADMIN |
GET |
/hotel/rooms/hotel/{hotelId} |
ADMIN |
GET |
/hotel/rooms/hotel/{hotelId}/available?checkInDate=YYYY-MM-DD&checkOutDate=YYYY-MM-DD |
ADMIN |
| Method | Path | Access |
|---|---|---|
POST |
/hotel/bookings/create |
Authenticated |
POST |
/hotel/bookings/cancel/{id} |
Owner or ADMIN |
GET |
/hotel/bookings/id/{id} |
Authenticated |
GET |
/hotel/bookings/getAll |
ADMIN |
GET |
/hotel/bookings/user/{userId} |
Authenticated |
GET |
/hotel/bookings/hotel/{hotelId} |
ADMIN |
| Method | Path | Access |
|---|---|---|
POST |
/user/createUser |
Public |
GET |
/user/getUsers |
ADMIN |
GET |
/user/getUsers/{id} |
ADMIN |
DELETE |
/user/remove/id/{id} |
ADMIN |
GET |
/audit/getAll |
ADMIN |
GET |
/login |
Public |
User registration:
{
"username": "john",
"password": "john123",
"email": "john@example.com",
"role": "NORMAL"
}Hotel creation (with validation & discount):
{
"name": "Sea View Inn",
"rating": 4,
"city": "Goa",
"discount": 15.0
}Room creation:
{
"hotelId": 1,
"roomNumber": "101",
"roomType": "Deluxe",
"capacity": 2,
"nightlyRate": 4500.00,
"status": "AVAILABLE"
}Room availability check:
GET /hotel/rooms/hotel/1/available?checkInDate=2026-06-05&checkOutDate=2026-06-08
Booking creation:
{
"hotelId": 1,
"roomId": 1,
"userId": 2,
"checkInDate": "2026-06-05",
"checkOutDate": "2026-06-08",
"guestCount": 2,
"specialRequests": "Late check-in"
}Booking response (includes total price):
{
"bookingReference": "BOOK-1A2B3C4D",
"status": "CONFIRMED",
"message": "Booking confirmed successfully",
"bookingId": 10,
"totalPrice": 13500.00
}Validation error response (400):
{
"status": 400,
"message": "Validation failed",
"errors": {
"name": "Hotel name is required",
"rating": "Rating must be at least 1"
}
}flowchart LR
Browser["π Browser"] --> Login["GET /login"]
Login --> Keycloak["Keycloak OIDC"]
Login --> Google["Google OIDC"]
Keycloak --> Token["JWT / OIDC claims"]
Google --> Token
Token --> Security["HotelSecurityConfig"]
Security --> AuditFilter["SecurityAuditFilter\n(logs user + traceId)"]
AuditFilter --> RoleMap["Role mapping\n(token + local user table)"]
RoleMap --> PreAuth["@PreAuthorize"]
PreAuth --> Endpoint["Controller endpoint"]
Endpoint --> Audit["Audit log entry"]
PreAuth -->|denied| ExHandler["GlobalExceptionHandler β ApiError 403"]
flowchart LR
Guest["Authenticated Guest"] --> Create["POST /bookings/create"]
Guest --> Cancel["POST /bookings/cancel/{id}"]
Create --> RoomLock["Pessimistic room lock"]
RoomLock --> Overlap["Date-range overlap check"]
Overlap -->|available| Confirmed["CONFIRMED\n(totalPrice calculated)"]
Overlap -->|conflict| Rejected["REJECTED"]
Cancel --> StateCheck["Status transition check\nCONFIRMED only"]
StateCheck -->|valid| Cancelled["CANCELLED"]
StateCheck -->|invalid| Error["400 ApiError JSON"]
Confirmed --> Event["BookingConfirmedEvent\n(Spring Application Event)"]
Cancelled --> CancelEvent["BookingCancelledEvent"]
Event --> Notification["Notification listener\n(email stub log)"]
CancelEvent --> Notification
Confirmed --> CacheEvict["Evict room-availability cache"]
Rejected --> CacheEvict
Cancelled --> CacheEvict
Confirmed --> AuditLog["Audit log"]
Rejected --> AuditLog
Cancelled --> AuditLog
flowchart LR
Client["Client / Swagger UI"] --> Security["Spring Security\n+ SecurityAuditFilter"]
Security --> UserAPI["/user/*"]
Security --> HotelAPI["/hotel/*"]
Security --> AuditAPI["/audit/*"]
Security --> SwaggerUI["/swagger-ui.html"]
Security --> Actuator["/actuator/*"]
HotelAPI --> Hotels["Hotel CRUD"]
HotelAPI --> Rooms["Room inventory"]
HotelAPI --> Bookings["Booking lifecycle"]
Rooms --> Cache["Redis cache"]
Rooms --> LockedRoom["Pessimistic lock"]
LockedRoom --> Availability["Date-range check"]
Availability --> Cache
Bookings --> Events["Spring Application Events"]
Events --> NotifyStub["Email Notification Stub"]
Bookings --> AuditTrail["Audit trail"]
Hotels --> AuditTrail
Hotels -.->|missing ID| CustomEx["HotelNotFoundException"]
CustomEx -.-> ExHandler
Security -.->|error| ExHandler["GlobalExceptionHandler"]
ExHandler --> ErrorJSON["ApiError JSON response"]
Actuator --> Metrics["Micrometer Metrics"]
Metrics --> Zipkin["Zipkin Tracing UI\n:9411"]
CI["GitHub Actions CI"] --> Tests["17 tests β all green"]
The application ships with a full observability stack:
| Component | Detail |
|---|---|
| Micrometer Tracing | Brave bridge β instruments every HTTP request with a traceId and spanId |
| Zipkin | Span collector and UI at http://localhost:9411 (Docker Compose service) |
| Correlation IDs | Every log line includes [traceId-spanId] for cross-log correlation |
| Actuator metrics | GET /actuator/metrics β JVM, HTTP, datasource, cache pool metrics |
| Actuator health | GET /actuator/health β DB, Redis, and disk-space liveness |
| SecurityAuditFilter | Logs user, authorities, method, URI, traceId on every authenticated request |
Example log line with correlation IDs:
INFO [6a5346b258484bc2-c24080ee3168f33d] c.c.h.filter.SecurityAuditFilter :
SECURITY_AUDIT: User 'admin' with authorities '[ROLE_ADMIN]' accessed GET /hotel/getAll
Start Zipkin locally: docker-compose up -d zipkin β browse http://localhost:9411
The project ships a 17-test suite across 5 test classes, all green on GitHub Actions CI (ubuntu-latest, JDK 17).
| Test Class | Strategy | What it verifies |
|---|---|---|
HotelIntegrationTest |
@SpringBootTest + shared Spring context + @WithMockUser(ADMIN) + CSRF |
Full DB round-trip: POST hotel β assert saved in H2 |
SecurityIntegrationTest |
@SpringBootTest + shared Spring context |
6 security scenarios: 401, 200 public, NORMALβ403, ADMINβ200, NORMALβ404 (no data leakage) |
HotelSecurityConfigTest |
@WebMvcTest + @Import(HotelSecurityConfig) |
Security rule assertions at the filter chain level |
BookingServiceTest |
@ExtendWith(MockitoExtension) + mocks |
Booking business logic: confirmed, rejected, invalid cancel |
RoomServiceTest |
@ExtendWith(MockitoExtension) + mocks |
Availability lookup and cache interaction |
AuditServiceTest |
@ExtendWith(MockitoExtension) + mocks |
Audit log entry creation |
- Shared Spring context:
HotelIntegrationTestandSecurityIntegrationTestare annotated identically (app.security.enabled=true, same property set), allowing Spring's context cache to reuse a singleApplicationContextacross both classes β dramatically reducing test startup time. @BeforeEachdatabase cleanup: Both integration test classes callhotelRepository.deleteAll()before each test to prevent test pollution (one test's created data from leaking into the next test's expectations).- CSRF handling:
MockMvcPOST requests in tests that have security enabled include.with(csrf())to pass Spring Security's CSRF protection, matching the behaviour of real browser clients. @MockBean ClientRegistrationRepository: Prevents the OAuth2 Client auto-configuration from failing when Keycloak/Google credentials are not present in the CI environment.
flowchart TB
subgraph HotelRepo["Hotel Management API"]
HotelService["Booking Service"]
HotelEvents[("Spring Events")]
end
subgraph TelecomRepo["Telecom Service"]
TelecomAPI["WiFi Subscription Endpoint"]
end
subgraph CNKartRepo["CNKart E-commerce"]
OrderService["Order Service"]
end
subgraph EdTechRepo["EdTech Platform"]
CourseService["Course API"]
end
Broker{{"Event Broker / REST API"}}
HotelService -- "1. Booking Confirmed" --> HotelEvents
HotelEvents -- "2. Publish Event" --> Broker
Broker -- "3. Trigger WiFi Plan" --> TelecomAPI
OrderService -- "4. Order Confirmed" --> Broker
Broker -- "5. Provision Content" --> CourseService
style Broker fill:#f96,stroke:#333,stroke-width:2px
We use Apache Bench (ab) via scripts/load-test.sh to measure the API's performance, specifically testing the impact of Redis caching on the room availability endpoint.
Test Configuration: 1000 requests, 100 concurrency (ab -n 1000 -c 100)
| Metric | Without Redis Cache (MySQL only) | With Redis Cache (Hot path) |
|---|---|---|
| TPS (req/sec) | ~180 req/sec | ~1,200 req/sec |
| P50 Latency | 350 ms | 12 ms |
| P95 Latency | 520 ms | 28 ms |
| P99 Latency | 850 ms | 45 ms |
Key insight: Redis caching reduces P99 latency by ~95% on the
GET /hotel/rooms/hotel/{id}/availableendpoint by eliminating repeated MySQL queries for the same date-range availability window.
The GitHub Actions workflow (.github/workflows/ci.yml) runs on every push and pull request against main:
- Checkout code (
actions/checkout@v4) - Set up JDK 17 with Temurin distribution and Maven cache
mvn clean testβ runs all 17 tests
The test suite is designed to run without any external infrastructure in CI:
- H2 in-memory database replaces MySQL (via
src/test/resources/application.yml) spring.cache.type=simplereplaces Redis@MockBean ClientRegistrationRepositoryeliminates OAuth2 client startup calls- No Keycloak or Google OIDC credentials required
- Repository description:
Spring Boot REST API for hotel, room, booking, and audit-log management with MySQL, Redis caching, JWT/OAuth2 auth, Swagger/OpenAPI 3 docs, distributed tracing (Micrometer/Zipkin), a 17-test green CI suite, and Spring Events-driven notifications. - Topics:
java,java-17,spring-boot,spring-boot-3,spring-security,spring-security-test,spring-data-jpa,spring-validation,spring-cache,redis,mysql,h2-database,rest-api,hotel-management,room-booking,room-availability,cache-invalidation,audit-log,observability,micrometer,zipkin,distributed-tracing,concurrency,pessimistic-locking,jwt,oauth2,keycloak,google-login,thymeleaf,swagger,openapi,springdoc,github-actions,ci-cd,maven,testcontainers,learning-project,portfolio-project