Skip to main content

Overview

The Product Distribution Dashboard uses a sophisticated distribution algorithm to assign products from warehouses to stores. The algorithm considers multiple factors including distance, capacity constraints, and inventory availability to optimize product fulfillment.

How It Works

The distribution process follows these steps:
  1. Load Data: Load stores, warehouses, and products from JSON sources
  2. Process Store Demand: For each store, process all demand items
  3. Select Warehouses: Use the configured strategy to select and rank warehouses
  4. Calculate Shipments: Determine optimal quantities considering constraints
  5. Track Unfulfilled Demand: Record any demand that cannot be satisfied
  6. Persist Results: Save stock assignments and unfulfilled demands to the database
@CacheEvict(value = {"globalMetrics", "detailedMetrics"}, allEntries = true)
@Transactional
public List<StockAssignment> distributeProducts() {
    logger.info("Starting product distribution process with strategy: {}", 
        getWarehouseSelectionStrategy().getClass().getSimpleName());
    
    loadData();
    logger.info("Data loaded: {} stores, {} warehouses, {} products", 
        stores.size(), warehouses.size(), products.size());

    List<StockAssignment> assignments = new ArrayList<>();
    List<UnfulfilledDemand> unfulfilledDemands = new ArrayList<>();

    for (Store store : stores) {
        processStoreDemand(store, assignments, unfulfilledDemands);
    }

    persistResults(assignments, unfulfilledDemands);
    
    logger.info("Distribution completed: {} assignments, {} unfulfilled demands", 
        assignments.size(), unfulfilledDemands.size());

    return assignments;
}

Warehouse Selection Strategies

The system supports pluggable warehouse selection strategies through the WarehouseSelectionStrategy interface. Two strategies are provided:

Distance Only Strategy

The simplest strategy that selects warehouses based solely on geographic distance using the Haversine formula.
@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();
}
Characteristics:
  • Orders warehouses by distance (closest first)
  • No consideration of stock levels
  • Best for scenarios where proximity is the primary concern

Distance With Tolerance Strategy

A more sophisticated strategy that balances distance with stock availability using a tolerance threshold.
@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 (diff >= Math.min(d1, d2) * DISTANCE_TOLERANCE) return Double.compare(d1, d2);
        
        int stock1 = w1.warehouse().getStockForProduct(productId, size);
        int stock2 = w2.warehouse().getStockForProduct(productId, size);
        return Integer.compare(stock2, stock1);
    });
    
    return warehousesWithDistance;
}
Characteristics:
  • Uses 10% distance tolerance (configurable via DISTANCE_TOLERANCE)
  • If warehouses are within tolerance range, prioritizes higher stock levels
  • Balances transportation costs with inventory consolidation
  • Default strategy in production

Demand Adjustment

The algorithm adjusts store demand based on expected return rates to avoid overstocking:
public int calculateAdjustedDemand(int originalDemand) {
    return (int) Math.ceil(originalDemand * (1 - this.expectedReturnRate));
}
Example: If a store expects a 20% return rate and orders 100 units:
Adjusted Demand = 100 × (1 - 0.20) = 80 units

Capacity Constraints

Each store has a maximum stock capacity that limits total units received:
private int calculateQuantityToSend(int adjustedDemand, int stockAvailable, int storeCapacityLeft) {
    return Math.min(adjustedDemand, Math.min(stockAvailable, storeCapacityLeft));
}
The algorithm considers three factors:
  1. Adjusted Demand: What the store needs (after return rate adjustment)
  2. Stock Available: What the warehouse has in inventory
  3. Store Capacity Left: Remaining space in the store
The minimum of these three values determines the shipment quantity.

Unfulfilled Demand Handling

When demand cannot be fully satisfied, the system tracks unfulfilled demand with specific reasons:
if (adjustedDemand > 0) {
    UnfulfilledReason reason = store.getRemainingCapacity() == 0 
        ? UnfulfilledReason.CAPACITY_SHORTAGE 
        : UnfulfilledReason.STOCK_SHORTAGE;
    
    String reasonText = reason == UnfulfilledReason.CAPACITY_SHORTAGE 
        ? "insufficient capacity" 
        : "insufficient stock";
    
    logger.warn("Unfulfilled demand due to {}: Store {} - Product {} size {} → {} units missing",
        reasonText, store.getId(), productId, size, adjustedDemand);
    
    unfulfilledDemands.add(new UnfulfilledDemand(
        store.getId(),
        productId,
        size,
        adjustedDemand,
        reason
    ));
}
Unfulfilled Reasons:
  • CAPACITY_SHORTAGE: Store reached maximum capacity
  • STOCK_SHORTAGE: No warehouse has sufficient inventory

Configuration

Configure the warehouse selection strategy in application.properties:
# Choose between 'distanceOnlyStrategy' or 'distanceWithToleranceStrategy'
distribution.strategy.warehouse-selection=distanceWithToleranceStrategy

Algorithm Flow

1

Initialize

Load all stores, warehouses, and products from data sources
2

Process Each Store

Iterate through each store and process their demand items
3

Adjust Demand

Calculate adjusted demand based on expected return rate
4

Select Warehouses

Use configured strategy to rank warehouses for the product
5

Allocate Stock

For each warehouse (in order):
  • Check if warehouse has the product in stock
  • Calculate quantity considering capacity and availability
  • Allocate store capacity and reduce warehouse stock
  • Create stock assignment record
  • Stop if demand fully satisfied or store at capacity
6

Track Unfulfilled

If demand remains after all warehouses checked, record as unfulfilled demand
7

Persist Results

Save all stock assignments and unfulfilled demands to database

Performance Considerations

  • The algorithm uses caching to improve performance (@CacheEvict annotation)
  • Results are cached in globalMetrics and detailedMetrics caches
  • Distribution runs transactionally to ensure data consistency
  • Warehouse selection strategies can be swapped without code changes

Next Steps

Dashboard

Explore the interactive dashboard visualization

Metrics

Learn about distribution metrics and analytics

Build docs developers (and LLMs) love