This guide will walk you through building a complete ticket booking system using Spring Boot, JPA, and Spring Security with multithreading support for concurrent bookings.
- User authentication and authorization
- Event management
- Concurrent ticket booking with thread safety
- Pessimistic and optimistic locking
- Async booking operations
- RESTful API endpoints
- Java 8 or higher
- Maven 3.6+
- IDE (IntelliJ IDEA, Eclipse, or VS Code)
- Basic knowledge of Spring Boot, JPA, and REST APIs
Create a new Maven project or use Spring Initializr (https://start.spring.io/)
Create the following pom.xml file in your project root:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/>
</parent>
<groupId>com.ticketbooking</groupId>
<artifactId>ticket-booking-system</artifactId>
<version>1.0.0</version>
<name>ticket-booking-system</name>
<description>Ticket booking system with Spring Boot</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot JPA Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Boot Security Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security Test -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Create src/main/resources/application.properties:
# Server Configuration
server.port=8080
# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:ticketdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# JPA Configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# Security Configuration
spring.security.user.name=admin
spring.security.user.password=admin123
spring.security.user.roles=ADMIN
# Thread Pool Configuration
ticket.booking.thread-pool.core-size=5
ticket.booking.thread-pool.max-size=10
ticket.booking.thread-pool.queue-capacity=100
# Logging
logging.level.com.ticketbooking=DEBUG
logging.level.org.springframework.security=DEBUGCreate src/main/java/com/ticketbooking/entity/User.java:
package com.ticketbooking.entity;
import javax.persistence.*;
import java.util.List;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String role = "USER";
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Booking> bookings;
// Constructors
public User() {}
public User(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public List<Booking> getBookings() { return bookings; }
public void setBookings(List<Booking> bookings) { this.bookings = bookings; }
}Create src/main/java/com/ticketbooking/entity/Event.java:
package com.ticketbooking.entity;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "events")
public class Event {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String venue;
@Column(nullable = false)
private LocalDateTime eventDate;
@Column(nullable = false)
private Integer totalSeats;
@Column(nullable = false)
private Integer availableSeats;
@Column(nullable = false)
private Double price;
@OneToMany(mappedBy = "event", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Booking> bookings;
@Version
private Long version; // For optimistic locking
// Constructors
public Event() {}
public Event(String name, String venue, LocalDateTime eventDate, Integer totalSeats, Double price) {
this.name = name;
this.venue = venue;
this.eventDate = eventDate;
this.totalSeats = totalSeats;
this.availableSeats = totalSeats;
this.price = price;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getVenue() { return venue; }
public void setVenue(String venue) { this.venue = venue; }
public LocalDateTime getEventDate() { return eventDate; }
public void setEventDate(LocalDateTime eventDate) { this.eventDate = eventDate; }
public Integer getTotalSeats() { return totalSeats; }
public void setTotalSeats(Integer totalSeats) { this.totalSeats = totalSeats; }
public Integer getAvailableSeats() { return availableSeats; }
public void setAvailableSeats(Integer availableSeats) { this.availableSeats = availableSeats; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public List<Booking> getBookings() { return bookings; }
public void setBookings(List<Booking> bookings) { this.bookings = bookings; }
public Long getVersion() { return version; }
public void setVersion(Long version) { this.version = version; }
}Create src/main/java/com/ticketbooking/entity/Booking.java:
package com.ticketbooking.entity;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "bookings")
public class Booking {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "event_id", nullable = false)
private Event event;
@Column(nullable = false)
private Integer seatsBooked;
@Column(nullable = false)
private LocalDateTime bookingDate;
@Column(nullable = false)
private Double totalAmount;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private BookingStatus status = BookingStatus.CONFIRMED;
// Constructors
public Booking() {}
public Booking(User user, Event event, Integer seatsBooked, Double totalAmount) {
this.user = user;
this.event = event;
this.seatsBooked = seatsBooked;
this.totalAmount = totalAmount;
this.bookingDate = LocalDateTime.now();
this.status = BookingStatus.CONFIRMED;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public User getUser() { return user; }
public void setUser(User user) { this.user = user; }
public Event getEvent() { return event; }
public void setEvent(Event event) { this.event = event; }
public Integer getSeatsBooked() { return seatsBooked; }
public void setSeatsBooked(Integer seatsBooked) { this.seatsBooked = seatsBooked; }
public LocalDateTime getBookingDate() { return bookingDate; }
public void setBookingDate(LocalDateTime bookingDate) { this.bookingDate = bookingDate; }
public Double getTotalAmount() { return totalAmount; }
public void setTotalAmount(Double totalAmount) { this.totalAmount = totalAmount; }
public BookingStatus getStatus() { return status; }
public void setStatus(BookingStatus status) { this.status = status; }
public enum BookingStatus {
CONFIRMED, CANCELLED, PENDING
}
}Create src/main/java/com/ticketbooking/repository/UserRepository.java:
package com.ticketbooking.repository;
import com.ticketbooking.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}Create src/main/java/com/ticketbooking/repository/EventRepository.java:
package com.ticketbooking.repository;
import com.ticketbooking.entity.Event;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import javax.persistence.LockModeType;
import java.util.List;
import java.util.Optional;
@Repository
public interface EventRepository extends JpaRepository<Event, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT e FROM Event e WHERE e.id = :id")
Optional<Event> findByIdWithLock(@Param("id") Long id);
@Query("SELECT e FROM Event e WHERE e.availableSeats > 0")
List<Event> findAvailableEvents();
@Query("SELECT e FROM Event e WHERE e.name LIKE %:name%")
List<Event> findByNameContaining(@Param("name") String name);
}Create src/main/java/com/ticketbooking/repository/BookingRepository.java:
package com.ticketbooking.repository;
import com.ticketbooking.entity.Booking;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface BookingRepository extends JpaRepository<Booking, Long> {
List<Booking> findByUserId(Long userId);
List<Booking> findByEventId(Long eventId);
@Query("SELECT b FROM Booking b WHERE b.user.username = :username")
List<Booking> findByUserUsername(@Param("username") String username);
@Query("SELECT COUNT(b) FROM Booking b WHERE b.event.id = :eventId")
Long countByEventId(@Param("eventId") Long eventId);
}Create src/main/java/com/ticketbooking/service/UserService.java:
package com.ticketbooking.service;
import com.ticketbooking.entity.User;
import com.ticketbooking.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public List<User> getAllUsers() {
return userRepository.findAll();
}
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
public Optional<User> getUserByUsername(String username) {
return userRepository.findByUsername(username);
}
public User createUser(User user) {
if (userRepository.existsByUsername(user.getUsername())) {
throw new RuntimeException("Username already exists");
}
if (userRepository.existsByEmail(user.getEmail())) {
throw new RuntimeException("Email already exists");
}
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
public User updateUser(User user) {
return userRepository.save(user);
}
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
public boolean existsByUsername(String username) {
return userRepository.existsByUsername(username);
}
public boolean existsByEmail(String email) {
return userRepository.existsByEmail(email);
}
}Create src/main/java/com/ticketbooking/service/EventService.java:
package com.ticketbooking.service;
import com.ticketbooking.entity.Event;
import com.ticketbooking.repository.EventRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class EventService {
@Autowired
private EventRepository eventRepository;
public List<Event> getAllEvents() {
return eventRepository.findAll();
}
public List<Event> getAvailableEvents() {
return eventRepository.findAvailableEvents();
}
public Optional<Event> getEventById(Long id) {
return eventRepository.findById(id);
}
public List<Event> searchEventsByName(String name) {
return eventRepository.findByNameContaining(name);
}
public Event createEvent(Event event) {
if (event.getAvailableSeats() == null) {
event.setAvailableSeats(event.getTotalSeats());
}
return eventRepository.save(event);
}
public Event updateEvent(Event event) {
return eventRepository.save(event);
}
public void deleteEvent(Long id) {
eventRepository.deleteById(id);
}
public Optional<Event> getEventByIdWithLock(Long id) {
return eventRepository.findByIdWithLock(id);
}
}Create src/main/java/com/ticketbooking/service/BookingService.java:
package com.ticketbooking.service;
import com.ticketbooking.entity.Booking;
import com.ticketbooking.entity.Event;
import com.ticketbooking.entity.User;
import com.ticketbooking.repository.BookingRepository;
import com.ticketbooking.repository.EventRepository;
import com.ticketbooking.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class BookingService {
private static final Logger logger = LoggerFactory.getLogger(BookingService.class);
@Autowired
private BookingRepository bookingRepository;
@Autowired
private EventRepository eventRepository;
@Autowired
private UserRepository userRepository;
// Lock per event to handle concurrent bookings
private final ConcurrentHashMap<Long, ReentrantLock> eventLocks = new ConcurrentHashMap<>();
@Transactional
public Booking bookTicket(Long userId, Long eventId, Integer seatsRequested) {
logger.info("Booking request: User {} wants {} seats for event {}", userId, seatsRequested, eventId);
// Get or create lock for this event
ReentrantLock eventLock = eventLocks.computeIfAbsent(eventId, k -> new ReentrantLock(true));
eventLock.lock();
try {
// Fetch user and event
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found with id: " + userId));
Event event = eventRepository.findByIdWithLock(eventId)
.orElseThrow(() -> new RuntimeException("Event not found with id: " + eventId));
// Check availability
if (event.getAvailableSeats() < seatsRequested) {
throw new RuntimeException("Not enough seats available. Available: " +
event.getAvailableSeats() + ", Requested: " + seatsRequested);
}
if (seatsRequested <= 0) {
throw new RuntimeException("Number of seats must be positive");
}
// Update available seats
event.setAvailableSeats(event.getAvailableSeats() - seatsRequested);
eventRepository.save(event);
// Create booking
Double totalAmount = event.getPrice() * seatsRequested;
Booking booking = new Booking(user, event, seatsRequested, totalAmount);
Booking savedBooking = bookingRepository.save(booking);
logger.info("Booking successful: {} for user {} with {} seats",
savedBooking.getId(), user.getUsername(), seatsRequested);
return savedBooking;
} finally {
eventLock.unlock();
}
}
@Async("bookingTaskExecutor")
public CompletableFuture<Booking> bookTicketAsync(Long userId, Long eventId, Integer seatsRequested) {
logger.info("Async booking request: User {} wants {} seats for event {}", userId, seatsRequested, eventId);
try {
Booking booking = bookTicket(userId, eventId, seatsRequested);
return CompletableFuture.completedFuture(booking);
} catch (Exception e) {
logger.error("Async booking failed: {}", e.getMessage());
CompletableFuture<Booking> future = new CompletableFuture<>();
future.completeExceptionally(e);
return future;
}
}
public List<Booking> getAllBookings() {
return bookingRepository.findAll();
}
public Optional<Booking> getBookingById(Long id) {
return bookingRepository.findById(id);
}
public List<Booking> getUserBookings(Long userId) {
return bookingRepository.findByUserId(userId);
}
public List<Booking> getEventBookings(Long eventId) {
return bookingRepository.findByEventId(eventId);
}
public List<Booking> getUserBookingsByUsername(String username) {
return bookingRepository.findByUserUsername(username);
}
@Transactional
public boolean cancelBooking(Long bookingId) {
logger.info("Canceling booking: {}", bookingId);
Optional<Booking> bookingOpt = bookingRepository.findById(bookingId);
if (bookingOpt.isPresent()) {
Booking booking = bookingOpt.get();
if (booking.getStatus() == Booking.BookingStatus.CANCELLED) {
throw new RuntimeException("Booking is already cancelled");
}
Event event = booking.getEvent();
// Get lock for this event
ReentrantLock eventLock = eventLocks.computeIfAbsent(event.getId(), k -> new ReentrantLock(true));
eventLock.lock();
try {
// Return seats to available pool
event.setAvailableSeats(event.getAvailableSeats() + booking.getSeatsBooked());
eventRepository.save(event);
// Update booking status
booking.setStatus(Booking.BookingStatus.CANCELLED);
bookingRepository.save(booking);
logger.info("Booking {} cancelled successfully", bookingId);
return true;
} finally {
eventLock.unlock();
}
}
logger.warn("Booking {} not found for cancellation", bookingId);
return false;
}
public Long getBookingCountForEvent(Long eventId) {
return bookingRepository.countByEventId(eventId);
}
}Create src/main/java/com/ticketbooking/config/ThreadPoolConfig.java:
package com.ticketbooking.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class ThreadPoolConfig {
@Value("${ticket.booking.thread-pool.core-size:5}")
private int corePoolSize;
@Value("${ticket.booking.thread-pool.max-size:10}")
private int maxPoolSize;
@Value("${ticket.booking.thread-pool.queue-capacity:100}")
private int queueCapacity;
@Bean(name = "bookingTaskExecutor")
public Executor bookingTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setThreadNamePrefix("BookingThread-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}Create src/main/java/com/ticketbooking/config/SecurityConfig.java:
package com.ticketbooking.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(authz -> authz
.antMatchers("/api/auth/**", "/h2-console/**", "/api/users/register").permitAll()
.antMatchers("/api/events/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/api/bookings/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic()
.and()
.headers().frameOptions().disable(); // For H2 console
return http.build();
}
}Create src/main/java/com/ticketbooking/controller/UserController.java:
package com.ticketbooking.controller;
import com.ticketbooking.entity.User;
import com.ticketbooking.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(user -> ResponseEntity.ok(user))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping("/register")
public ResponseEntity<?> registerUser(@RequestBody User user) {
try {
User savedUser = userService.createUser(user);
return ResponseEntity.ok(savedUser);
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
User updatedUser = userService.updateUser(user);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.ok("User deleted successfully");
}
}Create src/main/java/com/ticketbooking/controller/EventController.java:
package com.ticketbooking.controller;
import com.ticketbooking.entity.Event;
import com.ticketbooking.service.EventService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/events")
public class EventController {
@Autowired
private EventService eventService;
@GetMapping
public ResponseEntity<List<Event>> getAllEvents() {
List<Event> events = eventService.getAllEvents();
return ResponseEntity.ok(events);
}
@GetMapping("/available")
public ResponseEntity<List<Event>> getAvailableEvents() {
List<Event> events = eventService.getAvailableEvents();
return ResponseEntity.ok(events);
}
@GetMapping("/{id}")
public ResponseEntity<Event> getEventById(@PathVariable Long id) {
return eventService.getEventById(id)
.map(event -> ResponseEntity.ok(event))
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/search")
public ResponseEntity<List<Event>> searchEvents(@RequestParam String name) {
List<Event> events = eventService.searchEventsByName(name);
return ResponseEntity.ok(events);
}
@PostMapping
public ResponseEntity<Event> createEvent(@RequestBody Event event) {
Event savedEvent = eventService.createEvent(event);
return ResponseEntity.ok(savedEvent);
}
@PutMapping("/{id}")
public ResponseEntity<Event> updateEvent(@PathVariable Long id, @RequestBody Event event) {
event.setId(id);
Event updatedEvent = eventService.updateEvent(event);
return ResponseEntity.ok(updatedEvent);
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteEvent(@PathVariable Long id) {
eventService.deleteEvent(id);
return ResponseEntity.ok("Event deleted successfully");
}
}Create src/main/java/com/ticketbooking/controller/BookingController.java:
package com.ticketbooking.controller;
import com.ticketbooking.entity.Booking;
import com.ticketbooking.service.BookingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/api/bookings")
public class BookingController {
@Autowired
private BookingService bookingService;
@GetMapping
public ResponseEntity<List<Booking>> getAllBookings() {
List<Booking> bookings = bookingService.getAllBookings();
return ResponseEntity.ok(bookings);
}
@GetMapping("/{id}")
public ResponseEntity<Booking> getBookingById(@PathVariable Long id) {
return bookingService.getBookingById(id)
.map(booking -> ResponseEntity.ok(booking))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping("/book")
public ResponseEntity<?> bookTicket(@RequestBody BookingRequest request) {
try {
Booking booking = bookingService.bookTicket(
request.getUserId(),
request.getEventId(),
request.getSeatsRequested()
);
return ResponseEntity.ok(booking);
} catch (Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@PostMapping("/book-async")
public CompletableFuture<ResponseEntity<?>> bookTicketAsync(@RequestBody BookingRequest request) {
return bookingService.bookTicketAsync(
request.getUserId(),
request.getEventId(),
request.getSeatsRequested()
).thenApply(booking -> ResponseEntity.ok(booking))
.exceptionally(ex -> ResponseEntity.badRequest().body(ex.getCause().getMessage()));
}
@GetMapping("/user/{userId}")
public ResponseEntity<List<Booking>> getUserBookings(@PathVariable Long userId) {
List<Booking> bookings = bookingService.getUserBookings(userId);
return ResponseEntity.ok(bookings);
}
@GetMapping("/event/{eventId}")
public ResponseEntity<List<Booking>> getEventBookings(@PathVariable Long eventId) {
List<Booking> bookings = bookingService.getEventBookings(eventId);
return ResponseEntity.ok(bookings);
}
@DeleteMapping("/{bookingId}")
public ResponseEntity<?> cancelBooking(@PathVariable Long bookingId) {
try {
boolean cancelled = bookingService.cancelBooking(bookingId);
if (cancelled) {
return ResponseEntity.ok("Booking cancelled successfully");
} else {
return ResponseEntity.badRequest().body("Failed to cancel booking");
}
} catch (Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@GetMapping("/count/event/{eventId}")
public ResponseEntity<Long> getBookingCountForEvent(@PathVariable Long eventId) {
Long count = bookingService.getBookingCountForEvent(eventId);
return ResponseEntity.ok(count);
}
// Inner class for request body
public static class BookingRequest {
private Long userId;
private Long eventId;
private Integer seatsRequested;
public BookingRequest() {}
public BookingRequest(Long userId, Long eventId, Integer seatsRequested) {
this.userId = userId;
this.eventId = eventId;
this.seatsRequested = seatsRequested;
}
// Getters and setters
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public Long getEventId() { return eventId; }
public void setEventId(Long eventId) { this.eventId = eventId; }
public Integer getSeatsRequested() { return seatsRequested; }
public void setSeatsRequested(Integer seatsRequested) { this.seatsRequested = seatsRequested; }
}
}Create src/main/java/com/ticketbooking/TicketBookingSystemApplication.java:
package com.ticketbooking;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableAsync
@EnableTransactionManagement
public class TicketBookingSystemApplication {
public static void main(String[] args) {
SpringApplication.run(TicketBookingSystemApplication.class, args);
}
}Create src/main/java/com/ticketbooking/config/DataLoader.java:
package com.ticketbooking.config;
import com.ticketbooking.entity.Event;
import com.ticketbooking.entity.User;
import com.ticketbooking.service.EventService;
import com.ticketbooking.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class DataLoader implements ApplicationRunner {
@Autowired
private UserService userService;
@Autowired
private EventService eventService;
@Override
public void run(ApplicationArguments args) throws Exception {
// Create sample users
if (!userService.existsByUsername("john_doe")) {
User user1 = new User("john_doe", "password123", "john@example.com");
userService.createUser(user1);
}
if (!userService.existsByUsername("jane_smith")) {
User user2 = new User("jane_smith", "password123", "jane@example.com");
userService.createUser(user2);
}
if (!userService.existsByUsername("admin_user")) {
User admin = new User("admin_user", "admin123", "admin@example.com");
admin.setRole("ADMIN");
userService.createUser(admin);
}
// Create sample events
if (eventService.getAllEvents().isEmpty()) {
Event event1 = new Event(
"Rock Concert",
"Stadium Arena",
LocalDateTime.now().plusDays(30),
100,
75.0
);
eventService.createEvent(event1);
Event event2 = new Event(
"Jazz Festival",
"Music Hall",
LocalDateTime.now().plusDays(45),
50,
50.0
);
eventService.createEvent(event2);
Event event3 = new Event(
"Comedy Show",
"Comedy Club",
LocalDateTime.now().plusDays(15),
25,
35.0
);
eventService.createEvent(event3);
}
}
}Create src/test/java/com/ticketbooking/ConcurrentBookingTest.java:
package com.ticketbooking;
import com.ticketbooking.entity.Booking;
import com.ticketbooking.entity.Event;
import com.ticketbooking.entity.User;
import com.ticketbooking.service.BookingService;
import com.ticketbooking.service.EventService;
import com.ticketbooking.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
@SpringBootTest
@ActiveProfiles("test")
public class ConcurrentBookingTest {
@Autowired
private BookingService bookingService;
@Autowired
private EventService eventService;
@Autowired
private UserService userService;
@Test
public void testConcurrentBooking() throws Exception {
// Create test event with limited seats
Event event = new Event("Test Concert", "Test Venue", LocalDateTime.now().plusDays(1), 10, 100.0);
Event savedEvent = eventService.createEvent(event);
// Create test users
List<User> users = new ArrayList<>();
for (int i = 1; i <= 20; i++) {
User user = new User("testuser" + i, "password", "testuser" + i + "@test.com");
users.add(userService.createUser(user));
}
// Create thread pool
ExecutorService executor = Executors.newFixedThreadPool(10);
List<CompletableFuture<Void>> futures = new ArrayList<>();
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failureCount = new AtomicInteger(0);
// Simulate concurrent booking attempts
for (User user : users) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
Booking booking = bookingService.bookTicket(user.getId(), savedEvent.getId(), 1);
successCount.incrementAndGet();
System.out.println("✓ User " + user.getUsername() + " successfully booked ticket: " + booking.getId());
} catch (Exception e) {
failureCount.incrementAndGet();
System.out.println("✗ User " + user.getUsername() + " failed to book: " + e.getMessage());
}
}, executor);
futures.add(future);
}
// Wait for all threads to complete
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// Check results
Event updatedEvent = eventService.getEventById(savedEvent.getId()).orElse(null);
System.out.println("\n=== Test Results ===");
System.out.println("Initial seats: " + savedEvent.getTotalSeats());
System.out.println("Final available seats: " + updatedEvent.getAvailableSeats());
System.out.println("Successful bookings: " + successCount.get());
System.out.println("Failed bookings: " + failureCount.get());
System.out.println("Total booking attempts: " + users.size());
// Verify no overselling occurred
assert updatedEvent.getAvailableSeats() == (savedEvent.getTotalSeats() - successCount.get());
assert successCount.get() <= savedEvent.getTotalSeats();
executor.shutdown();
}
}Create src/test/resources/application-test.properties:
# Test Database Configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
# JPA Configuration for Testing
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false
# Disable security for tests
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
# Logging for tests
logging.level.com.ticketbooking=INFOmvn clean compile
mvn clean packagemvn spring-boot:runjava -jar target/ticket-booking-system-1.0.0.jar- URL:
http://localhost:8080/h2-console - JDBC URL:
jdbc:h2:mem:ticketdb - Username:
sa - Password:
password
curl -X POST http://localhost:8080/api/users/register \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"password": "password123",
"email": "testuser@example.com"
}'curl -u admin:admin123 http://localhost:8080/api/eventscurl -X POST http://localhost:8080/api/bookings/book \
-u admin:admin123 \
-H "Content-Type: application/json" \
-d '{
"userId": 1,
"eventId": 1,
"seatsRequested": 2
}'curl -X POST http://localhost:8080/api/bookings/book-async \
-u admin:admin123 \
-H "Content-Type: application/json" \
-d '{
"userId": 1,
"eventId": 1,
"seatsRequested": 1
}'curl -u admin:admin123 http://localhost:8080/api/bookings/user/1curl -X DELETE http://localhost:8080/api/bookings/1 \
-u admin:admin123Your final project structure should look like this:
ticket-booking-system/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── ticketbooking/
│ │ │ ├── TicketBookingSystemApplication.java
│ │ │ ├── config/
│ │ │ │ ├── DataLoader.java
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ └── ThreadPoolConfig.java
│ │ │ ├── controller/
│ │ │ │ ├── BookingController.java
│ │ │ │ ├── EventController.java
│ │ │ │ └── UserController.java
│ │ │ ├── entity/
│ │ │ │ ├── Booking.java
│ │ │ │ ├── Event.java
│ │ │ │ └── User.java
│ │ │ ├── repository/
│ │ │ │ ├── BookingRepository.java
│ │ │ │ ├── EventRepository.java
│ │ │ │ └── UserRepository.java
│ │ │ └── service/
│ │ │ ├── BookingService.java
│ │ │ ├── EventService.java
│ │ │ └── UserService.java
│ │ └── resources/
│ │ └── application.properties
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── ticketbooking/
│ │ └── ConcurrentBookingTest.java
│ └── resources/
│ └── application-test.properties
├── pom.xml
└── README.md
- Uses
@Lock(LockModeType.PESSIMISTIC_WRITE)in EventRepository - Prevents concurrent modifications to the same event
- Each event has its own lock to prevent race conditions
- Allows concurrent bookings for different events
- Custom thread pool for async operations
- Configurable core size, max size, and queue capacity
@Asyncannotation for non-blocking booking operations- CompletableFuture for async responses
@Versionannotation in Event entity- Handles concurrent updates gracefully
@Transactionalensures data consistency- Automatic rollback on exceptions
-
Port Already in Use
- Change port in
application.properties:server.port=8081
- Change port in
-
Database Connection Issues
- Check H2 console at
http://localhost:8080/h2-console - Verify JDBC URL:
jdbc:h2:mem:ticketdb
- Check H2 console at
-
Authentication Errors
- Use basic auth with username/password from
application.properties - Default:
admin:admin123
- Use basic auth with username/password from
-
Concurrent Booking Test Failures
- Increase thread pool size in
application.properties - Check logs for lock timeout issues
- Increase thread pool size in
-
Maven Build Errors
- Ensure Java 8+ is installed
- Run
mvn clean installto resolve dependencies
- Add Frontend: Create a React or Angular frontend
- Database Migration: Switch from H2 to PostgreSQL/MySQL
- Email Notifications: Send booking confirmations
- Payment Integration: Add payment gateway
- Monitoring: Add metrics and health checks
- Docker: Containerize the application
- Testing: Add more comprehensive tests
This ticket booking system demonstrates:
- Thread-safe concurrent booking operations
- Proper database locking mechanisms
- RESTful API design
- Spring Boot best practices
- Security implementation
- Async programming with CompletableFuture
The system ensures no overselling occurs even under high concurrent load while maintaining good performance through proper thread management and database optimization.