A production-grade RESTful API built with Spring Boot 3.x for creating events and managing seat bookings.
| Layer | Technology |
|---|---|
| Runtime | Java 17 |
| Framework | Spring Boot 3.2 |
| Build tool | Maven |
| Database | PostgreSQL 15+ |
| Migrations | Flyway |
| Cache / Idempotency | Redis 7+ |
| ORM | Spring Data JPA / Hibernate |
| Validation | Jakarta Bean Validation |
| API Docs | Springdoc OpenAPI 2 (Swagger UI) |
event-api/
├── src/main/java/com/digicore/eventapi/
│ ├── EventApiApplication.java
│ ├── archive/ # Soft-delete abstraction
│ │ ├── SoftDeletable.java # Interface — marks entities as soft-deletable
│ │ └── ArchiveService.java # stamps deletedAt, never issues physical DELETE
│ ├── config/
│ │ ├── RedisConfig.java # StringRedisTemplate + ObjectMapper beans
│ │ └── SwaggerConfig.java # OpenAPI metadata
│ ├── controllers/
│ │ ├── EventController.java
│ │ └── BookingController.java
│ ├── dto/
│ │ ├── request/
│ │ │ ├── CreateEventRequest.java
│ │ │ └── CreateBookingRequest.java
│ │ └── response/
│ │ ├── ApiResponse.java # Uniform envelope for all responses
│ │ ├── EventResponse.java
│ │ └── BookingResponse.java
│ ├── exception/
│ │ ├── GlobalExceptionHandler.java
│ │ ├── ResourceNotFoundException.java
│ │ └── BusinessException.java
│ ├── models/
│ │ ├── enums/EventStatus.java
│ │ ├── Event.java # @Version for optimistic locking
│ │ └── Booking.java
│ ├── repositories/
│ │ ├── EventRepository.java # PESSIMISTIC_WRITE query for booking
│ │ └── BookingRepository.java
│ ├── services/
│ │ ├── EventService.java
│ │ └── BookingService.java
│ └── utils/
│ ├── IdempotencyService.java # Redis-backed X-Idempotency-Key support
│ └── EventCacheService.java # Redis cache for GET /events/{id}
└── src/main/resources/
├── application.yml
└── db/migration/
├── V1__create_events_table.sql
└── V2__create_bookings_table.sql
- Java 17+
- Maven 3.8+
- PostgreSQL 15 running on
localhost:5432 - Redis 7 running on
localhost:6379
cd event-api
mvn clean package -DskipTestsCREATE DATABASE eventdb;
-- Default credentials assumed: postgres / postgres
-- Update src/main/resources/application.yml if yours differFlyway runs migrations automatically on startup — no manual SQL needed.
mvn spring-boot:runOr with the packaged JAR:
java -jar target/event-api-1.0.0.jarThe server starts on http://localhost:8080.
Open your browser:
http://localhost:8080/swagger-ui.html
Raw OpenAPI spec:
http://localhost:8080/v3/api-docs
| Method | Path | Description |
|---|---|---|
POST |
/events |
Create a new event |
GET |
/events |
List all events (paginated) |
GET |
/events/{id} |
Get event by ID |
| Method | Path | Description |
|---|---|---|
POST |
/events/{id}/bookings |
Book a seat |
DELETE |
/bookings/{id} |
Cancel a booking |
GET |
/events/{id}/bookings |
List bookings for an event |
| Param | Default | Description |
|---|---|---|
page |
0 |
Zero-based page number |
size |
10 |
Items per page |
sortBy |
eventDate |
Sort field |
direction |
asc |
asc or desc |
To prevent duplicate submissions (network retries, double-clicks), include a unique key on booking requests:
POST /events/{id}/bookings
X-Idempotency-Key: my-unique-request-id-abc123
Content-Type: application/json
{
"attendeeName": "John Doe",
"attendeeEmail": "john@example.com"
}Sending the same key again within 24 hours returns the original response without re-processing.
| Rule | Enforcement |
|---|---|
| Event date must be in the future | @Future on DTO + DB constraint |
totalSeats must be > 0 |
@Min(1) on DTO + DB check constraint |
Cannot book a CLOSED event |
Service layer check |
Cannot book when bookedSeats >= totalSeats |
Service layer check (under DB lock) |
| Duplicate email per event is rejected | existsActiveBooking query + partial unique index |
| Cancellation frees up a seat | Decrements bookedSeats, re-opens if CLOSED |
| Event auto-closes when fully booked | autoCloseIfFull() called after every booking |
Cancelled bookings and deleted events are never physically removed. Instead, a deleted_at timestamp is stamped via ArchiveService. All repository queries include a WHERE deleted_at IS NULL filter so archived records are invisible to normal operations but remain for audit/compliance.
mvn testUnit tests cover:
EventService— create, get (cache hit/miss), list, auto-closeBookingService— create, reject closed/full/duplicate, cancel, re-open on cancel
- Soft delete only — no hard deletes anywhere;
deletedAtis the single source of truth for archival state. - Email normalisation — emails are lower-cased and trimmed before storage and duplicate checks.
- Partial unique index in PostgreSQL (
WHERE deleted_at IS NULL) means a cancelled email can re-book the same event. - Redis is optional for startup — if Redis is unavailable, the application will fail fast at startup. For a more resilient setup, configure a fallback or make Redis optional with
@ConditionalOnBean. - No authentication — out of scope per the assignment; production would add Spring Security with JWT.
bookedSeatsis denormalised on theeventsrow (rather than a liveCOUNT(bookings)) for O(1) capacity checks under lock.