Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Appointment Scheduler

A backend service for managing vehicle maintenance appointments at car dealerships. Automates technician assignment, manages work shifts and leaves, enforces scheduling business rules, and handles concurrent booking safely.


overall

Since I don't use English very often in presentations, I've prepared a short summary of the main features of my project.

The project also includes several management screens, including:

Customer Management
Appointment Management
Technician Management

The main features include:

Book a single service or multiple services in one request (batch booking), with services scheduled sequentially.
Automatically select the earliest available time slot if no preferred time is provided.
Priority-based scheduling, where VIP customers are processed before normal customers.
A configurable technician scoring engine with multiple scoring criteria.
Conflict detection and distributed locking to prevent double-booking.
Suggest available time slots with guaranteed technician and service bay availability.
Manager override, including manual assignment, reassignment, locking, and unlocking assignments.
Automatically unassign appointments when a technician registers a leave request.
A background job that assigns technicians to unassigned appointments every 30 minutes, with VIP appointments processed first.
A background job that automatically cancels confirmed appointments after the configured grace period.
An SLA monitoring job that logs warnings when an appointment remains unassigned for more than 60 minutes.
A daily pricing job that calculates slot-level discounts based on service bay occupancy and automatically applies them during booking.

Tech Stack

Layer Technology
Language Java 17
Framework Spring Boot 4.1
Database PostgreSQL 16
Cache Redis 7 + Caffeine
Build Maven 3.9
Container Docker / Docker Compose

Requirements

  • Docker & Docker Compose — to run PostgreSQL and Redis
  • Java 17+ — to run the application
  • Maven 3.9+ — to build (or use the included ./mvnw)

Quick Start

Step 1 — Start PostgreSQL and Redis

docker-compose up -d

This creates:

  • PostgreSQL at localhost:5432, database appointment_db
  • Redis at localhost:6379

Step 2 — Build the project

./mvnw clean package -DskipTests

Step 3 — Run the application

java -jar target/appointment-scheduler-1.0.0.jar

App starts at http://localhost:8080. Schema and seed data are automatically created from src/main/resources/db/init.sql on first startup.


Run Everything with Docker

docker-compose up -d
docker build -t appointment-scheduler .
docker run -p 8080:8080 \
  -e DB_URL=jdbc:postgresql://host.docker.internal:5432/appointment_db \
  -e DB_USERNAME=postgres \
  -e DB_PASSWORD=postgres123 \
  -e REDIS_HOST=host.docker.internal \
  -e REDIS_PASSWORD=redis123 \
  appointment-scheduler

Environment Variables

Variable Default Description
DB_URL jdbc:postgresql://localhost:5432/appointment_db JDBC URL
DB_USERNAME postgres DB username
DB_PASSWORD postgres123 DB password
REDIS_HOST localhost Redis host
REDIS_PORT 6379 Redis port
REDIS_PASSWORD redis123 Redis password

Architecture

Controller → UseCase → DomainService → Repository (interface)
                                            ↓
                                    RepositoryImpl → JPA Entity → PostgreSQL

Following Clean Architecture:

  • domain/ — pure business logic, no framework dependency
  • application/ — use cases, controllers, DTOs
  • infrastructure/ — JPA, Redis, scheduled jobs

API Reference

Base URL: http://localhost:8080/api/v1

All responses follow this format:

{ "success": true, "data": { ... } }
{ "success": false, "message": "error detail" }

Appointments

Method Endpoint Description
POST /appointments Book one or multiple services
GET /appointments Get all (filter: ?dealershipId=&status=)
GET /appointments/{id} Get by ID
PATCH /appointments/{id}/status Update appointment status
DELETE /appointments/{id} Cancel appointment
PATCH /appointments/{id}/assign Manager force-assign a technician
PATCH /appointments/{id}/reassign Manager re-assign to another technician
PATCH /appointments/{id}/unlock-assignment Unlock so auto-job can re-assign
GET /appointments/conflicts Check technician schedule conflict
GET /appointments/technicians/{technicianId}/schedule Get technician's schedule
GET /appointments/available-slots Suggest available slots for a service

POST /appointments — Book appointment (single or batch)

serviceTypes accepts one or multiple services. For batch booking, services are scheduled sequentially — each service starts at the endTime of the previous one.

