A dockerized backend service built with Spring Boot and MySQL to manage sports venues, time slots, and bookings with availability checks and conflict prevention. This simulates a real-world sports ground/turf booking system.
- Java 17 with Spring Boot 3.2.0
- MySQL 8.0
- Docker & Docker Compose
- REST APIs (No UI)
- Venue Management: Add, list, view, and delete sports venues
- Slot Management: Add time slots per venue with overlap prevention
- Availability API: Fetch available venues for a given time range & sport
- Booking Management: Book, view, and cancel slots safely with double-booking prevention
The following assumptions are made in this system:
- Booking Model: One booking corresponds to exactly one slot (1 booking = 1 slot)
- Slot Immutability: Once a slot is booked, its time cannot be modified
- Cancellation Policy: Cancelled bookings immediately free the slot for re-booking
- Database: Single MySQL instance is used, no external caching layer
- Sports Validation: Sports must be validated against the external API (https://stapubox.com/sportslist/)
- Concurrency: Pessimistic locking is used to prevent double bookings
- Time Validation: Start time must be before end time for all slots
The system uses three main entities:
id(Primary Key)name(VARCHAR 200, NOT NULL)address(VARCHAR 500, NOT NULL)sport_code(VARCHAR 50, NOT NULL, Indexed)sport_id(INTEGER)created_at(TIMESTAMP)
id(Primary Key)venue_id(Foreign Key to Venues, Indexed)slot_date(DATE, NOT NULL, Indexed)start_time(TIME, NOT NULL, Indexed)end_time(TIME, NOT NULL, Indexed)is_available(BOOLEAN, NOT NULL, Indexed)price(DOUBLE, NOT NULL)created_at(TIMESTAMP)
id(Primary Key)slot_id(Foreign Key to Slots, UNIQUE, Indexed)customer_name(VARCHAR 200, NOT NULL)customer_email(VARCHAR 200, NOT NULL, Indexed)customer_phone(VARCHAR 20)status(ENUM: CONFIRMED, CANCELLED, Indexed)created_at(TIMESTAMP)updated_at(TIMESTAMP)
http://localhost:8080
POST /venues
Creates a new sports venue.
Request Body:
{
"name": "Football Ground A",
"address": "123 Sports Street, City",
"sportCode": "FOOTBALL",
"sportId": 1
}Response: 201 Created
{
"id": 1,
"name": "Football Ground A",
"address": "123 Sports Street, City",
"sportCode": "FOOTBALL",
"sportId": 1,
"createdAt": "2024-01-15T10:00:00"
}cURL Example:
curl -X POST http://localhost:8080/venues \
-H "Content-Type: application/json" \
-d '{
"name": "Football Ground A",
"address": "123 Sports Street, City",
"sportCode": "FOOTBALL",
"sportId": 1
}'GET /venues
Retrieves all venues.
Response: 200 OK
[
{
"id": 1,
"name": "Football Ground A",
"address": "123 Sports Street, City",
"sportCode": "FOOTBALL",
"sportId": 1,
"createdAt": "2024-01-15T10:00:00"
}
]cURL Example:
curl -X GET http://localhost:8080/venuesGET /venues/{id}
Retrieves a specific venue by ID.
Response: 200 OK
{
"id": 1,
"name": "Football Ground A",
"address": "123 Sports Street, City",
"sportCode": "FOOTBALL",
"sportId": 1,
"createdAt": "2024-01-15T10:00:00"
}cURL Example:
curl -X GET http://localhost:8080/venues/1DELETE /venues/{id}
Deletes a venue. Cannot delete venues with active bookings.
Response: 204 No Content
cURL Example:
curl -X DELETE http://localhost:8080/venues/1POST /venues/{venueId}/slots
Creates a time slot for a venue. Prevents overlapping slots.
Request Body:
{
"slotDate": "2024-01-20",
"startTime": "10:00:00",
"endTime": "11:00:00",
"price": 500.0
}Response: 201 Created
{
"id": 1,
"venueId": 1,
"slotDate": "2024-01-20",
"startTime": "10:00:00",
"endTime": "11:00:00",
"isAvailable": true,
"price": 500.0,
"createdAt": "2024-01-15T10:00:00"
}cURL Example:
curl -X POST http://localhost:8080/venues/1/slots \
-H "Content-Type: application/json" \
-d '{
"slotDate": "2024-01-20",
"startTime": "10:00:00",
"endTime": "11:00:00",
"price": 500.0
}'GET /venues/available
Fetches available venues and slots for a given time range. Optionally filters by sport code.
Query Parameters:
startDate(required): Start date (YYYY-MM-DD)startTime(required): Start time (HH:mm:ss)endDate(required): End date (YYYY-MM-DD)endTime(required): End time (HH:mm:ss)sportCode(optional): Filter by sport code
Response: 200 OK
{
"venues": [
{
"venueId": 1,
"venueName": "Football Ground A",
"address": "123 Sports Street, City",
"sportCode": "FOOTBALL",
"sportId": 1,
"availableSlots": [
{
"slotId": 1,
"slotDate": "2024-01-20",
"startTime": "10:00:00",
"endTime": "11:00:00",
"price": 500.0
}
]
}
]
}cURL Example:
curl -X GET "http://localhost:8080/venues/available?startDate=2024-01-20&startTime=09:00:00&endDate=2024-01-20&endTime=12:00:00&sportCode=FOOTBALL"POST /bookings
Creates a booking for a slot. Prevents double booking using pessimistic locking.
Request Body:
{
"slotId": 1,
"customerName": "John Doe",
"customerEmail": "john.doe@example.com",
"customerPhone": "+1234567890"
}Response: 201 Created
{
"id": 1,
"slotId": 1,
"venueId": 1,
"venueName": "Football Ground A",
"slotDate": "2024-01-20",
"startTime": "10:00:00",
"endTime": "11:00:00",
"customerName": "John Doe",
"customerEmail": "john.doe@example.com",
"customerPhone": "+1234567890",
"status": "CONFIRMED",
"price": 500.0,
"createdAt": "2024-01-15T10:00:00"
}cURL Example:
curl -X POST http://localhost:8080/bookings \
-H "Content-Type: application/json" \
-d '{
"slotId": 1,
"customerName": "John Doe",
"customerEmail": "john.doe@example.com",
"customerPhone": "+1234567890"
}'GET /bookings/{id}
Retrieves a specific booking by ID.
Response: 200 OK
{
"id": 1,
"slotId": 1,
"venueId": 1,
"venueName": "Football Ground A",
"slotDate": "2024-01-20",
"startTime": "10:00:00",
"endTime": "11:00:00",
"customerName": "John Doe",
"customerEmail": "john.doe@example.com",
"customerPhone": "+1234567890",
"status": "CONFIRMED",
"price": 500.0,
"createdAt": "2024-01-15T10:00:00"
}cURL Example:
curl -X GET http://localhost:8080/bookings/1PUT /bookings/{id}/cancel
Cancels a booking and immediately frees the slot.
Response: 200 OK
{
"id": 1,
"slotId": 1,
"venueId": 1,
"venueName": "Football Ground A",
"slotDate": "2024-01-20",
"startTime": "10:00:00",
"endTime": "11:00:00",
"customerName": "John Doe",
"customerEmail": "john.doe@example.com",
"customerPhone": "+1234567890",
"status": "CANCELLED",
"price": 500.0,
"createdAt": "2024-01-15T10:00:00"
}cURL Example:
curl -X PUT http://localhost:8080/bookings/1/cancelGET /bookings/customer/{email}
Retrieves all bookings for a customer.
Response: 200 OK
[
{
"id": 1,
"slotId": 1,
"venueId": 1,
"venueName": "Football Ground A",
"slotDate": "2024-01-20",
"startTime": "10:00:00",
"endTime": "11:00:00",
"customerName": "John Doe",
"customerEmail": "john.doe@example.com",
"customerPhone": "+1234567890",
"status": "CONFIRMED",
"price": 500.0,
"createdAt": "2024-01-15T10:00:00"
}
]cURL Example:
curl -X GET http://localhost:8080/bookings/customer/john.doe@example.comAll error responses follow this format:
{
"error": "Error message description"
}Common HTTP Status Codes:
400 Bad Request: Validation errors or invalid input404 Not Found: Resource not found500 Internal Server Error: Unexpected server errors
- Docker and Docker Compose installed
- Java 17 (for local development, optional)
-
Clone or navigate to the project directory:
cd /path/to/project -
Start the services using Docker Compose:
docker-compose up --build
This will:
- Build the Spring Boot application
- Start MySQL database
- Start the application on port 8080
-
Verify the application is running:
curl http://localhost:8080/venues
-
Stop the services:
docker-compose down
- Host:
localhost(from host machine) ormysql(from within Docker network) - Port:
3306 - Database:
sports_booking - Username:
root - Password:
rootpassword
The system integrates with the external sports API at https://stapubox.com/sportslist/ to validate sport codes and IDs. When creating a venue, the system validates that the provided sportCode and sportId exist in the external API.
The system implements several mechanisms to prevent conflicts:
-
Slot Overlap Prevention: When creating a slot, the system checks for overlapping time slots on the same date for the same venue.
-
Double Booking Prevention: Uses pessimistic locking (
PESSIMISTIC_WRITE) when booking a slot to prevent concurrent bookings of the same slot. -
Slot Immutability: Once a slot is booked, its time cannot be modified (enforced at the application level).
-
Immediate Slot Release: When a booking is cancelled, the slot is immediately marked as available.
The following indexes are created for optimal query performance:
venues.sport_code: For filtering venues by sportslots.venue_id: For finding slots by venueslots.slot_date, start_time, end_time: Composite index for date/time range queriesslots.is_available: For filtering available slotsbookings.slot_id: For finding booking by slot (unique constraint)bookings.status: For filtering bookings by statusbookings.customer_email: For finding bookings by customer
You can use the provided cURL examples above or import the following Postman collection format:
{
"info": {
"name": "Sports Venue Booking API",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Create Venue",
"request": {
"method": "POST",
"header": [{"key": "Content-Type", "value": "application/json"}],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"Football Ground A\",\n \"address\": \"123 Sports Street\",\n \"sportCode\": \"FOOTBALL\",\n \"sportId\": 1\n}"
},
"url": "http://localhost:8080/venues"
}
},
{
"name": "Get All Venues",
"request": {
"method": "GET",
"url": "http://localhost:8080/venues"
}
},
{
"name": "Create Slot",
"request": {
"method": "POST",
"header": [{"key": "Content-Type", "value": "application/json"}],
"body": {
"mode": "raw",
"raw": "{\n \"slotDate\": \"2024-01-20\",\n \"startTime\": \"10:00:00\",\n \"endTime\": \"11:00:00\",\n \"price\": 500.0\n}"
},
"url": "http://localhost:8080/venues/1/slots"
}
},
{
"name": "Get Available Venues",
"request": {
"method": "GET",
"url": {
"raw": "http://localhost:8080/venues/available?startDate=2024-01-20&startTime=09:00:00&endDate=2024-01-20&endTime=12:00:00",
"query": [
{"key": "startDate", "value": "2024-01-20"},
{"key": "startTime", "value": "09:00:00"},
{"key": "endDate", "value": "2024-01-20"},
{"key": "endTime", "value": "12:00:00"}
]
}
}
},
{
"name": "Create Booking",
"request": {
"method": "POST",
"header": [{"key": "Content-Type", "value": "application/json"}],
"body": {
"mode": "raw",
"raw": "{\n \"slotId\": 1,\n \"customerName\": \"John Doe\",\n \"customerEmail\": \"john.doe@example.com\",\n \"customerPhone\": \"+1234567890\"\n}"
},
"url": "http://localhost:8080/bookings"
}
},
{
"name": "Cancel Booking",
"request": {
"method": "PUT",
"url": "http://localhost:8080/bookings/1/cancel"
}
}
]
}.
├── src/
│ └── main/
│ ├── java/com/sportsbooking/
│ │ ├── controller/ # REST Controllers
│ │ ├── service/ # Business Logic
│ │ ├── repository/ # Data Access Layer
│ │ ├── entity/ # JPA Entities
│ │ ├── dto/ # Data Transfer Objects
│ │ ├── exception/ # Exception Handlers
│ │ └── config/ # Configuration
│ └── resources/
│ └── application.yml # Application Configuration
├── Dockerfile # Docker image definition
├── docker-compose.yml # Docker Compose configuration
├── pom.xml # Maven dependencies
└── README.md # This file
This project is created for demonstration purposes.