Live: [https://your-app.railway.app/swagger-ui.html](link goes here after deployment)
Swagger UI is the interactive API demo — no separate frontend needed.
A production-grade Spring Boot REST API for managing hotels, rooms, and bookings. The application features a rich approval-based booking workflow, JWT/OAuth2 security, Redis caching, rate limiting via Bucket4j, async email notifications, custom Micrometer metrics, AOP-driven audit logging, WebSocket/STOMP push notifications, paginated endpoints, Swagger/OpenAPI 3 docs, distributed tracing via Micrometer/Zipkin, compile-time MapStruct response DTO mapping, RFC 9457 Problem Details error handling, validation groups, Hibernate 6 soft delete with audit recovery endpoints, and a fully green GitHub Actions CI pipeline with JaCoCo test coverage enforcement backed by Testcontainers MySQL.
These improvements were added across 15 phased commits on top of the original baseline.
| Phase | Feature | Impact |
|---|---|---|
| 1 | Lombok annotations on all entities | Eliminated ~40% of entity boilerplate; added @Builder, @EqualsAndHashCode(of="id"), safe @ToString |
| 2 | Hotel entity enrichment | Added address, state, country, phoneNumber, email, website, description, totalRooms, starCategory, createdAt, updatedAt with @PrePersist/@PreUpdate |
| 3 | Booking lifecycle upgrade | 4-state machine (REQUESTED → CONFIRMED / REJECTED → CANCELLED), rejectionReason field, dedicated confirm/reject PATCH endpoints |
| 4 | Async email notifications | @Async NotificationListener — decoupled from booking transaction, non-blocking |
| 5 | Bucket4j rate limiting | Per-user/IP limits: 20 bookings/min · 5 registrations/hr · 60 availability searches/min |
| 6 | Pagination everywhere | All list endpoints accept page, size, sortBy, direction query params |
| 7 | Custom Micrometer metrics | BookingMetrics — counters for created/confirmed/rejected/cancelled; gauge for active bookings |
| 8 | AOP audit logging | @AuditLogged annotation + AuditAspect with SpEL resource-ID extraction |
| 9 | End-to-end test suite | EndToEndWorkflowIntegrationTest covering the full lifecycle on real MySQL via Testcontainers |
| 10 | CI hardening | Fixed H2↔Testcontainers driver clash; all tests green on GitHub Actions |
| 11 | Response DTOs & MapStruct | Eliminated JPA entity leakage, circular JSON dependencies, and password hash exposure across all controllers |
| 12 | JaCoCo coverage gate & CI | Enforced 50% line coverage quality gate via jacoco-maven-plugin with automated XML/HTML report artifact upload in CI |
| 13 | RFC 9457 Problem Details | Standardized all error responses to application/problem+json with errorCode, timestamp, traceId, and invalid fields map |
| 14 | Validation Groups | Applied OnCreate vs OnUpdate Bean Validation groups to distinguish mandatory creation vs partial update fields |
| 15 | Soft Delete & Recovery | Added @SQLRestriction("deleted = false"), deleted/deletedAt on Hotel & Room, safe deletion preserving booking history, plus GET /hotel/deleted and PATCH /hotel/restore/{id} |
flowchart TB
subgraph Client Layer
Swagger["🌐 Swagger UI / REST Client"]
WS["🔔 ws-test.html (WebSocket)"]
end
subgraph Security Layer
RateLimit["RateLimitingFilter\n(Bucket4j — per user/IP)"]
AuditFilter["SecurityAuditFilter\n(traceId + user on every req)"]
OAuth["HotelSecurityConfig\n(JWT + OAuth2 + @PreAuthorize)"]
end
subgraph API & Error Layer
Hotel["HotelController"]
Room["RoomController"]
Booking["BookingController"]
User["UserController"]
Audit["AuditController"]
GlobalErrors["GlobalExceptionHandler\n(RFC 9457 ProblemDetail)"]
end
subgraph Mapping Layer
Mappers["MapStruct Mappers\n(Type-Safe Bean Mapping)"]
DTOs["Response DTOs\n(Hotel, Room, User, Booking)"]
end
subgraph Service Layer
BS["BookingService\n(state machine + price calc)"]
HS["HotelService"]
RS["RoomService"]
US["UserService"]
AS["AuditService"]
NS["NotificationService\n(@Async)"]
end
subgraph Cross-Cutting
AOP["AuditAspect\n(@AuditLogged + SpEL)"]
Metrics["BookingMetrics\n(Micrometer counters)"]
Events["Spring Application Events"]
end
subgraph Persistence Layer
MySQL[("MySQL\n(JPA / Hibernate)")]
Redis[("Redis\n(room availability cache)")]
end
subgraph Observability
Actuator["Spring Boot Actuator"]
Zipkin["Zipkin :9411"]
Prometheus["Prometheus-ready metrics"]
end
Swagger --> RateLimit --> AuditFilter --> OAuth
WS --> Booking
OAuth --> Hotel & Room & Booking & User & Audit
Hotel & Room & Booking & User -.-> GlobalErrors
Hotel & Room & Booking & User --> Mappers --> DTOs
Hotel --> HS --> MySQL
Room --> RS --> Redis & MySQL
Booking --> BS --> MySQL
BS --> Events --> NS
BS --> Metrics
BS -.-> AOP
Actuator --> Prometheus --> Zipkin
stateDiagram-v2
[*] --> REQUESTED : POST /bookings/create
REQUESTED --> CONFIRMED : PATCH /{id}/confirm (Admin)
REQUESTED --> REJECTED : PATCH /{id}/reject (Admin + reason)
REQUESTED --> CANCELLED : POST /cancel/{id} (Owner or Admin)
CONFIRMED --> CANCELLED : POST /cancel/{id} (Owner or Admin)
REJECTED --> [*]
CANCELLED --> [*]
CONFIRMED : CONFIRMED\n● Sends WebSocket push\n● Fires BookingConfirmedEvent\n● Evicts Redis cache
REJECTED : REJECTED\n● Stores rejectionReason\n● Sends WebSocket push
CANCELLED : CANCELLED\n● Fires BookingCancelledEvent\n● Evicts Redis cache
| Layer | Component | Responsibility |
|---|---|---|
| Filter | RateLimitingFilter |
Per-user/IP rate limits via Bucket4j (before security) |
| Filter | SecurityAuditFilter |
Logs user, traceId, method, URI on every authenticated request |
| Security | HotelSecurityConfig |
JWT resource-server, OAuth2 login (Keycloak + Google), @PreAuthorize |
| API | Controllers | REST endpoints; validate inputs; delegate to services; map entities to response DTOs; annotated with @AuditLogged |
| Mapping | MapStruct Mappers (*Mapper) |
Compile-time type-safe conversion of domain entities to DTOs; prevents password exposure & circular references |
| Error Handling | GlobalExceptionHandler |
Translates domain exceptions into RFC 9457 application/problem+json standard payloads with errorCode and traceId |
| Validation | Jakarta Bean Validation | Validation Groups (OnCreate, OnUpdate) for context-sensitive payload constraints |
| AOP | AuditAspect |
Intercepts @AuditLogged methods, evaluates SpEL, writes audit log entry |
| Service | BookingService |
Booking state machine, pessimistic room locking, price calculation, metrics |
| Service | NotificationService |
@Async email notifications on booking lifecycle events |
| Metrics | BookingMetrics |
Micrometer counters + gauge for booking lifecycle events |
| Cache | Redis | Room-availability lookups; @CacheEvict on every booking change |
| Persistence | MySQL + Hibernate | All domain data; pessimistic locking on room rows during booking |
| Observability | Micrometer + Zipkin | Distributed traces; custom booking counters; Actuator health/metrics |
| Events | Spring App Events | BookingConfirmedEvent, BookingCancelledEvent → async notification listener |
| WebSocket | STOMP/SockJS | Real-time push to /topic/bookings/{hotelId} on booking state changes |
| Quality | JaCoCo Quality Gate | Enforces 50% line coverage gate via jacoco-maven-plugin during Maven verification |
| Test | Testcontainers + MySQL | Real-DB integration tests in CI; EndToEndWorkflowIntegrationTest |
| Category | Technology |
|---|---|
| Runtime | Java 17, Spring Boot 3.3.5 |
| Web | Spring Web MVC, Spring WebSocket, STOMP, SockJS |
| Security | Spring Security, OAuth2 Resource Server, OAuth2 Client (Keycloak, Google) |
| DTO Mapping | MapStruct 1.5.5.Final, Lombok-MapStruct-Binding |
| Error Contract | RFC 9457 Problem Details (ProblemDetail, application/problem+json) |
| Validation | Jakarta Bean Validation (Hibernate Validator) with Validation Groups (OnCreate, OnUpdate) |
| Data | Spring Data JPA, Hibernate, MySQL 8, H2 (test) |
| Cache | Spring Cache, Redis |
| Rate Limiting | Bucket4j |
| Async | Spring @Async, @EnableAsync |
| Observability | Micrometer Tracing (Brave), Zipkin Reporter, Spring Boot Actuator |
| Metrics | Micrometer (custom counters + gauges via MeterRegistry) |
| AOP | Spring AOP (@Aspect, @AfterReturning), SpEL |
| Documentation | Springdoc OpenAPI 3 (Swagger UI) |
| Build | Maven, Maven Wrapper |
| Code Gen | Lombok (@Data, @Builder, @Slf4j, etc.) |
| Coverage | JaCoCo 0.8.12 (jacoco-maven-plugin with 50% line coverage enforcement) |
| Testing | JUnit 5, Mockito, Testcontainers (MySQL), @SpringBootTest, @WebMvcTest |
| CI | GitHub Actions (ubuntu-latest, JDK 17 Temurin, JaCoCo artifact upload) |
| Templates | Thymeleaf (login page), static HTML (ws-test.html) |
hotel-management-api/
├── .github/
│ └── workflows/
│ └── ci.yml ← GitHub Actions CI (Maven test + JaCoCo artifact upload)
├── scripts/
│ └── load-test.sh ← Apache Bench load-test script
├── CHANGELOG.md
├── README.md
├── docker-compose.yml ← Zipkin + Redis local services
├── pom.xml
├── mvnw / mvnw.cmd
└── src/
├── main/
│ ├── java/com/cn/hotelDemo/
│ │ ├── annotation/ ← @AuditLogged (custom annotation)
│ │ ├── aspect/ ← AuditAspect (AOP + SpEL evaluation)
│ │ ├── config/ ← HotelSecurityConfig, PasswordEncoderConfig, OpenApiConfig, AsyncConfig
│ │ ├── controller/ ← Hotel, Room, Booking, User, Audit, Login (return DTOs)
│ │ ├── dto/ ← Requests, Responses (HotelResponse, RoomResponse, UserResponse, BookingResponse, BookingDetailResponse)
│ │ │ └── validation/ ← OnCreate, OnUpdate (Jakarta Bean Validation groups)
│ │ ├── event/ ← BookingConfirmedEvent, BookingCancelledEvent, NotificationListener (@Async)
│ │ ├── exception/ ← ErrorCode enum, GlobalExceptionHandler (RFC 9457 ProblemDetail), custom exceptions
│ │ ├── filter/ ← RateLimitingFilter (Bucket4j), SecurityAuditFilter
│ │ ├── mapper/ ← HotelMapper, RoomMapper, UserMapper, BookingMapper (MapStruct)
│ │ ├── metrics/ ← BookingMetrics (Micrometer counters + active-booking gauge)
│ │ ├── model/ ← Hotel, Room, User, Booking, AuditLog, BookingStatus, RoomStatus, RoomType
│ │ ├── repository/ ← JPA repositories (incl. pageable variants + pessimistic lock query)
│ │ ├── service/ ← BookingService, HotelService, RoomService, UserService, AuditService, NotificationService
│ │ └── HotelDemoApplication.java
│ └── resources/
│ ├── application.yml ← tracing, actuator, log pattern, async, cache config
│ ├── static/
│ │ └── ws-test.html ← real-time booking notification demo page
│ └── templates/
│ └── login.html
└── test/
├── java/com/cn/hotelDemo/
│ ├── config/ ← HotelSecurityConfigTest (@WebMvcTest security rules)
│ ├── controller/ ← HotelControllerTest, RoomControllerTest, BookingControllerTest, UserControllerTest, SecurityIntegrationTest
│ ├── exception/ ← GlobalExceptionHandlerTest (RFC 9457 error contracts)
│ ├── integration/ ← BaseIntegrationTest, HotelIntegrationTest, BookingFlowIntegrationTest, EndToEndWorkflowIntegrationTest, SoftDeleteIntegrationTest
│ ├── mapper/ ← HotelMapperTest, RoomMapperTest, UserMapperTest, BookingMapperTest
│ ├── service/ ← AuditServiceTest, BookingServiceTest, RoomServiceTest, UserServiceTest
│ └── validation/ ← HotelRequestValidationTest (groups verification)
└── resources/
└── application.yml ← test datasource, cache=simple, security exclusions
erDiagram
HOTEL {
Long id PK
String name
Long rating
String city
Double discount
String address
String state
String country
String phoneNumber
String email
String website
String description
Integer totalRooms
String starCategory
Boolean deleted
LocalDateTime deletedAt
LocalDateTime createdAt
LocalDateTime updatedAt
}
ROOM {
Long id PK
String roomNumber
RoomType roomType
Integer capacity
BigDecimal nightlyRate
RoomStatus status
Boolean deleted
LocalDateTime deletedAt
}
BOOKING {
Long id PK
String bookingReference
LocalDate checkInDate
LocalDate checkOutDate
Integer guestCount
String specialRequests
BigDecimal totalPrice
BookingStatus status
String rejectionReason
}
USER {
Long id PK
String username
String password
String email
String role
}
AUDIT_LOG {
Long id PK
String action
String username
String resourceType
String resourceId
String outcome
String detail
LocalDateTime timestamp
}
HOTEL ||--o{ ROOM : "has"
HOTEL ||--o{ BOOKING : "receives"
ROOM ||--o{ BOOKING : "booked via"
USER ||--o{ BOOKING : "makes"
flowchart LR
Browser["🌐 Browser / API Client"] --> RateLimitF["RateLimitingFilter\n(Bucket4j)"]
RateLimitF -->|within limit| Login["GET /login"]
RateLimitF -->|exceeded| HTTP429["429 Too Many Requests"]
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 --> AOP["@AuditLogged\n(AuditAspect → AuditService)"]
PreAuth -->|denied| ExHandler["GlobalExceptionHandler → ApiError 403"]
flowchart TD
Guest["Authenticated User"] --> Create["POST /bookings/create"]
Admin["Admin"] --> Confirm["PATCH /{id}/confirm"]
Admin --> Reject["PATCH /{id}/reject + reason"]
UserOrAdmin["User or Admin"] --> Cancel["POST /cancel/{id}"]
Create --> RoomLock["Pessimistic room lock\n(SELECT ... FOR UPDATE)"]
RoomLock --> DateCheck["Date-range overlap check"]
DateCheck -->|no overlap| Requested["REQUESTED\n(totalPrice calculated)"]
DateCheck -->|conflict| AutoRej["REJECTED\n(overlap detected)"]
RoomLock -->|unavailable room| AutoRej2["REJECTED\n(room not AVAILABLE)"]
Requested --> Confirm
Requested --> Reject
Requested --> Cancel
Confirm --> Confirmed["CONFIRMED"]
Reject --> Rejected["REJECTED\n(+ rejectionReason stored)"]
Cancel -->|from REQUESTED| Cancelled["CANCELLED"]
Confirmed --> Cancel
Cancel -->|from CONFIRMED| Cancelled
Confirmed --> WsPush["📡 WebSocket push\n/topic/bookings/{hotelId}"]
Confirmed --> Event["BookingConfirmedEvent"]
Rejected --> WsPush
Cancelled --> Event2["BookingCancelledEvent"]
AutoRej --> WsPush
Event --> AsyncEmail["📧 Async Email Notification\n(@Async NotificationListener)"]
Event2 --> AsyncEmail
Confirmed --> CacheEvict["🗑 Evict Redis cache\n(roomAvailability)"]
Cancelled --> CacheEvict
Rejected --> CacheEvict
Confirmed --> Metrics["📊 BookingMetrics\nincrementConfirmed()"]
Cancelled --> MetricsC["📊 BookingMetrics\nincrementCancelled()"]
Rejected --> MetricsR["📊 BookingMetrics\nincrementRejected()"]
The RateLimitingFilter runs before Spring Security (@Order(HIGHEST_PRECEDENCE + 1)) and enforces per-user/IP token-bucket limits via Bucket4j.
| Endpoint | Identifier | Limit | Window |
|---|---|---|---|
POST /hotel/bookings/create |
Authenticated username or IP | 20 requests | per minute |
POST /user/createUser |
Remote IP | 5 requests | per hour |
GET /hotel/rooms/hotel/{id}/available |
Authenticated username or IP | 60 requests | per minute |
Response on breach: HTTP 429 Too Many Requests with JSON body:
{ "error": "Too many booking requests. Limit: 20 per minute." }Client resolution priority: Authenticated username → X-Forwarded-For header → Remote IP.
The BookingMetrics component registers the following meters with the Prometheus-compatible MeterRegistry:
| Metric Name | Type | Description |
|---|---|---|
hotel.bookings.created |
Counter | Total bookings ever created |
hotel.bookings.confirmed |
Counter | Total bookings confirmed by admin |
hotel.bookings.rejected |
Counter | Total bookings rejected (auto + manual) |
hotel.bookings.cancelled |
Counter | Total bookings cancelled |
hotel.bookings.active |
Gauge | Currently active (non-terminal) bookings |
Query via Actuator:
GET /actuator/metrics/hotel.bookings.confirmed
GET /actuator/metrics/hotel.bookings.activeControllers are annotated with @AuditLogged, which triggers AuditAspect after a successful method return. SpEL expressions extract resource IDs from arguments or return values at runtime.
// Example annotation usage
@AuditLogged(
action = "#result != null ? 'BOOKING_' + #result.body.status.name() : 'UNKNOWN'",
resourceType = "BOOKING",
resourceIdSpel = "#result.body.bookingId"
)Flow:
- Controller method returns successfully
@AfterReturningadvice fires inAuditAspect- SpEL is evaluated against method args + return value
AuditService.record(action, username, resourceType, resourceId, "SUCCESS", ...)persists the log
Booking lifecycle events are published via Spring's ApplicationEventPublisher. The NotificationListener processes them on a separate thread pool using @Async, completely decoupled from the booking transaction.
sequenceDiagram
participant BS as BookingService
participant EP as ApplicationEventPublisher
participant NL as NotificationListener (@Async)
participant Email as Email Provider (stub)
BS->>EP: publishEvent(BookingConfirmedEvent)
EP-->>BS: (returns immediately)
Note over BS: Transaction commits
EP->>NL: onBookingConfirmed() [async thread]
NL->>Email: send email to user
All collection endpoints support standard Spring Data pagination parameters:
| Parameter | Default | Description |
|---|---|---|
page |
0 |
Zero-based page index |
size |
20 |
Items per page |
sortBy |
id |
Field to sort by |
direction |
asc |
asc or desc |
Example:
GET /hotel/bookings/getAll?page=0&size=10&sortBy=checkInDate&direction=desc
GET /hotel/bookings/user/1?page=1&size=5
GET /hotel/bookings/hotel/2?page=0&size=20&sortBy=totalPrice&direction=descResponse is a Spring Page<T> with content, totalElements, totalPages, number, size fields.
Create a .env file in the project root and configure these variables:
| Variable | Description | Default |
|---|---|---|
DB_URL |
Full MySQL JDBC connection URL | — |
DB_USERNAME |
MySQL username | demouser |
DB_PASSWORD |
MySQL database password | — |
KEYCLOAK_CLIENT_ID |
Keycloak/OAuth client ID | hotelDemoApplication |
KEYCLOAK_CLIENT_SECRET |
Keycloak OIDC client secret | — |
JWT_ISSUER_URI |
Keycloak/JWT issuer URI used by Spring Security | — |
KEYCLOAK_AUTH_SERVER_URL |
Keycloak authentication server endpoint | — |
GOOGLE_CLIENT_ID |
Google OAuth client ID | — |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret | — |
REDIS_HOST |
Redis server hostname | localhost |
REDIS_PORT |
Redis server port | 6379 |
See .env.example and docker-compose.override.yml.example for templates.
- Clone the repository and open a terminal at the project root.
- Copy
.env.exampleto.envand configure your credentials. - Start MySQL and Redis (or use Docker Compose):
docker-compose up -d redis zipkin
- 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. - Open
http://localhost:8082/ws-test.htmlto subscribe to real-time booking notifications. - (Optional) View distributed traces at
http://localhost:9411.
# Full test suite — uses Testcontainers MySQL in CI, H2 locally (no Docker needed for local)
mvn test
# Run a specific test class
mvn test -Dtest=EndToEndWorkflowIntegrationTest
mvn test -Dtest=BookingFlowIntegrationTest
mvn test -Dtest=SecurityIntegrationTest# Requires Apache Bench (ab)
bash scripts/load-test.sh| Method | Path | Access | New in Upgrade |
|---|---|---|---|
POST |
/hotel/create |
ADMIN | — |
GET |
/hotel/getAll |
ADMIN | — |
GET |
/hotel/id/{id} |
NORMAL, ADMIN | — |
PUT |
/hotel/update/{id} |
ADMIN | ✅ Hotel update endpoint |
DELETE |
/hotel/remove/id/{id} |
ADMIN | ✅ Soft-deletes hotel & rooms (preserves booking history) |
GET |
/hotel/deleted |
ADMIN | ✅ List soft-deleted hotels for audit/recovery |
PATCH |
/hotel/restore/{id} |
ADMIN | ✅ Restore soft-deleted hotel & rooms |
GET |
/hotel/search?city=&name= |
ADMIN | ✅ Search by city/name |
GET |
/hotel/userDetail |
Authenticated (OIDC) | — |
| Method | Path | Access | New in Upgrade |
|---|---|---|---|
POST |
/hotel/rooms/create |
ADMIN | — |
GET |
/hotel/rooms/getAll |
ADMIN | ✅ Paginated |
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 | ✅ Redis-cached |
DELETE |
/hotel/rooms/remove/id/{id} |
ADMIN | ✅ Soft-deletes room |
| Method | Path | Access | New in Upgrade |
|---|---|---|---|
POST |
/hotel/bookings/create |
Authenticated | — |
GET |
/hotel/bookings/id/{id} |
Authenticated | — |
GET |
/hotel/bookings/getAll?page=&size=&sortBy=&direction= |
ADMIN | ✅ Paginated |
GET |
/hotel/bookings/user/{userId}?page=&size= |
Authenticated | ✅ Paginated |
GET |
/hotel/bookings/hotel/{hotelId}?page=&size= |
ADMIN | ✅ Paginated |
PATCH |
/hotel/bookings/{id}/confirm |
ADMIN | ✅ Approval workflow |
PATCH |
/hotel/bookings/{id}/reject |
ADMIN | ✅ Rejection with reason |
POST |
/hotel/bookings/cancel/{id} |
Owner or ADMIN | — |
| Method | Path | Access |
|---|---|---|
POST |
/user/createUser |
Public (rate-limited: 5/hr) |
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 enriched metadata):
{
"name": "Sea View Inn",
"rating": 4,
"city": "Goa",
"discount": 15.0,
"address": "Beach Road, Calangute",
"state": "Goa",
"country": "India",
"phoneNumber": "+91-9876543210",
"email": "info@seaviewinn.com",
"website": "https://seaviewinn.com",
"description": "Luxury beachfront hotel with stunning ocean views.",
"totalRooms": 50,
"starCategory": "4-Star"
}Room creation:
{
"hotelId": 1,
"roomNumber": "101",
"roomType": "DELUXE",
"capacity": 2,
"nightlyRate": 4500.00,
"status": "AVAILABLE"
}Booking creation (starts in REQUESTED state):
{
"hotelId": 1,
"roomId": 1,
"userId": 2,
"checkInDate": "2026-06-05",
"checkOutDate": "2026-06-08",
"guestCount": 2,
"specialRequests": "Late check-in, king bed preferred"
}Booking creation response (REQUESTED):
{
"bookingReference": "BOOK-1A2B3C4D",
"status": "REQUESTED",
"message": "Booking requested, awaiting confirmation",
"bookingId": 10,
"totalPrice": 11475.00
}Admin confirms booking — PATCH /hotel/bookings/10/confirm:
{
"bookingReference": "BOOK-1A2B3C4D",
"status": "CONFIRMED",
"message": "Booking confirmed by admin",
"bookingId": 10,
"totalPrice": 11475.00
}Admin rejects booking — PATCH /hotel/bookings/10/reject:
{ "reason": "Room under maintenance during requested dates." }Rejection response:
{
"bookingReference": "BOOK-1A2B3C4D",
"status": "REJECTED",
"message": "Booking rejected: Room under maintenance during requested dates.",
"bookingId": 10,
"totalPrice": null
}User retrieval response (GET /user/getUsers/2 — password hash omitted):
{
"id": 2,
"username": "john",
"email": "john@example.com",
"role": "NORMAL"
}Hotel response (GET /hotel/id/1 via HotelResponse DTO):
{
"id": 1,
"name": "Sea View Inn",
"rating": 4,
"city": "Goa",
"discount": 15.0,
"address": "Beach Road, Calangute",
"state": "Goa",
"country": "India",
"phoneNumber": "+91-9876543210",
"email": "info@seaviewinn.com",
"website": "https://seaviewinn.com",
"description": "Luxury beachfront hotel with stunning ocean views.",
"totalRooms": 50,
"starCategory": "4-Star",
"createdAt": "2026-06-01T10:00:00",
"updatedAt": "2026-06-01T10:00:00",
"rooms": []
}Validation error response (RFC 9457 application/problem+json — 400 Bad Request):
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Validation failed for request",
"instance": "/hotel/create",
"errorCode": "VALIDATION_FAILED",
"timestamp": "2026-10-18T20:30:00.123456",
"traceId": "6a5346b258484bc2",
"invalidFields": {
"name": "must not be blank",
"rating": "must be between 1 and 5"
}
}Entity not found response (RFC 9457 application/problem+json — 404 Not Found):
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "Booking with ID 999 not found",
"instance": "/hotel/bookings/id/999",
"errorCode": "BOOKING_NOT_FOUND",
"timestamp": "2026-10-18T20:30:00.789101",
"traceId": "c24080ee3168f33d"
}Rate limit exceeded (429):
{ "error": "Too many booking requests. Limit: 20 per minute." }Total price is calculated at booking creation time:
totalPrice = nightlyRate × nights × (1 - discount/100)
| Input | Example |
|---|---|
| Nightly rate | ₹4,500 |
| Nights | 3 (Jun 5 → Jun 8) |
| Hotel discount | 15% |
| Total | ₹11,475.00 |
If the hotel has no discount set (discount = 0), the full rate applies. Price is stored in Booking.totalPrice as BigDecimal (2 decimal places, HALF_UP rounding).
After any booking state change, all clients subscribed to /topic/bookings/{hotelId} receive a push notification via STOMP over SockJS.
Notification payload:
{
"bookingReference": "BOOK-1A2B3C4D",
"status": "CONFIRMED",
"message": "Booking confirmed by admin",
"hotelId": 1,
"roomId": 3
}Test at http://localhost:8082/ws-test.html — enter a hotel ID and subscribe.
flowchart LR
Request["Incoming HTTP Request"] --> Brave["Micrometer Tracing\n(Brave bridge)"]
Brave --> TraceId["traceId + spanId injected\ninto MDC"]
TraceId --> LogPattern["Log Pattern:\n[traceId-spanId] class : message"]
TraceId --> Zipkin["Zipkin UI\nlocalhost:9411"]
BookingService --> BM["BookingMetrics\n(MeterRegistry)"]
BM --> Actuator["GET /actuator/metrics\n/hotel.bookings.*"]
Actuator --> Prometheus["Prometheus scrape\n(ready)"]
| Component | Detail |
|---|---|
| Micrometer Tracing | Brave bridge — instruments every HTTP request with traceId and spanId |
| Zipkin | Span collector and UI at http://localhost:9411 (Docker Compose) |
| MDC Correlation IDs | Every log line includes [traceId-spanId] for cross-log correlation |
| Booking MDC | userId, hotelId, bookingRef added to MDC within BookingService for per-request traceability |
| Custom Metrics | hotel.bookings.* counters + active gauge via BookingMetrics |
| Actuator metrics | GET /actuator/metrics — JVM, HTTP, datasource, cache, booking-specific 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 PATCH /hotel/bookings/10/confirm
The project ships an extensive multi-layer test suite with 93 tests across 20 test classes, all green on GitHub Actions CI (ubuntu-latest, JDK 17).
| Test Class | Layer / Strategy | Coverage |
|---|---|---|
EndToEndWorkflowIntegrationTest |
@SpringBootTest + @Testcontainers + MySQLContainer |
Full lifecycle: User → Hotel → Room → Booking (REQUESTED → CONFIRMED → CANCELLED) + Metrics + Audit |
BookingFlowIntegrationTest |
@SpringBootTest + @Testcontainers + MySQLContainer |
Booking happy path with WebSocket notification and audit log verification |
SoftDeleteIntegrationTest |
@SpringBootTest + shared Spring context |
Soft delete lifecycle: DELETE hotel → 404 on getById → absent in getAll → present in /deleted → restore via PATCH → booking references remain valid |
HotelIntegrationTest |
@SpringBootTest + shared Spring context + @WithMockUser(ADMIN) |
Full DB round-trip: POST hotel → assert persisted |
SecurityIntegrationTest |
@SpringBootTest + shared Spring context |
6 security scenarios: 401, 200 public, NORMAL→403, ADMIN→200 |
HotelSecurityConfigTest |
@WebMvcTest + @Import(HotelSecurityConfig) |
Security rule assertions at the filter chain level |
HotelControllerTest |
@WebMvcTest(HotelController) + MockMvc |
Controller endpoints returning HotelResponse DTOs, pagination, search, validation, soft-delete, and restore |
RoomControllerTest |
@WebMvcTest(RoomController) + MockMvc |
Room endpoints returning RoomResponse DTOs, availability queries, soft delete |
BookingControllerTest |
@WebMvcTest(BookingController) + MockMvc |
Booking creation, approval, and cancellation endpoints with DTO assertions |
UserControllerTest |
@WebMvcTest(UserController) + MockMvc |
User endpoints verifying UserResponse DTO without sensitive password |
GlobalExceptionHandlerTest |
Unit / Mockito | RFC 9457 ProblemDetail contract verification, error codes, trace ID propagation |
HotelMapperTest |
Unit | MapStruct HotelMapper entity ↔ DTO mapping and nested room mapping |
RoomMapperTest |
Unit | MapStruct RoomMapper entity ↔ DTO conversion |
UserMapperTest |
Unit | MapStruct UserMapper entity ↔ DTO conversion ensuring password omission |
BookingMapperTest |
Unit | MapStruct BookingMapper entity ↔ BookingDetailResponse and BookingResponse |
HotelRequestValidationTest |
Unit / Validator | Jakarta Bean Validation groups (OnCreate vs OnUpdate) |
BookingServiceTest |
@ExtendWith(MockitoExtension) + mocks |
Booking state machine: confirmed, rejected, invalid cancel transitions |
RoomServiceTest |
@ExtendWith(MockitoExtension) + mocks |
Availability lookup, Redis cache interaction, soft delete |
HotelServiceTest |
@ExtendWith(MockitoExtension) + mocks |
Hotel management, soft deletion cascade to rooms, restore logic |
UserServiceTest |
@ExtendWith(MockitoExtension) + mocks |
User registration, password encoding, and duplicate checks |
AuditServiceTest |
@ExtendWith(MockitoExtension) + mocks |
Audit log entry creation and persistence |
RateLimitingFilterTest |
Unit / MockHttpServletRequest | Bucket4j token bucket rate limiting on sensitive routes |
- Testcontainers MySQL —
BookingFlowIntegrationTestandEndToEndWorkflowIntegrationTestspin up a real MySQL container for production-parity testing. In environments without Docker running,@Testcontainers(disabledWithoutDocker = true)allows local development without failures while running strictly in CI. - CI driver fix — Explicitly registers
com.mysql.cj.jdbc.DriverinBaseIntegrationTestto prevent a Testcontainers↔Spring Boot driver-class clash when nospring.datasource.driver-class-nameis set. - Shared Spring context —
HotelIntegrationTestandSecurityIntegrationTestshare a singleApplicationContextto reduce test startup overhead. @BeforeEachcleanup — All integration tests callrepository.deleteAll()before each test to prevent data leakage.- CSRF handling —
MockMvcPOST requests include.with(csrf())to match real browser behavior. @MockBean ClientRegistrationRepository— Prevents OAuth2 Client auto-configuration from failing when Keycloak/Google credentials are absent in CI.- Soft Delete Safety —
@SQLRestriction("deleted = false")onHotelandRoomautomatically filters deleted records from queries while@NotFound(action = NotFoundAction.IGNORE)onBookingpreserves relational integrity for historical bookings. - JaCoCo Quality Gate — Enforces a minimum 50% line coverage requirement on business logic; automatically uploads HTML and XML coverage reports as workflow artifacts.
The GitHub Actions workflow (.github/workflows/ci.yml) runs on every push and pull request to main:
flowchart LR
Push["Git Push / PR"] --> Checkout["actions/checkout@v4"]
Checkout --> JDK["Setup JDK 17\n(Temurin + Maven cache)"]
JDK --> Test["mvn clean test"]
Test --> JaCoCo["JaCoCo Gate Check\n(Line coverage >= 50%)"]
JaCoCo --> Artifact["Upload JaCoCo Report\n(actions/upload-artifact@v4)"]
Artifact --> Report["Build & Test Status\n(All green ✅)"]
subgraph Test Suite
Unit["Unit & Mapper Tests\n(Mockito + MapStruct)"]
Validation["Validation Group Tests\n(Hibernate Validator)"]
SliceTest["Web Slice Tests\n(@WebMvcTest)"]
IT["Integration Tests\n(H2 + Testcontainers MySQL)"]
end
Test --> Unit & Validation & SliceTest & IT
The test suite is designed to run without any external infrastructure in CI:
@SpringBootTesttests with H2 replace MySQL for lightweight context tests- Testcontainers spins up a real MySQL container for
BookingFlowIntegrationTestandEndToEndWorkflowIntegrationTest spring.cache.type=simplereplaces Redis@MockBean ClientRegistrationRepositoryeliminates OAuth2 client startup calls- No Keycloak or Google OIDC credentials required
- Coverage reports are archived as build artifacts on every pipeline run
We use Apache Bench (ab) via scripts/load-test.sh to measure 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.
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
- Repository description:
Production-grade Spring Boot REST API for hotel/room/booking management — featuring a 4-state booking approval workflow, Bucket4j rate limiting, async email notifications, custom Micrometer metrics, AOP audit logging, paginated endpoints, Redis caching, JWT/OAuth2, WebSocket push notifications, Swagger/OpenAPI 3, Testcontainers integration tests, and a fully green GitHub Actions CI pipeline. - Topics:
java,java-17,spring-boot,spring-boot-3,spring-security,spring-data-jpa,spring-validation,spring-cache,spring-aop,spring-events,spring-async,redis,mysql,h2-database,rest-api,hotel-management,room-booking,booking-workflow,state-machine,rate-limiting,bucket4j,pagination,micrometer,custom-metrics,zipkin,distributed-tracing,concurrency,pessimistic-locking,jwt,oauth2,keycloak,google-login,thymeleaf,swagger,openapi,springdoc,lombok,testcontainers,github-actions,ci-cd,maven,portfolio-project