If desiredTime is omitted, the system automatically finds the earliest available slot within the next 7 days.

curl -X POST http://localhost:8080/api/v1/appointments \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "C1",
    "customerName": "John Doe",
    "customerPhone": "0901234567",
    "customerEmail": "john@example.com",
    "licensePlate": "51A-12345",
    "vehicleMake": "Toyota",
    "vehicleModel": "Camry",
    "vehicleYear": 2022,
    "serviceTypes": ["OIL_CHANGE", "TIRE_ROTATION"],
    "dealershipId": "DEALER_001",
    "desiredTime": "2026-07-30T09:00:00"
  }'

Response:

{
  "success": true,
  "data": {
    "customerId": "C1",
    "customerName": "John Doe",
    "licensePlate": "51A-12345",
    "dealershipId": "DEALER_001",
    "services": [
      {
        "appointmentId": "...",
        "serviceType": "OIL_CHANGE",
        "startTime": "2025-12-01T09:00:00",
        "endTime": "2025-12-01T09:30:00",
        "technicianName": "Charlie",
        "bayName": "BAY-1",
        "estimatedPrice": 50.00
      },
      {
        "appointmentId": "...",
        "serviceType": "TIRE_ROTATION",
        "startTime": "2025-12-01T09:30:00",
        "endTime": "2025-12-01T10:30:00",
        "technicianName": "Bob",
        "bayName": "BAY-2",
        "estimatedPrice": 40.00
      }
    ]
  }
}

GET /appointments — Get all appointments

# All appointments
curl http://localhost:8080/api/v1/appointments

# Filter by dealership
curl "http://localhost:8080/api/v1/appointments?dealershipId=DEALER_001"

# Filter by status
curl "http://localhost:8080/api/v1/appointments?dealershipId=DEALER_001&status=INIT"

Results are always sorted priority DESC, startTime ASC.


GET /appointments/{id} — Get by ID

curl http://localhost:8080/api/v1/appointments/{id}

PATCH /appointments/{id}/status — Update status

Must send the current version to prevent concurrent update conflicts.

# Confirm appointment
curl -X PATCH http://localhost:8080/api/v1/appointments/{id}/status \
  -H "Content-Type: application/json" \
  -d '{ "status": "CONFIRMED", "version": 0 }'

# Mark in progress
curl -X PATCH http://localhost:8080/api/v1/appointments/{id}/status \
  -H "Content-Type: application/json" \
  -d '{ "status": "IN_PROGRESS", "version": 1 }'

# Complete with final price
curl -X PATCH http://localhost:8080/api/v1/appointments/{id}/status \
  -H "Content-Type: application/json" \
  -d '{ "status": "COMPLETED", "version": 2, "finalPrice": 500000 }'

Valid status transitions:

INIT → CONFIRMED → IN_PROGRESS → COMPLETED
INIT → CANCELLED
CONFIRMED → CANCELLED

DELETE /appointments/{id} — Cancel appointment

curl -X DELETE "http://localhost:8080/api/v1/appointments/{id}?version=0&reason=Customer+request"

PATCH /appointments/{id}/assign — Force assign technician

Manager can override any existing assignment. lock: true prevents auto-job from overwriting.

curl -X PATCH http://localhost:8080/api/v1/appointments/{id}/assign \
  -H "Content-Type: application/json" \
  -d '{ "technicianId": "T1", "note": "VIP customer request", "lock": true }'

PATCH /appointments/{id}/reassign — Re-assign to another technician

No need to unassign first.

curl -X PATCH http://localhost:8080/api/v1/appointments/{id}/reassign \
  -H "Content-Type: application/json" \
  -d '{ "technicianId": "T2", "note": "T1 called in sick" }'

PATCH /appointments/{id}/unlock-assignment — Unlock assignment

Returns scheduling control back to the auto-assignment job.

curl -X PATCH http://localhost:8080/api/v1/appointments/{id}/unlock-assignment

GET /appointments/conflicts — Check schedule conflict

Returns true if there is a conflict, false otherwise.

curl "http://localhost:8080/api/v1/appointments/conflicts?technicianId=T1&dealershipId=DEALER_001&start=2025-12-01T09:00:00&end=2025-12-01T10:00:00"

