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

Files touched

--- 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));
+    }
+}
+

Review this PR

Java practice