Skip to main content

Overview

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

Confirmation Codes

Unique confirmation numbers for each registration

Email Notifications

Automatic confirmation emails

Registration Flow

1

Student Initiates Registration

Student clicks “Register” for an event
2

Validation Checks

System validates capacity, conflicts, and event status
3

Optimistic Lock

Event capacity is incremented with version check
4

Create Registration

Registration record is created with confirmation code
5

Send Confirmation

Email notification sent asynchronously

Register for Event

Endpoint

POST /api/registrations/event/{eventID}

Authorization

RegistrationController.java:29-30
@PostMapping("/event/{eventID}")
@PreAuthorize("hasRole('STUDENT')")
Only users with STUDENT role can register for events.

Implementation

RegistrationController.java:31-40
public ResponseEntity<Response<Void>> register(
        @PathVariable Long eventID,
        @AuthenticationPrincipal CustomUserDetails userDetails) {

    Long studentID = userDetails.getUser().getUserID();
    registrationService.registerStudentForEvent(eventID, studentID);

    return ResponseEntity.status(HttpStatus.CREATED)
            .body(new Response<>(201, "Registration successful", null));
}

Example Request

curl -X POST https://api.ems.edu/api/registrations/event/456 \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Response Example

{
  "statusCode": 201,
  "message": "Registration successful",
  "data": null,
  "timestamp": "2024-03-15T10:30:00"
}

The Gatekeeper Pattern

The registration service implements a sophisticated concurrency control pattern:
RegistrationServiceImpl.java:37-114
@Override
@Transactional
public 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();
            }
        }
    }
}

Key Concepts

Update capacity BEFORE creating registration:
event.setCurrentRegistrations(currentRegistrations + 1);
eventRepository.saveAndFlush(event); // Forces @Version check
This ensures:
  • Capacity is the single source of truth
  • Race conditions are detected immediately
  • No orphan registrations when capacity is exceeded
JPA @Version field:
Event.java:82-84
@Version
private Long version;
How it works:
  1. Event loaded with version = 5
  2. Request A and B both read same event
  3. Request A saves first: version → 6 ✓
  4. Request B tries to save: version mismatch → Exception ✗
3 attempts with 50ms backoff:
int maxAttempts = 3;
while (attempt < maxAttempts) {
    try {
        // ... registration logic
    } catch (ObjectOptimisticLockingFailureException e) {
        attempt++;
        Thread.sleep(50); // Backoff
    }
}
Benefits:
  • Handles temporary contention
  • Improves success rate during high traffic
  • Graceful degradation
Immediate persistence:
eventRepository.saveAndFlush(event);
Why not just save()?
  • 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.

Validation Checks

1. Event Status Check

RegistrationServiceImpl.java:48-50
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.

2. User Role Verification

RegistrationServiceImpl.java:52-57
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).

3. Duplicate Registration Check

RegistrationServiceImpl.java:60-62
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.

4. Schedule Conflict Detection

RegistrationServiceImpl.java:63-67
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.

5. Capacity Check

RegistrationServiceImpl.java:68-73
Integer currentRegistrations = event.getCurrentRegistrations() == null
        ? 0
        : event.getCurrentRegistrations();
if (currentRegistrations >= event.getCapacity()) {
    throw new IllegalStateException("This event has reached full capacity.");
}
This is a preliminary check. The real capacity enforcement happens via optimistic locking.

Confirmation Codes

Each registration receives a unique confirmation code:
RegistrationServiceImpl.java:84-87
String confirmationCode = "REG-" + java.util.UUID.randomUUID()
        .toString()
        .substring(0, 8)
        .toUpperCase();
Format: REG-XXXXXXXX (e.g., REG-A3B7C9D2)

Unique Identifier

UUID-based generation ensures uniqueness

Short Format

8-character codes are easy to share and verify

Scannable

Can be encoded in QR codes for check-in

Verifiable

Used for attendance tracking at events

Get My Registrations

Endpoint

GET /api/registrations/me

Authorization

RegistrationController.java:42-43
@GetMapping("/me")
@PreAuthorize("hasRole('STUDENT')")

Implementation

RegistrationController.java:44-51
public ResponseEntity<Response<List<RegistrationDTO>>> getMyRegistrations(
        @AuthenticationPrincipal CustomUserDetails userDetails) {

    Long studentID = userDetails.getUser().getUserID();
    List<RegistrationDTO> registrations = registrationService.getMyRegistrations(studentID);

    return ResponseEntity.ok(new Response<>(200, "Registrations retrieved successfully", registrations));
}

Response Example

{
  "statusCode": 200,
  "message": "Registrations retrieved successfully",
  "data": [
    {
      "registrationID": 789,
      "eventID": 456,
      "eventTitle": "React Workshop",
      "eventDate": "2024-04-15",
      "venue": "Room 301",
      "status": "CONFIRMED",
      "registeredAt": "2024-03-15T10:30:00"
    },
    {
      "registrationID": 788,
      "eventID": 455,
      "eventTitle": "Career Fair 2024",
      "eventDate": "2024-03-20",
      "venue": "Main Hall",
      "status": "CONFIRMED",
      "registeredAt": "2024-03-10T14:20:00"
    }
  ],
  "timestamp": "2024-03-15T10:35:00"
}