GET /appointments/technicians/{technicianId}/schedule — Get technician schedule

curl "http://localhost:8080/api/v1/appointments/technicians/T1/schedule?dealershipId=DEALER_001"

GET /appointments/available-slots — Suggest available slots

Returns slots where both a bay and a technician are confirmed available (shift, leave, workload cap, and conflict all checked). Steps through the range in 30-minute increments.

curl "http://localhost:8080/api/v1/appointments/available-slots?dealershipId=DEALER_001&serviceType=OIL_CHANGE&from=2025-12-01T08:00:00&searchDays=3&maxResults=5"

Response:

{
  "success": true,
  "data": [
    {
      "startTime": "2025-12-01T09:00:00",
      "endTime": "2025-12-01T09:30:00",
      "technicianId": "T3",
      "technicianName": "Charlie",
      "technicianLevel": "JUNIOR",
      "bayId": "BAY-1"
    }
  ]
}

Technicians

Method Endpoint Description
POST /technicians Create a technician
GET /technicians Get all (filter: ?dealershipId=)
GET /technicians/{id} Get by ID
PUT /technicians/{id} Update technician info
PATCH /technicians/{id}/deactivate Deactivate technician
DELETE /technicians/{id} Delete (only if no appointment history)

Valid levels: JUNIOR | SENIOR | EXPERT


POST /technicians — Create technician

curl -X POST http://localhost:8080/api/v1/technicians \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Alice",
    "phone": "0901111111",
    "email": "alice@dealer.com",
    "yearsOfExperience": 8,
    "level": "EXPERT",
    "status": "ACTIVE",
    "dealershipId": "DEALER_001",
    "qualifications": ["ENGINE_REPAIR", "FULL_SERVICE", "BRAKE_INSPECTION"]
  }'

GET /technicians — Get all technicians

# All technicians
curl http://localhost:8080/api/v1/technicians

# Filter by dealership
curl "http://localhost:8080/api/v1/technicians?dealershipId=DEALER_001"

GET /technicians/{id} — Get by ID

curl http://localhost:8080/api/v1/technicians/T1

PUT /technicians/{id} — Update technician

curl -X PUT http://localhost:8080/api/v1/technicians/T1 \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Alice Updated",
    "phone": "0901111111",
    "email": "alice@dealer.com",
    "yearsOfExperience": 9,
    "level": "EXPERT",
    "status": "ACTIVE",
    "dealershipId": "DEALER_001",
    "qualifications": ["ENGINE_REPAIR", "FULL_SERVICE", "BRAKE_INSPECTION"]
  }'

PATCH /technicians/{id}/deactivate — Deactivate technician

curl -X PATCH http://localhost:8080/api/v1/technicians/T1/deactivate

DELETE /technicians/{id} — Delete technician

Only allowed if the technician has no appointment history.

curl -X DELETE http://localhost:8080/api/v1/technicians/T1

Technician Shifts

Method Endpoint Description
POST /technicians/shifts Create a shift
GET /technicians/{technicianId}/shifts Get all shifts for a technician
DELETE /technicians/shifts/{shiftId} Delete a shift

POST /technicians/shifts — Create shift

curl -X POST http://localhost:8080/api/v1/technicians/shifts \
  -H "Content-Type: application/json" \
  -d '{
    "technicianId": "T1",
    "date": "2025-12-01",
    "startTime": "08:00:00",
    "endTime": "17:00:00"
  }'

GET /technicians/{technicianId}/shifts — Get shifts

curl http://localhost:8080/api/v1/technicians/T1/shifts

DELETE /technicians/shifts/{shiftId} — Delete shift

curl -X DELETE http://localhost:8080/api/v1/technicians/shifts/S1

Technician Leaves

Method Endpoint Description
POST /technicians/leaves Register a leave
GET /technicians/{technicianId}/leaves Get all leaves for a technician
DELETE /technicians/leaves/{leaveId} Delete a leave record

When a leave is added, the system automatically unassigns all affected appointments for that technician on that day and resets them to INIT for the auto-job to re-assign.

Valid leave types: ANNUAL_LEAVE | SICK_LEAVE | PUBLIC_HOLIDAY | BREAK

startTime/endTime = null means full-day leave. Providing values means partial leave (used for BREAK).


POST /technicians/leaves — Register leave

