Skip to main content

Overview

Warehouse selection strategies determine how the system chooses which warehouse to assign products from when fulfilling store demands. The application provides two built-in strategies and supports custom strategy implementation.

Configuration

The warehouse selection strategy is configured in application.properties:
distribution.strategy.warehouse-selection=distanceWithToleranceStrategy
Change this value to switch between strategies:
  • distanceOnlyStrategy
  • distanceWithToleranceStrategy

Available Strategies

Distance Only Strategy

Bean Name: distanceOnlyStrategy Description: Orders warehouses solely by geographic distance from the store using the Haversine formula. Implementation:
@Component("distanceOnlyStrategy")
public class DistanceOnlyStrategy implements WarehouseSelectionStrategy {
    
    @Override
    public List<WarehouseWithDistance> selectWarehouses(
        Store store, 
        List<Warehouse> warehouses, 
        String productId, 
        String size
    ) {
        return warehouses.stream()
            .map(warehouse -> {
                double distance = geoDistanceService.calculateHaversineDistance(
                    store.getLatitude(), store.getLongitude(),
                    warehouse.getLatitude(), warehouse.getLongitude()
                );
                return new WarehouseWithDistance(warehouse, distance);
            })
            .sorted(Comparator.comparingDouble(WarehouseWithDistance::distanceKm))
            .toList();
    }
}
Use Cases:
  • Minimize transportation costs
  • Reduce delivery times
  • Simplest strategy for predictable demand patterns
  • When all warehouses have similar stock levels
Pros:
  • Simple and predictable behavior
  • Minimizes transportation distance
  • Easy to understand and explain
  • Fast computation
Cons:
  • Ignores stock availability at closer warehouses
  • May deplete nearby warehouse stock quickly
  • Doesn’t balance warehouse utilization
  • Can lead to more unfulfilled demands

Distance With Tolerance Strategy

Bean Name: distanceWithToleranceStrategy Description: Orders warehouses considering both distance and product availability. If two warehouses are within a distance tolerance (10%), prioritizes the warehouse with more stock for the specific product and size. Implementation:
@Component("distanceWithToleranceStrategy")
public class DistanceWithToleranceStrategy implements WarehouseSelectionStrategy {
    
    private static final double DISTANCE_TOLERANCE = 0.10; // 10%
    
    @Override
    public List<WarehouseWithDistance> selectWarehouses(
        Store store, 
        List<Warehouse> warehouses, 
        String productId, 
        String size
    ) {
        List<WarehouseWithDistance> warehousesWithDistance = warehouses.stream()
            .map(warehouse -> {
                double distance = geoDistanceService.calculateHaversineDistance(
                    store.getLatitude(), store.getLongitude(),
                    warehouse.getLatitude(), warehouse.getLongitude()
                );
                return new WarehouseWithDistance(warehouse, distance);
            })
            .collect(Collectors.toList());
        
        warehousesWithDistance.sort((w1, w2) -> {
            double d1 = w1.distanceKm();
            double d2 = w2.distanceKm();
            double diff = Math.abs(d1 - d2);
            
            // If distance difference is significant (>10%), sort by distance
            if (diff >= Math.min(d1, d2) * DISTANCE_TOLERANCE) {
                return Double.compare(d1, d2);
            }
            
            // If distances are similar, prioritize warehouse with more stock
            int stock1 = w1.warehouse().getStockForProduct(productId, size);
            int stock2 = w2.warehouse().getStockForProduct(productId, size);
            return Integer.compare(stock2, stock1); // Higher stock first
        });
        
        return warehousesWithDistance;
    }
}
Use Cases:
  • Balance between distance and stock availability
  • Reduce unfulfilled demands
  • Better warehouse utilization
  • When warehouses have varying stock levels
Pros:
  • Reduces unfulfilled demands
  • Better stock distribution across warehouses
  • Balances transportation costs with fulfillment rates
  • Considers product-specific availability
