Let members book a spot in a class
Java · Spring Boot · intermediate · greenfield
Adds class booking. `BookingController` exposes `POST /classes/{classId}/bookings` for the signed-in member and `BookingService.book` does the work in one transaction: it loads the class, rejects with 409 when the class is already at capacity, and otherwise saves the booking. The member-per-class uniqueness stays with the existing database constraint.
Studio-management backend on Spring Boot 3.3 (Java 21), Spring Data JPA with Hibernate 6.5 on PostgreSQL 16 at the default READ COMMITTED isolation. `server.error.include-message=always` is set, so a `ResponseStatusException` reason reaches the JSON error body. Existing and unchanged: `FitnessClass` (`Long id`, `int capacity` of at least 1), `Booking` (`classId`, `memberId`, with a unique constraint on `(class_id, member_id)`), `ClassRepository` (a plain `JpaRepository<FitnessClass, Long>`), `BookingRepository` with `long countByClassId(Long classId)`, and `CurrentMember.id()`, which returns the signed-in member's id. Only upcoming classes are offered in the app, so start times are not checked here. Cancelling a booking deletes its row. The service is a Spring singleton and each request runs in its own thread and its own transaction.
Requirements
- `POST /classes/{classId}/bookings` books the signed-in member into the class and returns the new booking's id with 201. An unknown class is a 404.
- A class never holds more bookings than its `capacity`. When the class is full the request is rejected with 409 "class is full" and nothing is saved, however many members book at the same moment: the studio has a fixed number of mats.
- A member can hold only one booking per class. The database already enforces this with a unique constraint on `(class_id, member_id)`, and the existing `@ExceptionHandler(DataIntegrityViolationException.class)` turns a violation into 409 "already booked". Do not add handling for it here.
- Members book from the app and from the front-desk tablet, so several requests for the same class can run at once. Cancelling, waitlists and changing a class's capacity already exist or are out of scope for this change.
Files touched
- src/main/java/com/acme/studio/booking/BookingService.java
- src/main/java/com/acme/studio/booking/BookingController.java
--- src/main/java/com/acme/studio/booking/BookingService.java
+package com.acme.studio.booking;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.server.ResponseStatusException;
+
+@Service
+public class BookingService {
+
+ private final ClassRepository classes;
+ private final BookingRepository bookings;
+
+ public BookingService(ClassRepository classes, BookingRepository bookings) {
+ this.classes = classes;
+ this.bookings = bookings;
+ }
+
+ @Transactional
+ public Booking book(long classId, long memberId) {
+ FitnessClass fitnessClass = classes.findById(classId)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "no such class"));
+ if (bookings.countByClassId(classId) >= fitnessClass.getCapacity()) {
+ throw new ResponseStatusException(HttpStatus.CONFLICT, "class is full");
+ }
+ return bookings.saveAndFlush(new Booking(classId, memberId));
+ }
+}
+