# Full-day sick leave
curl -X POST http://localhost:8080/api/v1/technicians/leaves \
  -H "Content-Type: application/json" \
  -d '{
    "technicianId": "T1",
    "leaveType": "SICK_LEAVE",
    "date": "2025-12-01",
    "startTime": null,
    "endTime": null
  }'

# Partial leave (break)
curl -X POST http://localhost:8080/api/v1/technicians/leaves \
  -H "Content-Type: application/json" \
  -d '{
    "technicianId": "T1",
    "leaveType": "BREAK",
    "date": "2025-12-01",
    "startTime": "12:00:00",
    "endTime": "13:00:00"
  }'

GET /technicians/{technicianId}/leaves — Get leaves

curl http://localhost:8080/api/v1/technicians/T1/leaves

DELETE /technicians/leaves/{leaveId} — Delete leave

curl -X DELETE http://localhost:8080/api/v1/technicians/leaves/{leaveId}

Skill Matrix

Method Endpoint Description
GET /technicians/skill-matrix?dealershipId= Technicians and their qualifications
GET /technicians/skill-matrix/by-service?dealershipId= Matrix grouped by service type

GET /technicians/skill-matrix

curl "http://localhost:8080/api/v1/technicians/skill-matrix?dealershipId=DEALER_001"

GET /technicians/skill-matrix/by-service

curl "http://localhost:8080/api/v1/technicians/skill-matrix/by-service?dealershipId=DEALER_001"

Response:

{
  "success": true,
  "data": {
    "OIL_CHANGE":       ["Charlie"],
    "TIRE_ROTATION":    ["Bob", "Charlie"],
    "BRAKE_INSPECTION": ["Alice", "Bob"],
    "ENGINE_REPAIR":    ["Alice"],
    "FULL_SERVICE":     ["Alice", "Bob"]
  }
}

Service Config

Method Endpoint Description
POST /service-configs Create a config
PUT /service-configs Update a config
GET /service-configs Get all configs
GET /service-configs/lookup?serviceType=&dealershipId= Lookup by service type
DELETE /service-configs?serviceType=&dealershipId= Deactivate a config

Valid service types: OIL_CHANGE | TIRE_ROTATION | BRAKE_INSPECTION | ENGINE_REPAIR | FULL_SERVICE

durationHours and durationMinutes are summed for total duration. At least one must be > 0.


POST /service-configs — Create config

curl -X POST http://localhost:8080/api/v1/service-configs \
  -H "Content-Type: application/json" \
  -d '{
    "serviceType": "OIL_CHANGE",
    "dealershipId": "DEALER_001",
    "durationHours": 0,
    "durationMinutes": 30,
    "autoCancelMinutes": 15,
    "estimatedPrice": 50.00
  }'

Response includes totalDurationMinutes for client-side ETA calculation:

{
  "serviceType": "OIL_CHANGE",
  "durationHours": 0,
  "durationMinutes": 30,
  "totalDurationMinutes": 30,
  "autoCancelMinutes": 15,
  "estimatedPrice": 50.00
}

PUT /service-configs — Update config

curl -X PUT http://localhost:8080/api/v1/service-configs \
  -H "Content-Type: application/json" \
  -d '{
    "serviceType": "OIL_CHANGE",
    "dealershipId": "DEALER_001",
    "durationHours": 0,
    "durationMinutes": 45,
    "autoCancelMinutes": 15,
    "estimatedPrice": 55.00
  }'

GET /service-configs — Get all configs

curl http://localhost:8080/api/v1/service-configs

GET /service-configs/lookup — Lookup by service type

curl "http://localhost:8080/api/v1/service-configs/lookup?serviceType=OIL_CHANGE&dealershipId=DEALER_001"

DELETE /service-configs — Deactivate config

curl -X DELETE "http://localhost:8080/api/v1/service-configs?serviceType=OIL_CHANGE&dealershipId=DEALER_001"

Customers

Method Endpoint Description
GET /customers Get all customers
GET /customers/{id} Get by ID
GET /customers/phone/{phone} Find by phone number
PATCH /customers/{id}/tier?tier=VIP Update customer tier

Valid tiers: VIP | NORMAL

Tier directly affects appointment priority at booking time (VIP=3, NORMAL=1).