Cancel Registration

Endpoint

DELETE /api/registrations/{registrationID}

Authorization

RegistrationController.java:53-54
@DeleteMapping("/{registrationID}")
@PreAuthorize("hasRole('STUDENT')")

Implementation

RegistrationServiceImpl.java:136-158
@Override
@Transactional
public 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);
}

Cancellation Policy

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.");
}

Example Request

curl -X DELETE https://api.ems.edu/api/registrations/789 \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Response Example

{
  "statusCode": 200,
  "message": "Registration cancelled",
  "data": null,
  "timestamp": "2024-03-15T10:30:00"
}

View Participants (Organizers/Admins)

Endpoint

GET /api/registrations/event/{eventID}/participants

Authorization

RegistrationController.java:65-66
@GetMapping("/event/{eventID}/participants")
@PreAuthorize("hasAnyRole('ADMIN','ORGANIZER')")
Organizers can only view participants for their own events. Admins can view participants for any event.

Implementation

RegistrationServiceImpl.java:184-209
@Override
@Transactional(readOnly = true)
public List<Map<String, String>> getParticipantDetails(Long eventID, User requester) {
    Event event = eventRepository.findById(eventID)
            .orElseThrow(() -> new NotFoundException("Event not found with ID: " + eventID));

    if (requester.getRole() == UserRole.ORGANIZER) {
        Long organizerId = event.getOrganizer() != null ? event.getOrganizer().getUserID() : null;
        if (organizerId == null || !organizerId.equals(requester.getUserID())) {
            throw new IllegalStateException("Unauthorized to view participants for this event.");
        }
    }

    List<Registration> registrations = registrationRepository.findByEvent_EventID(eventID);
    return registrations.stream()
            .map(reg -> {
                Student student = reg.getStudent();
                String firstName = student.getFirstName() != null ? student.getFirstName() : "";
                String lastName = student.getLastName() != null ? student.getLastName() : "";
                String name = (firstName + " " + lastName).trim();
                Map<String, String> map = new HashMap<>();
                map.put("name", name.isBlank() ? "Student" : name);
                map.put("email", student.getEmail());
                return map;
            })
            .toList();
}

Response Example

{
  "statusCode": 200,
  "message": "Participants fetched",
  "data": [
    {
      "name": "John Doe",
      "email": "[email protected]"
    },
    {
      "name": "Jane Smith",
      "email": "[email protected]"
    }
  ],
  "timestamp": "2024-03-15T10:35:00"
}
Participant data is limited to name and email for privacy reasons.

Email Notifications

Confirmation Email

After successful registration, a confirmation email is sent asynchronously:
RegistrationServiceImpl.java:211-224
private void sendConfirmationEmail(Student student, Event event, String confirmationCode) {
    String emailBody = String.format(
            "Hello %s,%n%nYour registration for '%s' is confirmed.%n" +
                    "Confirmation Number: %s%n" +
                    "Venue: %s%nDate: %s%n%nSee you there!",
            student.getFirstName(),
            event.getTitle(),
            confirmationCode,
            event.getVenue(),
            event.getEventDate().toString()
    );

    emailService.sendNotification(student.getEmail(), "Registration Confirmed", emailBody);
}

Email Content Example

Subject: [EMS] Registration Confirmed

Hello John,

Your registration for 'React Workshop' is confirmed.
Confirmation Number: REG-A3B7C9D2
Venue: Room 301
Date: 2024-04-15

See you there!
See Notifications for details on the email service implementation.

Error Handling

{
  "statusCode": 404,
  "message": "Event not found with ID: 456",
  "timestamp": "2024-03-15T10:30:00"
}
{
  "statusCode": 400,
  "message": "This event has reached full capacity.",
  "timestamp": "2024-03-15T10:30:00"
}
{
  "statusCode": 400,
  "message": "Schedule conflict: You have another event registered during this time slot.",
  "timestamp": "2024-03-15T10:30:00"
}
{
  "statusCode": 400,
  "message": "Registrations are closed for this event.",
  "timestamp": "2024-03-15T10:30:00"
}
{
  "statusCode": 400,
  "message": "High traffic detected. Please try again.",
  "timestamp": "2024-03-15T10:30:00"
}
{
  "statusCode": 400,
  "message": "Cancellation Policy: You must provide at least 24 hours notice to cancel your registration.",
  "timestamp": "2024-03-15T10:30:00"
}

Best Practices

1

Check Availability First

Query event details before attempting registration to show capacity status to users.
2

Handle High Traffic Errors

Implement retry logic on the client side for “High traffic detected” errors.
3

Show Confirmation Codes

Display confirmation codes prominently after successful registration.
4

Enforce 24-Hour Policy

Show countdown or disable cancel button within 24 hours of event start.

Events

Learn about event browsing and management

Notifications

Understand the email notification system

Build docs developers (and LLMs) love