Cons:
  • Slightly more complex logic
  • May result in longer average distances
  • Requires real-time stock information
  • 10% tolerance is fixed (not configurable)

Strategy Comparison

FeatureDistance OnlyDistance With Tolerance
Primary FactorDistanceDistance + Stock
Stock AwarenessNoYes
Distance ToleranceN/A10%
Computation CostLowMedium
Fulfillment RateLowerHigher
Average DistanceMinimizedSlightly higher
Warehouse UtilizationUnbalancedBalanced
Best ForCost minimizationFulfillment optimization

Distance Tolerance Explained

The 10% distance tolerance works as follows: Example 1: Warehouses are significantly different distances
  • Warehouse A: 50 km away
  • Warehouse B: 70 km away
  • Difference: 20 km (40% of 50 km) → Significant difference
  • Result: Choose Warehouse A (closer)
Example 2: Warehouses are similar distances
  • Warehouse A: 50 km away, 10 units in stock
  • Warehouse B: 54 km away, 100 units in stock
  • Difference: 4 km (8% of 50 km) → Within tolerance
  • Result: Choose Warehouse B (more stock)

Creating Custom Strategies

You can implement custom warehouse selection strategies by following these steps:

1. Implement the Interface

Create a class that implements WarehouseSelectionStrategy:
package com.productdistribution.backend.services.strategies;

import org.springframework.stereotype.Component;
import java.util.List;

@Component("myCustomStrategy")
public class MyCustomStrategy implements WarehouseSelectionStrategy {
    
    @Override
    public List<WarehouseWithDistance> selectWarehouses(
        Store store, 
        List<Warehouse> warehouses, 
        String productId, 
        String size
    ) {
        // Your custom logic here
        // Return sorted list of warehouses with distances
    }
}

2. Register as Spring Bean

Ensure your strategy is annotated with @Component and has a unique bean name:
@Component("myCustomStrategy")
public class MyCustomStrategy implements WarehouseSelectionStrategy {
    // ...
}

3. Update Configuration

Change the strategy in application.properties:
distribution.strategy.warehouse-selection=myCustomStrategy

4. Access Dependencies

Inject services via constructor injection:
@Component("myCustomStrategy")
public class MyCustomStrategy implements WarehouseSelectionStrategy {
    
    private final GeoDistanceService geoDistanceService;
    private final SomeOtherService otherService;
    
    @Autowired
    public MyCustomStrategy(
        GeoDistanceService geoDistanceService,
        SomeOtherService otherService
    ) {
        this.geoDistanceService = geoDistanceService;
        this.otherService = otherService;
    }
    
    @Override
    public List<WarehouseWithDistance> selectWarehouses(...) {
        // Use injected services
    }
}

Custom Strategy Examples

Priority-Based Strategy

Prioritize specific warehouses based on business rules:
@Component("priorityStrategy")
public class PriorityStrategy implements WarehouseSelectionStrategy {
    
    private final GeoDistanceService geoDistanceService;
    private final Map<String, Integer> warehousePriorities;
    
    @Override
    public List<WarehouseWithDistance> selectWarehouses(...) {
        return warehouses.stream()
            .map(warehouse -> {
                double distance = geoDistanceService.calculateHaversineDistance(
                    store.getLatitude(), store.getLongitude(),
                    warehouse.getLatitude(), warehouse.getLongitude()
                );
                return new WarehouseWithDistance(warehouse, distance);
            })
            .sorted(Comparator
                .comparing(w -> warehousePriorities.getOrDefault(w.warehouse().getId(), 999))
                .thenComparing(WarehouseWithDistance::distanceKm)
            )
            .toList();
    }
}

Region-Based Strategy

Prefer warehouses in the same country or region:
@Component("regionStrategy")
public class RegionStrategy implements WarehouseSelectionStrategy {
    
    private final GeoDistanceService geoDistanceService;
    
    @Override
    public List<WarehouseWithDistance> selectWarehouses(
        Store store, 
        List<Warehouse> warehouses, 
        String productId, 
        String size
    ) {
        return warehouses.stream()
            .map(warehouse -> {
                double distance = geoDistanceService.calculateHaversineDistance(
                    store.getLatitude(), store.getLongitude(),
                    warehouse.getLatitude(), warehouse.getLongitude()
                );
                return new WarehouseWithDistance(warehouse, distance);
            })
            .sorted(Comparator
                .comparing((WarehouseWithDistance w) -> 
                    !w.warehouse().getCountry().equals(store.getCountry()))
                .thenComparing(WarehouseWithDistance::distanceKm)
            )
            .toList();
    }
}

Cost-Optimized Strategy

Consider transportation costs with different rates per km:
@Component("costOptimizedStrategy")
public class CostOptimizedStrategy implements WarehouseSelectionStrategy {
    
    private final GeoDistanceService geoDistanceService;
    private static final double COST_PER_KM = 0.50; // $0.50 per km
    private static final double STOCK_VALUE = 10.0; // Value per unit
    
    @Override
    public List<WarehouseWithDistance> selectWarehouses(...) {
        return warehouses.stream()
            .map(warehouse -> {
                double distance = geoDistanceService.calculateHaversineDistance(
                    store.getLatitude(), store.getLongitude(),
                    warehouse.getLatitude(), warehouse.getLongitude()
                );
                return new WarehouseWithDistance(warehouse, distance);
            })
            .sorted(Comparator.comparingDouble(w -> {
                double transportCost = w.distanceKm() * COST_PER_KM;
                int stock = w.warehouse().getStockForProduct(productId, size);
                double stockValue = stock * STOCK_VALUE;
                return transportCost / Math.max(stockValue, 1.0); // Cost per value
            }))
            .toList();
    }
}

Testing Strategies

Create unit tests for your custom strategies:
@Test
void testCustomStrategy() {
    // Arrange
    Store store = createTestStore(40.7128, -74.0060); // New York
    List<Warehouse> warehouses = List.of(
        createTestWarehouse("W1", 40.7580, -73.9855, 100), // Close, high stock
        createTestWarehouse("W2", 40.7489, -73.9680, 10)   // Close, low stock
    );
    
    MyCustomStrategy strategy = new MyCustomStrategy(geoDistanceService);
    
    // Act
    List<WarehouseWithDistance> result = strategy.selectWarehouses(
        store, warehouses, "PROD123", "M"
    );
    
    // Assert
    assertEquals("W1", result.get(0).warehouse().getId());
}

Best Practices

  1. Performance: Keep strategy logic efficient as it runs for every product-store combination
  2. Consistency: Return a consistent ordering for the same inputs
  3. Null Safety: Handle edge cases like empty warehouse lists or missing coordinates
  4. Distance Calculation: Reuse GeoDistanceService for consistent distance calculations
  5. Documentation: Document your strategy’s behavior and use cases
  6. Testing: Write comprehensive unit tests covering edge cases
  7. Monitoring: Log strategy decisions for debugging and optimization

Switching Strategies

To switch strategies at runtime:
  1. Update Configuration: Modify application.properties
  2. Restart Application: Required to load the new strategy bean
  3. Verify: Check logs for strategy initialization messages
  4. Monitor: Observe metrics to evaluate strategy effectiveness

Strategy Performance Impact

The strategy selection affects:
  • Fulfillment Rate: Percentage of demands successfully fulfilled
  • Average Distance: Mean distance between warehouses and stores
  • Transportation Cost: Total shipping costs
  • Warehouse Utilization: Balance of stock across warehouses
  • Processing Time: Computation time for distribution algorithm
Monitor these metrics to evaluate strategy effectiveness and make data-driven decisions.

Build docs developers (and LLMs) love