GET /customers — Get all customers

curl http://localhost:8080/api/v1/customers

GET /customers/{id} — Get by ID

curl http://localhost:8080/api/v1/customers/C1

GET /customers/phone/{phone} — Find by phone

curl http://localhost:8080/api/v1/customers/phone/0901234567

PATCH /customers/{id}/tier — Update tier

curl -X PATCH "http://localhost:8080/api/v1/customers/C1/tier?tier=VIP"

Scheduled Jobs

Job Interval Description
TechnicianAssignmentJob 30 min Assigns technicians to unassigned INIT appointments, skips locked
AutoCancelAppointmentJob 5 min Cancels CONFIRMED appointments past startTime + autoCancelMinutes
SlaMonitoringJob 5 min Logs WARN when an appointment has been unassigned for > 60 min
AvailabilityStatsJob Configurable Computes availability statistics

SLA threshold is configurable via scheduler.sla.unassigned-threshold-minutes (default: 60).


Key Features

Automatic Technician Assignment Job runs every 30 minutes (and immediately on booking), finds the best technician using a 6-criteria scoring engine:

  • Workload (50%) — prefer less busy technicians
  • Specialization (30%) — prefer specialists for the service
  • Level (30%) — match service complexity to technician level
  • Experience (20%) — years of experience
  • Availability (20%) — remaining daily capacity
  • Priority (10%) — boost for VIP appointments

Weights are configurable per dealership in the scoring_weight_config table.

Hard Cap Workload Each technician is capped at 6 jobs/day. Technicians at the cap are excluded from candidates regardless of score.

Optimistic Locking Every status update requires the current version. Concurrent updates result in 409 Conflict.

Distributed Lock Redis locks prevent double-booking when multiple requests arrive simultaneously. Lock TTL is 30 seconds, auto-released after booking completes.

Auto Cancel Job runs every 5 minutes, automatically cancels CONFIRMED appointments that have passed startTime + autoCancelMinutes.

Manual Override Managers can force-assign any technician, set lock=true to prevent auto-job overrides, re-assign directly, or unlock to return control to the auto-job.

Batch Booking POST /appointments accepts serviceTypes as an array. Services are scheduled sequentially — each starts at the endTime of the previous. Each service independently finds the best available bay and technician.


Database Schema

Table Description
customers Customer info with tier (VIP/NORMAL)
vehicles Customer vehicles (PK: license_plate)
dealerships Dealership info with business hours
service_bays Physical service bays at a dealership
technicians Technician records
technician_qualifications Service types a technician is qualified for
technician_shifts Work shifts by date
technician_leaves Leave records (annual, sick, holiday, break)
appointments Appointments with priority, assignment_locked, version
service_config Service duration, price, and auto-cancel config
scoring_weight_config Scoring weights per dealership

Seed Data

Available after first startup:

Type Data
Dealership DEALER_001 — Main Dealership (08:00–18:00)
Technicians T1 Alice (EXPERT), T2 Bob (SENIOR), T3 Charlie (JUNIOR)
Customers C1 John Doe (VIP), C2 Jane Smith (NORMAL)
Vehicles 51A-12345 Toyota Camry (C1), 51B-67890 Honda Civic (C2)
Service Bays BAY-1, BAY-2 at DEALER_001
Service Configs 5 service types with realistic durations (dealership DEFAULT)
Shifts T1, T2, T3 have shifts for today through the next 6 days (08:00–17:00)

Default service configs:

Service Duration Estimated Price
OIL_CHANGE 30 min $50
TIRE_ROTATION 60 min $40
BRAKE_INSPECTION 105 min $80
ENGINE_REPAIR 240 min $300
FULL_SERVICE 180 min $150

Default qualifications:

Technician Qualified Services
T1 Alice ENGINE_REPAIR, FULL_SERVICE, BRAKE_INSPECTION
T2 Bob BRAKE_INSPECTION, FULL_SERVICE, TIRE_ROTATION
T3 Charlie OIL_CHANGE, TIRE_ROTATION

HTTP Status Codes

Code Meaning
200 Success
201 Created successfully
400 Bad request (validation failure, missing parameter)
404 Resource not found
409 Conflict (business rule violation, version mismatch, or scheduling conflict)
500 Internal server error

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages