A high-performance Spring Boot application that solves the Vehicle Routing Problem (VRP) by optimally allocating delivery orders to a fleet of vehicles. It uses a custom Greedy Nearest-Neighbor Algorithm with the Haversine Formula to minimize total travel distance while strictly respecting priority tiers and vehicle capacity constraints.
- Features
- Technology Stack
- Project Structure
- How It Works
- Getting Started
- API Documentation
- Sample Inputs
- H2 Database Console
- Running Tests
- Error Handling
- Key Design Decisions
| Feature | Description |
|---|---|
| Priority-Based Scheduling | Orders sorted HIGH β MEDIUM β LOW; within the same priority, heavier orders are assigned first |
| Capacity Enforcement | Vehicle weight limits are strictly never exceeded during assignment |
| Greedy Nearest-Neighbor | Each order is assigned to the closest eligible vehicle using the Haversine distance |
| Cumulative Routing | After each assignment, a vehicle's position updates to the delivery location, simulating real multi-stop routing |
| Batch Upsert Operations | Uses findAllByOrderIdIn / findAllByVehicleIdIn for a single bulk fetch then saveAll β zero N+1 queries |
| Graceful Overflow Handling | Unassignable orders are collected by ID in unassignedOrders and returned in the response |
| H2 Web Console | Built-in browser UI to inspect live DB state at /h2-console during development |
| SQL Logging | spring.jpa.show-sql=true prints every Hibernate query to the console for easy debugging |
| Clean Architecture | Controller β Service β Repository layers with strict DTO separation |
| Comprehensive Tests | Unit tests (Mockito) + Integration tests (MockMvc) covering all critical paths |
| Layer | Technology | Version |
|---|---|---|
| Language | Java | 17 |
| Framework | Spring Boot (Web, Data JPA, Validation) | 3.5.10 |
| Database | H2 In-Memory | Runtime |
| Build Tool | Maven Wrapper | 3.9.12 |
| Utilities | Lombok | Managed by Spring Boot BOM |
| Testing | JUnit 5, Mockito, MockMvc | Managed by Spring Boot BOM |
shendeyogesh11-load-balancer/
βββ pom.xml
βββ mvnw / mvnw.cmd # Maven wrapper scripts
βββ .mvn/wrapper/maven-wrapper.properties
βββ src/
βββ main/
β βββ java/com/dispatch/loadbalancer/
β β βββ DispatchLoadBalancerApplication.java # Spring Boot entry point
β β βββ controller/
β β β βββ DispatchController.java # REST endpoints
β β βββ service/
β β β βββ DispatchService.java # Batch upsert + optimization logic
β β βββ repository/
β β β βββ OrderRepository.java # JPA + bulk finders
β β β βββ VehicleRepository.java # JPA + bulk finders
β β βββ model/
β β β βββ Order.java # JPA Entity (@Table name: "orders")
β β β βββ Vehicle.java # JPA Entity (@Table name: "vehicles")
β β β βββ Priority.java # Enum: HIGH, MEDIUM, LOW
β β βββ dto/
β β β βββ DispatchPlanResponse.java # Response DTO with nested static classes
β β βββ util/
β β β βββ DistanceCalculator.java # Haversine formula (@Component)
β β βββ exception/
β β βββ GlobalExceptionHandler.java # @RestControllerAdvice
β βββ resources/
β βββ application.properties
βββ test/
βββ java/com/dispatch/loadbalancer/
βββ DispatchLoadBalancerApplicationTests.java # Context load smoke test
βββ DispatchServiceTest.java # Unit tests (Mockito)
βββ DispatchIntegrationTest.java # End-to-end API tests (MockMvc)
βββ DistanceCalculatorTest.java # Additional unit test scenarios
When vehicles or orders are POST-ed, the service executes a two-query upsert that scales safely to large payloads:
- Fetch all existing records matching the incoming IDs in one query (
findAllByOrderIdIn/findAllByVehicleIdIn). - Build an
O(1)lookup map from the results. - Merge: update fields on existing entities, queue new ones for insert.
- Persist everything in one batch via
saveAll.
All orders are sorted before assignment using a two-level comparator:
Primary: Priority rank HIGH (1) β MEDIUM (2) β LOW (3)
Secondary: Package weight DESCENDING (heavier packages in the same tier go first)
The secondary sort ensures that bulkier items β which are harder to fit as capacity depletes β are placed early, maximising overall fleet utilisation.
For each order (in sorted order), the algorithm runs inside DispatchService.generateDispatchPlan():
- Scan all vehicles that have sufficient remaining capacity for the order's weight.
- Among eligible vehicles, pick the one with the smallest Haversine distance from its current position to the order's coordinates.
- Assign the order β add to
assignedOrders, incrementcurrentLoad, advance the vehicle's current position to the delivery coordinate, and accumulatetotalDistanceKm. - If no vehicle can carry the order, add its ID to
unassignedOrders.
The position-update after each stop is handled by the private VehicleSimulationState inner class, keeping all mutable routing state separate from the immutable JPA entities.
Implemented in DistanceCalculator (Earth radius = 6,371 km):
Ξlat = latβ β latβ (radians)
Ξlon = lonβ β lonβ (radians)
a = sinΒ²(Ξlat/2) + cos(latβ) Β· cos(latβ) Β· sinΒ²(Ξlon/2)
c = 2 Β· atan2(βa, β(1βa))
d = 6371 Β· c km
Distance appears in responses formatted as "%.2f km" (e.g., "5.30 km").
- JDK 17 or higher
- No separate Maven installation needed β the bundled
mvnwwrapper downloads Maven 3.9.12 automatically on first run.
cd path\to\DispatchLoadBalancer
:: Using the included wrapper (recommended β no Maven required)
mvnw.cmd spring-boot:run
:: Or if Maven is installed globally
mvn spring-boot:runcd path/to/DispatchLoadBalancer
# Grant execute permission to the wrapper (first time only)
chmod +x mvnw
# Run with the wrapper (recommended β no Maven required)
./mvnw spring-boot:run
# Or if Maven is installed globally
mvn spring-boot:runβ The server starts at
http://localhost:8080
http://localhost:8080/api/dispatch
| Method | Endpoint | Description |
|---|---|---|
POST |
/vehicles |
Register or update the vehicle fleet |
POST |
/orders |
Submit delivery orders for processing |
GET |
/plan |
Trigger optimization and retrieve the dispatch plan |
Registers or updates vehicles via batch upsert β re-submitting an existing vehicleId updates it in-place.
- URL:
POST /api/dispatch/vehicles - Content-Type:
application/json
Request Body:
{
"vehicles": [
{
"vehicleId": "VEH001",
"capacity": 100,
"currentLatitude": 28.7041,
"currentLongitude": 77.1025,
"currentAddress": "Karol Bagh, Delhi, India"
},
{
"vehicleId": "VEH002",
"capacity": 80,
"currentLatitude": 28.5355,
"currentLongitude": 77.3910,
"currentAddress": "Sector 18, Noida, Uttar Pradesh, India"
}
]
}Response 200 OK:
{
"message": "Vehicle details accepted.",
"status": "success"
}Response 400 Bad Request (empty or missing vehicles key):
{
"message": "No vehicles provided"
}Submits orders via batch upsert. priority must be one of HIGH, MEDIUM, or LOW.
- URL:
POST /api/dispatch/orders - Content-Type:
application/json
Request Body:
{
"orders": [
{
"orderId": "ORD001",
"latitude": 28.6139,
"longitude": 77.2090,
"address": "Connaught Place, Delhi, India",
"packageWeight": 15,
"priority": "HIGH"
},
{
"orderId": "ORD002",
"latitude": 28.4595,
"longitude": 77.0266,
"address": "Cyber Hub, Gurgaon, Haryana, India",
"packageWeight": 30,
"priority": "MEDIUM"
}
]
}Response 200 OK:
{
"message": "Delivery orders accepted.",
"status": "success"
}Response 400 Bad Request (empty or missing orders key):
{
"message": "No orders provided"
}Runs the full optimization algorithm over all stored vehicles and pending orders.
- URL:
GET /api/dispatch/plan
Response 200 OK:
{
"dispatchPlan": [
{
"vehicleId": "VEH001",
"totalLoad": 45,
"totalDistance": "5.30 km",
"assignedOrders": [
{
"orderId": "ORD001",
"latitude": 28.6139,
"longitude": 77.2090,
"address": "Connaught Place, Delhi, India",
"packageWeight": 15,
"priority": "HIGH"
},
{
"orderId": "ORD002",
"latitude": 28.4595,
"longitude": 77.0266,
"address": "Cyber Hub, Gurgaon, Haryana, India",
"packageWeight": 30,
"priority": "MEDIUM"
}
]
}
],
"unassignedOrders": ["ORD015", "ORD022"]
}
unassignedOrdersis a flat array of order ID strings (not full objects). It will be[]when all orders were successfully assigned.
totalDistanceis the vehicle's cumulative travel distance across all its stops, formatted to 2 decimal places (e.g.,"12.47 km").
A full 30-order / 5-vehicle dataset to validate the end-to-end flow.
βΆ Click to expand β 30 Orders
{
"orders": [
{ "orderId": "ORD001", "latitude": 28.6139, "longitude": 77.2090, "address": "Connaught Place, Delhi, India", "packageWeight": 15, "priority": "HIGH" },
{ "orderId": "ORD002", "latitude": 28.6139, "longitude": 77.2090, "address": "Connaught Place, Delhi, India", "packageWeight": 10, "priority": "MEDIUM" },
{ "orderId": "ORD003", "latitude": 28.7041, "longitude": 77.1025, "address": "Karol Bagh, Delhi, India", "packageWeight": 20, "priority": "LOW" },
{ "orderId": "ORD004", "latitude": 28.5355, "longitude": 77.3910, "address": "Sector 18, Noida, Uttar Pradesh, India", "packageWeight": 25, "priority": "HIGH" },
{ "orderId": "ORD005", "latitude": 28.4595, "longitude": 77.0266, "address": "Cyber Hub, Gurgaon, Haryana, India", "packageWeight": 30, "priority": "MEDIUM" },
{ "orderId": "ORD006", "latitude": 28.6139, "longitude": 77.2090, "address": "Connaught Place, Delhi, India", "packageWeight": 40, "priority": "LOW" },
{ "orderId": "ORD007", "latitude": 28.7041, "longitude": 77.1025, "address": "Karol Bagh, Delhi, India", "packageWeight": 20, "priority": "HIGH" },
{ "orderId": "ORD008", "latitude": 28.5355, "longitude": 77.3910, "address": "Sector 18, Noida, Uttar Pradesh, India", "packageWeight": 25, "priority": "HIGH" },
{ "orderId": "ORD009", "latitude": 28.4595, "longitude": 77.0266, "address": "Cyber Hub, Gurgaon, Haryana, India", "packageWeight": 15, "priority": "MEDIUM" },
{ "orderId": "ORD010", "latitude": 28.6139, "longitude": 77.2090, "address": "Connaught Place, Delhi, India", "packageWeight": 30, "priority": "LOW" },
{ "orderId": "ORD011", "latitude": 28.7041, "longitude": 77.1025, "address": "Karol Bagh, Delhi, India", "packageWeight": 20, "priority": "HIGH" },
{ "orderId": "ORD012", "latitude": 28.5355, "longitude": 77.3910, "address": "Sector 18, Noida, Uttar Pradesh, India", "packageWeight": 10, "priority": "MEDIUM" },
{ "orderId": "ORD013", "latitude": 28.4595, "longitude": 77.0266, "address": "Cyber Hub, Gurgaon, Haryana, India", "packageWeight": 25, "priority": "HIGH" },
{ "orderId": "ORD014", "latitude": 28.6139, "longitude": 77.2090, "address": "Connaught Place, Delhi, India", "packageWeight": 15, "priority": "LOW" },
{ "orderId": "ORD015", "latitude": 28.7041, "longitude": 77.1025, "address": "Karol Bagh, Delhi, India", "packageWeight": 20, "priority": "HIGH" },
{ "orderId": "ORD016", "latitude": 28.5355, "longitude": 77.3910, "address": "Sector 18, Noida, Uttar Pradesh, India", "packageWeight": 30, "priority": "LOW" },
{ "orderId": "ORD017", "latitude": 28.4595, "longitude": 77.0266, "address": "Cyber Hub, Gurgaon, Haryana, India", "packageWeight": 15, "priority": "MEDIUM" },
{ "orderId": "ORD018", "latitude": 28.6139, "longitude": 77.2090, "address": "Connaught Place, Delhi, India", "packageWeight": 10, "priority": "HIGH" },
{ "orderId": "ORD019", "latitude": 28.7041, "longitude": 77.1025, "address": "Karol Bagh, Delhi, India", "packageWeight": 20, "priority": "LOW" },
{ "orderId": "ORD020", "latitude": 28.5355, "longitude": 77.3910, "address": "Sector 18, Noida, Uttar Pradesh, India", "packageWeight": 30, "priority": "HIGH" },
{ "orderId": "ORD021", "latitude": 28.6139, "longitude": 77.2090, "address": "Connaught Place, Delhi, India", "packageWeight": 25, "priority": "MEDIUM" },
{ "orderId": "ORD022", "latitude": 28.7041, "longitude": 77.1025, "address": "Karol Bagh, Delhi, India", "packageWeight": 20, "priority": "LOW" },
{ "orderId": "ORD023", "latitude": 28.5355, "longitude": 77.3910, "address": "Sector 18, Noida, Uttar Pradesh, India", "packageWeight": 30, "priority": "HIGH" },
{ "orderId": "ORD024", "latitude": 28.4595, "longitude": 77.0266, "address": "Cyber Hub, Gurgaon, Haryana, India", "packageWeight": 15, "priority": "LOW" },
{ "orderId": "ORD025", "latitude": 28.6139, "longitude": 77.2090, "address": "Connaught Place, Delhi, India", "packageWeight": 20, "priority": "MEDIUM" },
{ "orderId": "ORD026", "latitude": 28.7041, "longitude": 77.1025, "address": "Karol Bagh, Delhi, India", "packageWeight": 25, "priority": "HIGH" },
{ "orderId": "ORD027", "latitude": 28.5355, "longitude": 77.3910, "address": "Sector 18, Noida, Uttar Pradesh, India", "packageWeight": 20, "priority": "MEDIUM" },
{ "orderId": "ORD028", "latitude": 28.4595, "longitude": 77.0266, "address": "Cyber Hub, Gurgaon, Haryana, India", "packageWeight": 15, "priority": "LOW" },
{ "orderId": "ORD029", "latitude": 28.6139, "longitude": 77.2090, "address": "Connaught Place, Delhi, India", "packageWeight": 30, "priority": "HIGH" },
{ "orderId": "ORD030", "latitude": 28.7041, "longitude": 77.1025, "address": "Karol Bagh, Delhi, India", "packageWeight": 20, "priority": "LOW" }
]
}βΆ Click to expand β 5 Vehicles
{
"vehicles": [
{ "vehicleId": "VEH001", "capacity": 100, "currentLatitude": 28.7041, "currentLongitude": 77.1025, "currentAddress": "Karol Bagh, Delhi, India" },
{ "vehicleId": "VEH002", "capacity": 80, "currentLatitude": 28.5355, "currentLongitude": 77.3910, "currentAddress": "Sector 18, Noida, Uttar Pradesh, India" },
{ "vehicleId": "VEH003", "capacity": 120, "currentLatitude": 28.4595, "currentLongitude": 77.0266, "currentAddress": "Cyber Hub, Gurgaon, Haryana, India" },
{ "vehicleId": "VEH004", "capacity": 90, "currentLatitude": 28.6139, "currentLongitude": 77.2090, "currentAddress": "Connaught Place, Delhi, India" },
{ "vehicleId": "VEH005", "capacity": 110, "currentLatitude": 28.7041, "currentLongitude": 77.1025, "currentAddress": "Karol Bagh, Delhi, India" }
]
}The H2 web console is enabled and accessible while the app is running:
| Setting | Value |
|---|---|
| URL | http://localhost:8080/h2-console |
| JDBC URL | jdbc:h2:mem:testdb |
| Username | sa |
| Password | password |
Use it to run ad-hoc SQL against the ORDERS and VEHICLES tables during development.
β οΈ H2 is an in-memory database. All data is cleared when the application restarts. Re-upload vehicles and orders after each restart.
# macOS / Linux
./mvnw test
# Windows
mvnw.cmd test| Test Class | Type | What It Verifies |
|---|---|---|
DispatchLoadBalancerApplicationTests |
Smoke | Spring application context loads without errors |
DispatchServiceTest |
Unit (Mockito) | Priority sorting, capacity constraint rejection, nearest-vehicle greedy selection |
DistanceCalculatorTest |
Unit (Mockito) | Additional service-level edge-case scenarios with mocked repositories |
DispatchIntegrationTest |
Integration (MockMvc) | Full end-to-end: POST /vehicles β POST /orders β GET /plan, JSON response assertions |
Priority sorting β given a LOW order before a HIGH order in the DB, the HIGH order must appear at index 0 in assignedOrders.
Capacity constraint β an order with packageWeight: 60 against a vehicle with capacity: 50 must land in unassignedOrders, not assignedOrders, and totalLoad must remain 0.
Nearest vehicle selection β given V_NEAR at 5.0 km and V_FAR at 5,000 km, the order must be routed to V_NEAR.
GlobalExceptionHandler (@RestControllerAdvice) handles two categories globally.
Triggered when a @NotNull-annotated field (e.g., orderId, vehicleId) is absent from the request body.
{
"status": "error",
"message": "Validation Failed",
"errors": {
"orderId": "Order ID cannot be null",
"vehicleId": "Vehicle ID cannot be null"
}
}Catches all other exceptions and returns a clean message instead of a stack trace.
{
"status": "error",
"message": "<exception message>"
}Before the service layer is ever reached, the controller performs an explicit empty-list check:
| Condition | HTTP | Body |
|---|---|---|
orders key missing or empty array |
400 |
{ "message": "No orders provided" } |
vehicles key missing or empty array |
400 |
{ "message": "No vehicles provided" } |
| No data in DB when plan is requested | 200 |
{ "dispatchPlan": [], "unassignedOrders": [] } |
unassignedOrders is List<String> (IDs only) β The response only needs to flag which orders couldn't be placed. Full order objects would be redundant since they're already persisted in the DB and retrievable.
Secondary sort by weight descending β Within the same priority tier, heavier items are harder to fit as capacity depletes. Assigning them first improves overall fleet utilisation.
VehicleSimulationState inner class β The algorithm requires mutable in-memory state (remaining capacity, current GPS position, accumulated km) that must not leak into the immutable JPA Vehicle entity. The inner class isolates this transient routing state cleanly inside DispatchService.
Table name "orders" not "order" β ORDER is a reserved SQL keyword. The explicit @Table(name = "orders") annotation on the Order entity prevents a Hibernate DDL failure at startup.
spring.jpa.hibernate.ddl-auto=update β Schema is auto-managed by Hibernate on startup, keeping the setup completely zero-config for development and testing.
Β© 2026 Yogesh Shende. All Rights Reserved.