Students can register for events with automatic capacity checking, schedule conflict detection, and optimistic locking to prevent overbooking during high traffic.
Capacity Control
Prevent overbooking with optimistic locking
Conflict Detection
Detect schedule overlaps with existing registrations
The registration service implements a sophisticated concurrency control pattern:
RegistrationServiceImpl.java:37-114
@Override@Transactionalpublic void registerStudentForEvent(Long eventID, Long studentID) { int maxAttempts = 3; int attempt = 0; while (attempt < maxAttempts) { try { Event event = eventRepository.findById(eventID) .orElseThrow(() -> new NotFoundException("Event not found with ID: " + eventID)); if (event.getStatus() == EventStatus.CANCELLED || event.getStatus() == EventStatus.COMPLETED) { throw new IllegalStateException("Registrations are closed for this event."); } User user = userRepository.findById(studentID) .orElseThrow(() -> new NotFoundException("Student not found with ID: " + studentID)); if (!(user instanceof Student)) { throw new IllegalStateException("User is not a student."); } Student student = (Student) user; // 2. Initial Validations if (registrationRepository.existsByStudent_UserIDAndEvent_EventID(studentID, eventID)) { return; // Already registered - treat as success to be user-friendly } boolean hasOverlap = registrationRepository.existsByStudentAndDateOverlap( studentID, event.getEventDate(), event.getStartTime(), event.getEndTime()); if (hasOverlap) { throw new IllegalStateException("Schedule conflict: You have another event registered during this time slot."); } Integer currentRegistrations = event.getCurrentRegistrations() == null ? 0 : event.getCurrentRegistrations(); if (currentRegistrations >= event.getCapacity()) { throw new IllegalStateException("This event has reached full capacity."); } // 3. THE GATEKEEPER: Update the Event Count FIRST event.setCurrentRegistrations(currentRegistrations + 1); // DNA: saveAndFlush forces the @Version check IMMEDIATELY. // If another device moved first, this line throws the Exception. eventRepository.saveAndFlush(event); // 4. THE RECORD: Only create registration if the Gatekeeper allowed us through String confirmationCode = "REG-" + java.util.UUID.randomUUID() .toString() .substring(0, 8) .toUpperCase(); Registration registration = Registration.builder() .event(event) .student(student) .status(RegistrationStatus.CONFIRMED) .confirmationNumber(confirmationCode) .registeredAt(LocalDateTime.now()) .build(); registrationRepository.saveAndFlush(registration); // 5. Success sendConfirmationEmail(student, event, confirmationCode); return; } catch (ObjectOptimisticLockingFailureException e) { attempt++; if (attempt >= maxAttempts) { throw new IllegalStateException("High traffic detected. Please try again."); } // Small backoff before retry try { Thread.sleep(50); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } } }}
save(): Queues changes, commits at transaction end
saveAndFlush(): Writes to DB immediately, triggers @Version check NOW
Critical for detecting conflicts before creating registration
The Gatekeeper Pattern is Essential: Without updating capacity first, two concurrent requests could both pass the capacity check and create registrations, resulting in overbooking.
if (event.getStatus() == EventStatus.CANCELLED || event.getStatus() == EventStatus.COMPLETED) { throw new IllegalStateException("Registrations are closed for this event.");}
Prevents registration for cancelled or completed events.
User user = userRepository.findById(studentID) .orElseThrow(() -> new NotFoundException("Student not found with ID: " + studentID));if (!(user instanceof Student)) { throw new IllegalStateException("User is not a student.");}Student student = (Student) user;
Ensures only Student entities can register (defense-in-depth beyond @PreAuthorize).
if (registrationRepository.existsByStudent_UserIDAndEvent_EventID(studentID, eventID)) { return; // Already registered - treat as success to be user-friendly}
Idempotent design: Multiple registration attempts return success without error.
boolean hasOverlap = registrationRepository.existsByStudentAndDateOverlap( studentID, event.getEventDate(), event.getStartTime(), event.getEndTime());if (hasOverlap) { throw new IllegalStateException("Schedule conflict: You have another event registered during this time slot.");}
Prevents students from registering for overlapping events on the same day.
@Override@Transactionalpublic void cancelRegistration(Long registrationID, Long studentID) { Registration registration = registrationRepository.findById(registrationID) .orElseThrow(() -> new NotFoundException("Registration not found with ID: " + registrationID)); if (!registration.getStudent().getUserID().equals(studentID)) { throw new IllegalStateException("Unauthorized to cancel this registration."); } Event event = registration.getEvent(); LocalDateTime eventStart = LocalDateTime.of(event.getEventDate(), event.getStartTime()); if (Duration.between(LocalDateTime.now(), eventStart).toHours() < 24) { throw new IllegalStateException("Cancellation Policy: You must provide at least 24 hours notice to cancel your registration."); } Integer currentRegistrations = event.getCurrentRegistrations() == null ? 0 : event.getCurrentRegistrations(); event.setCurrentRegistrations(Math.max(0, currentRegistrations - 1)); eventRepository.save(event); registrationRepository.delete(registration);}
24-Hour Notice Required: Students must cancel at least 24 hours before the event start time.
RegistrationServiceImpl.java:146-149
LocalDateTime eventStart = LocalDateTime.of(event.getEventDate(), event.getStartTime());if (Duration.between(LocalDateTime.now(), eventStart).toHours() < 24) { throw new IllegalStateException("Cancellation Policy: You must provide at least 24 hours notice to cancel your registration.");}
{ "statusCode": 400, "message": "Cancellation Policy: You must provide at least 24 hours notice to cancel your registration.", "timestamp": "2024-03-15T10:30:00"}