Warehouse Bin Location System in Java: Complete Guide

A warehouse bin location system manages storage locations, inventory placement, and retrieval operations. This implementation covers bin management, inventory tracking, and optimization algorithms.


1. Project Setup and Dependencies

Maven Dependencies (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>warehouse-bin-system</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.boot.version>2.7.14</spring.boot.version>
<jackson.version>2.15.2</jackson.version>
<hibernate.spatial.version>5.6.15.Final</hibernate.spatial.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Spatial Data Support -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-spatial</artifactId>
<version>${hibernate.spatial.version}</version>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Cache -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.6</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
</plugin>
</plugins>
</build>
</project>

2. Configuration Classes

Application Properties (application.yml)

# Database Configuration
spring:
datasource:
url: jdbc:postgresql://localhost:5432/warehouse_db
username: warehouse_user
password: warehouse_pass
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
jdbc:
lob:
non_contextual_creation: true
show-sql: true
# Spatial Data Configuration
jpa:
properties:
hibernate:
spatial:
dialect: org.hibernate.spatial.dialect.postgis.PostgisDialect
# Warehouse Configuration
warehouse:
bin:
code:
pattern: "[A-Z]{2}-[0-9]{3}-[A-Z]{1}-[0-9]{2}"
separator: "-"
dimensions:
max-weight: 1000.0 # kg
max-volume: 10.0   # cubic meters
optimization:
putaway-strategy: FEFO # FIFO, LIFO, FEFO, Closest
picking-strategy: FEFO
enable-slotting: true
replenishment:
min-threshold: 0.2 # 20%
max-threshold: 0.8 # 80%
# Server Configuration
server:
port: 8080
# Logging
logging:
level:
com.example.warehouse: DEBUG
org.hibernate.SQL: DEBUG

Warehouse Configuration Class

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "warehouse.bin")
public class WarehouseConfig {
private String codePattern = "[A-Z]{2}-[0-9]{3}-[A-Z]{1}-[0-9]{2}";
private String separator = "-";
private Dimensions dimensions = new Dimensions();
private Optimization optimization = new Optimization();
private Replenishment replenishment = new Replenishment();
@Data
public static class Dimensions {
private double maxWeight = 1000.0;
private double maxVolume = 10.0;
private double maxHeight = 3.0;
private double maxWidth = 2.0;
private double maxDepth = 2.0;
}
@Data
public static class Optimization {
private PutawayStrategy putawayStrategy = PutawayStrategy.FEFO;
private PickingStrategy pickingStrategy = PickingStrategy.FEFO;
private boolean enableSlotting = true;
private boolean enableCrossDocking = false;
}
@Data
public static class Replenishment {
private double minThreshold = 0.2;
private double maxThreshold = 0.8;
private int bufferStock = 10;
}
public enum PutawayStrategy {
FIFO, LIFO, FEFO, CLOSEST, RANDOM, OPTIMAL_SPACE
}
public enum PickingStrategy {
FIFO, LIFO, FEFO, CLOSEST, BATCH
}
}

3. Database Entities

Warehouse Entity

import lombok.Data;
import org.locationtech.jts.geom.Point;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "warehouses")
@Data
public class Warehouse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "code", unique = true, nullable = false)
private String code;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "description")
private String description;
@Column(name = "address_line1")
private String addressLine1;
@Column(name = "address_line2")
private String addressLine2;
@Column(name = "city")
private String city;
@Column(name = "state")
private String state;
@Column(name = "country")
private String country;
@Column(name = "postal_code")
private String postalCode;
@Column(name = "total_capacity")
private Integer totalCapacity;
@Column(name = "used_capacity")
private Integer usedCapacity;
@Column(name = "is_active")
private Boolean isActive = true;
@Column(columnDefinition = "geometry(Point,4326)")
private Point location;
@Column(name = "operating_hours")
private String operatingHours;
@Column(name = "contact_phone")
private String contactPhone;
@Column(name = "contact_email")
private String contactEmail;
@OneToMany(mappedBy = "warehouse", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Zone> zones = new ArrayList<>();
@OneToMany(mappedBy = "warehouse", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Aisle> aisles = new ArrayList<>();
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
if (usedCapacity == null) {
usedCapacity = 0;
}
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

Zone Entity

import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "zones")
@Data
public class Zone {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "code", nullable = false)
private String code;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "description")
private String description;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "warehouse_id", nullable = false)
private Warehouse warehouse;
@Column(name = "zone_type")
@Enumerated(EnumType.STRING)
private ZoneType zoneType;
@Column(name = "temperature_range")
private String temperatureRange;
@Column(name = "security_level")
private String securityLevel;
@Column(name = "max_weight_capacity")
private Double maxWeightCapacity;
@Column(name = "max_volume_capacity")
private Double maxVolumeCapacity;
@Column(name = "current_weight_utilization")
private Double currentWeightUtilization = 0.0;
@Column(name = "current_volume_utilization")
private Double currentVolumeUtilization = 0.0;
@Column(name = "is_active")
private Boolean isActive = true;
@OneToMany(mappedBy = "zone", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Aisle> aisles = new ArrayList<>();
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
public enum ZoneType {
BULK_STORAGE, RACK_STORAGE, PICKING_ZONE, 
COLD_STORAGE, HAZMAT_STORAGE, QUARANTINE, 
CROSS_DOCK, STAGING
}
}

Aisle Entity

import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "aisles")
@Data
public class Aisle {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "code", nullable = false)
private String code;
@Column(name = "name")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "warehouse_id", nullable = false)
private Warehouse warehouse;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "zone_id")
private Zone zone;
@Column(name = "aisle_number")
private Integer aisleNumber;
@Column(name = "side")
@Enumerated(EnumType.STRING)
private Side side;
@Column(name = "width")
private Double width; // meters
@Column(name = "height")
private Double height; // meters
@Column(name = "length")
private Double length; // meters
@Column(name = "access_type")
@Enumerated(EnumType.STRING)
private AccessType accessType;
@Column(name = "is_active")
private Boolean isActive = true;
@OneToMany(mappedBy = "aisle", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Rack> racks = new ArrayList<>();
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
public enum Side {
LEFT, RIGHT, BOTH
}
public enum AccessType {
SINGLE_ACCESS, DOUBLE_ACCESS, DRIVE_THROUGH
}
}

Rack Entity

import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "racks")
@Data
public class Rack {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "code", nullable = false)
private String code;
@Column(name = "name")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "aisle_id", nullable = false)
private Aisle aisle;
@Column(name = "rack_number")
private Integer rackNumber;
@Column(name = "rack_type")
@Enumerated(EnumType.STRING)
private RackType rackType;
@Column(name = "levels")
private Integer levels;
@Column(name = "bays")
private Integer bays;
@Column(name = "max_weight_per_level")
private Double maxWeightPerLevel;
@Column(name = "max_height")
private Double maxHeight;
@Column(name = "current_weight_utilization")
private Double currentWeightUtilization = 0.0;
@Column(name = "is_active")
private Boolean isActive = true;
@OneToMany(mappedBy = "rack", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<BinLocation> binLocations = new ArrayList<>();
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
public enum RackType {
SELECTIVE, DRIVE_IN, DRIVE_THROUGH, 
PUSH_BACK, CANTILEVER, MOBILE, PALLET_FLOW
}
}

Bin Location Entity (Core Entity)

import lombok.Data;
import org.locationtech.jts.geom.Point;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "bin_locations", 
indexes = {
@Index(name = "idx_bin_code", columnList = "code"),
@Index(name = "idx_bin_warehouse", columnList = "warehouse_id"),
@Index(name = "idx_bin_status", columnList = "status"),
@Index(name = "idx_bin_zone", columnList = "zone_type")
})
@Data
public class BinLocation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "code", unique = true, nullable = false)
private String code;
@Column(name = "name")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "warehouse_id", nullable = false)
private Warehouse warehouse;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "zone_id")
private Zone zone;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "aisle_id")
private Aisle aisle;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rack_id")
private Rack rack;
@Column(name = "level_number")
private Integer levelNumber;
@Column(name = "bay_number")
private Integer bayNumber;
@Column(name = "position_number")
private Integer positionNumber;
@Column(name = "bin_type")
@Enumerated(EnumType.STRING)
private BinType binType;
@Column(name = "zone_type")
@Enumerated(EnumType.STRING)
private ZoneType zoneType;
@Column(name = "status")
@Enumerated(EnumType.STRING)
private BinStatus status;
@Column(name = "max_weight_capacity")
private Double maxWeightCapacity;
@Column(name = "max_volume_capacity")
private Double maxVolumeCapacity;
@Column(name = "current_weight")
private Double currentWeight = 0.0;
@Column(name = "current_volume")
private Double currentVolume = 0.0;
@Column(name = "utilization_percentage")
private Double utilizationPercentage = 0.0;
@Column(name = "dimension_width")
private Double dimensionWidth;
@Column(name = "dimension_height")
private Double dimensionHeight;
@Column(name = "dimension_depth")
private Double dimensionDepth;
@Column(name = "pick_sequence")
private Integer pickSequence;
@Column(name = "putaway_sequence")
private Integer putawaySequence;
@Column(name = "is_fast_moving")
private Boolean isFastMoving = false;
@Column(name = "last_activity_date")
private LocalDateTime lastActivityDate;
@Column(columnDefinition = "geometry(Point,4326)")
private Point coordinates;
@Column(name = "address_string")
private String addressString;
@OneToMany(mappedBy = "binLocation", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<InventoryItem> inventoryItems = new ArrayList<>();
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
if (lastActivityDate == null) {
lastActivityDate = LocalDateTime.now();
}
calculateUtilization();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
calculateUtilization();
}
private void calculateUtilization() {
if (maxVolumeCapacity != null && maxVolumeCapacity > 0) {
this.utilizationPercentage = (currentVolume / maxVolumeCapacity) * 100;
}
}
public boolean canAccommodate(double weight, double volume) {
return (currentWeight + weight) <= maxWeightCapacity && 
(currentVolume + volume) <= maxVolumeCapacity;
}
public enum BinType {
PALLET, SHELF, BIN, CARTON_FLOW, FLOOR_STACK, 
BULK_STORAGE, MEZZANINE, AUTOMATED
}
public enum ZoneType {
PICKING, STORAGE, OVERFLOW, QUARANTINE, 
CROSS_DOCK, STAGING, HAZMAT
}
public enum BinStatus {
AVAILABLE, OCCUPIED, RESERVED, BLOCKED, 
UNDER_MAINTENANCE, QUARANTINED
}
}

Inventory Item Entity

import lombok.Data;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "inventory_items")
@Data
public class InventoryItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "sku", nullable = false)
private String sku;
@Column(name = "product_name")
private String productName;
@Column(name = "description")
private String description;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "bin_location_id", nullable = false)
private BinLocation binLocation;
@Column(name = "quantity")
private Integer quantity;
@Column(name = "allocated_quantity")
private Integer allocatedQuantity = 0;
@Column(name = "available_quantity")
private Integer availableQuantity;
@Column(name = "batch_number")
private String batchNumber;
@Column(name = "lot_number")
private String lotNumber;
@Column(name = "serial_number")
private String serialNumber;
@Column(name = "expiry_date")
private LocalDateTime expiryDate;
@Column(name = "manufacturing_date")
private LocalDateTime manufacturingDate;
@Column(name = "received_date")
private LocalDateTime receivedDate;
@Column(name = "unit_cost", precision = 15, scale = 4)
private BigDecimal unitCost;
@Column(name = "total_value", precision = 15, scale = 4)
private BigDecimal totalValue;
@Column(name = "weight")
private Double weight;
@Column(name = "volume")
private Double volume;
@Column(name = "dimensions")
private String dimensions;
@Column(name = "uom")
private String uom; // Unit of Measure
@Column(name = "status")
@Enumerated(EnumType.STRING)
private InventoryStatus status;
@Column(name = "abc_classification")
@Enumerated(EnumType.STRING)
private AbcClassification abcClassification;
@Column(name = "turnover_rate")
private Double turnoverRate;
@Column(name = "last_count_date")
private LocalDateTime lastCountDate;
@Column(name = "next_count_date")
private LocalDateTime nextCountDate;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
if (receivedDate == null) {
receivedDate = LocalDateTime.now();
}
calculateAvailableQuantity();
calculateTotalValue();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
calculateAvailableQuantity();
calculateTotalValue();
}
private void calculateAvailableQuantity() {
this.availableQuantity = this.quantity - this.allocatedQuantity;
}
private void calculateTotalValue() {
if (unitCost != null && quantity != null) {
this.totalValue = unitCost.multiply(BigDecimal.valueOf(quantity));
}
}
public enum InventoryStatus {
AVAILABLE, ALLOCATED, RESERVED, QUARANTINED, 
DAMAGED, EXPIRED, IN_TRANSIT
}
public enum AbcClassification {
A, B, C, D
}
}

4. Data Transfer Objects (DTOs)

Bin Location DTOs

import lombok.Data;
import javax.validation.constraints.*;
import java.time.LocalDateTime;
@Data
public class BinLocationDTO {
private Long id;
@NotBlank(message = "Bin code is required")
@Pattern(regexp = "[A-Z]{2}-[0-9]{3}-[A-Z]{1}-[0-9]{2}", 
message = "Invalid bin code format")
private String code;
private String name;
@NotNull(message = "Warehouse ID is required")
private Long warehouseId;
private Long zoneId;
private Long aisleId;
private Long rackId;
@Min(1)
private Integer levelNumber;
@Min(1)
private Integer bayNumber;
@Min(1)
private Integer positionNumber;
@NotNull
private BinLocation.BinType binType;
@NotNull
private BinLocation.ZoneType zoneType;
@NotNull
private BinLocation.BinStatus status;
@Positive
private Double maxWeightCapacity;
@Positive
private Double maxVolumeCapacity;
@Positive
private Double dimensionWidth;
@Positive
private Double dimensionHeight;
@Positive
private Double dimensionDepth;
private Integer pickSequence;
private Integer putawaySequence;
private Boolean isFastMoving = false;
// Coordinates
private Double latitude;
private Double longitude;
}
@Data
public class BinLocationSearchDTO {
private Long warehouseId;
private Long zoneId;
private BinLocation.BinType binType;
private BinLocation.ZoneType zoneType;
private BinLocation.BinStatus status;
private Double minUtilization;
private Double maxUtilization;
private Boolean isFastMoving;
private String sku; // Search bins containing specific SKU
}
@Data
public class BinAllocationRequestDTO {
@NotBlank
private String sku;
@NotNull
@Positive
private Integer quantity;
private String batchNumber;
private String lotNumber;
@NotNull
@Positive
private Double weight;
@NotNull
@Positive
private Double volume;
private LocalDateTime expiryDate;
@NotNull
private AllocationStrategy strategy = AllocationStrategy.OPTIMAL;
public enum AllocationStrategy {
OPTIMAL, CLOSEST, FEFO, FIFO, RANDOM
}
}

Inventory Movement DTOs

import lombok.Data;
import javax.validation.constraints.*;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class PutawayRequestDTO {
@NotBlank
private String referenceNumber;
@NotNull
private LocalDateTime putawayDate;
@NotBlank
private String sku;
@NotNull
@Positive
private Integer quantity;
private String batchNumber;
private String lotNumber;
@NotNull
@Positive
private Double weight;
@NotNull
@Positive
private Double volume;
private LocalDateTime expiryDate;
private LocalDateTime manufacturingDate;
@NotNull
private PutawayStrategy strategy = PutawayStrategy.OPTIMAL;
private Long suggestedBinId; // Optional pre-selected bin
public enum PutawayStrategy {
OPTIMAL, CLOSEST, FEFO, SAME_SKU, RANDOM
}
}
@Data
public class PickingRequestDTO {
@NotBlank
private String orderNumber;
@NotNull
private LocalDateTime pickingDate;
@NotNull
@Size(min = 1)
private List<PickingLineDTO> lines;
@NotNull
private PickingStrategy strategy = PickingStrategy.FEFO;
private Boolean wavePicking = false;
@Data
public static class PickingLineDTO {
@NotBlank
private String sku;
@NotNull
@Positive
private Integer quantity;
private String batchNumber; // Optional specific batch
private String lotNumber;   // Optional specific lot
}
public enum PickingStrategy {
FIFO, LIFO, FEFO, BATCH, ZONE
}
}
@Data
public class MoveRequestDTO {
@NotNull
private Long sourceBinId;
@NotNull
private Long destinationBinId;
@NotBlank
private String sku;
@NotNull
@Positive
private Integer quantity;
private String batchNumber;
private String lotNumber;
@NotBlank
private String reason;
}

5. Service Layer

Bin Location Service

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Slf4j
@Service
@Transactional
public class BinLocationService {
@Autowired
private BinLocationRepository binLocationRepository;
@Autowired
private WarehouseRepository warehouseRepository;
@Autowired
private ZoneRepository zoneRepository;
@Autowired
private AisleRepository aisleRepository;
@Autowired
private RackRepository rackRepository;
@Autowired
private WarehouseConfig warehouseConfig;
/**
* Create new bin location
*/
public BinLocation createBinLocation(BinLocationDTO binLocationDTO) {
try {
BinLocation binLocation = new BinLocation();
mapBinLocationDTOToEntity(binLocationDTO, binLocation);
// Generate bin code if not provided
if (binLocation.getCode() == null) {
binLocation.setCode(generateBinCode(binLocation));
}
// Validate capacity
validateBinCapacity(binLocation);
BinLocation savedBin = binLocationRepository.save(binLocation);
log.info("Created bin location: {}", savedBin.getCode());
return savedBin;
} catch (Exception e) {
log.error("Error creating bin location: {}", e.getMessage(), e);
throw new WarehouseException("Failed to create bin location: " + e.getMessage());
}
}
/**
* Find optimal bin for putaway
*/
@Transactional(readOnly = true)
public Optional<BinLocation> findOptimalBin(PutawayRequestDTO putawayRequest) {
try {
List<BinLocation> candidateBins = binLocationRepository
.findAvailableBinsByCapacity(
putawayRequest.getWeight(),
putawayRequest.getVolume(),
BinLocation.BinStatus.AVAILABLE
);
if (candidateBins.isEmpty()) {
return Optional.empty();
}
// Apply putaway strategy
return applyPutawayStrategy(candidateBins, putawayRequest);
} catch (Exception e) {
log.error("Error finding optimal bin: {}", e.getMessage(), e);
throw new WarehouseException("Failed to find optimal bin: " + e.getMessage());
}
}
/**
* Get bin utilization report
*/
@Cacheable(value = "binUtilization", key = "#warehouseId")
public BinUtilizationReport getBinUtilizationReport(Long warehouseId) {
try {
List<Object[]> utilizationData = binLocationRepository
.getBinUtilizationByWarehouse(warehouseId);
BinUtilizationReport report = new BinUtilizationReport();
report.setWarehouseId(warehouseId);
report.setGeneratedAt(LocalDateTime.now());
for (Object[] data : utilizationData) {
String zoneType = (String) data[0];
Double avgUtilization = (Double) data[1];
Long totalBins = (Long) data[2];
Long availableBins = (Long) data[3];
report.addZoneUtilization(zoneType, avgUtilization, totalBins, availableBins);
}
return report;
} catch (Exception e) {
log.error("Error generating utilization report: {}", e.getMessage(), e);
throw new WarehouseException("Failed to generate utilization report: " + e.getMessage());
}
}
/**
* Search bins with filters
*/
@Transactional(readOnly = true)
public Page<BinLocation> searchBins(BinLocationSearchDTO searchDTO, Pageable pageable) {
try {
return binLocationRepository.findBySearchCriteria(searchDTO, pageable);
} catch (Exception e) {
log.error("Error searching bins: {}", e.getMessage(), e);
throw new WarehouseException("Failed to search bins: " + e.getMessage());
}
}
/**
* Update bin status
*/
public BinLocation updateBinStatus(Long binId, BinLocation.BinStatus newStatus, String reason) {
try {
BinLocation binLocation = binLocationRepository.findById(binId)
.orElseThrow(() -> new WarehouseException("Bin not found with id: " + binId));
BinLocation.BinStatus oldStatus = binLocation.getStatus();
binLocation.setStatus(newStatus);
binLocation.setLastActivityDate(LocalDateTime.now());
BinLocation updatedBin = binLocationRepository.save(binLocation);
log.info("Updated bin {} status from {} to {} - Reason: {}", 
binLocation.getCode(), oldStatus, newStatus, reason);
return updatedBin;
} catch (Exception e) {
log.error("Error updating bin status: {}", e.getMessage(), e);
throw new WarehouseException("Failed to update bin status: " + e.getMessage());
}
}
private void mapBinLocationDTOToEntity(BinLocationDTO dto, BinLocation entity) {
entity.setCode(dto.getCode());
entity.setName(dto.getName());
entity.setLevelNumber(dto.getLevelNumber());
entity.setBayNumber(dto.getBayNumber());
entity.setPositionNumber(dto.getPositionNumber());
entity.setBinType(dto.getBinType());
entity.setZoneType(dto.getZoneType());
entity.setStatus(dto.getStatus());
entity.setMaxWeightCapacity(dto.getMaxWeightCapacity());
entity.setMaxVolumeCapacity(dto.getMaxVolumeCapacity());
entity.setDimensionWidth(dto.getDimensionWidth());
entity.setDimensionHeight(dto.getDimensionHeight());
entity.setDimensionDepth(dto.getDimensionDepth());
entity.setPickSequence(dto.getPickSequence());
entity.setPutawaySequence(dto.getPutawaySequence());
entity.setIsFastMoving(dto.getIsFastMoving());
// Set relationships
if (dto.getWarehouseId() != null) {
Warehouse warehouse = warehouseRepository.findById(dto.getWarehouseId())
.orElseThrow(() -> new WarehouseException("Warehouse not found"));
entity.setWarehouse(warehouse);
}
if (dto.getZoneId() != null) {
Zone zone = zoneRepository.findById(dto.getZoneId())
.orElseThrow(() -> new WarehouseException("Zone not found"));
entity.setZone(zone);
}
if (dto.getAisleId() != null) {
Aisle aisle = aisleRepository.findById(dto.getAisleId())
.orElseThrow(() -> new WarehouseException("Aisle not found"));
entity.setAisle(aisle);
}
if (dto.getRackId() != null) {
Rack rack = rackRepository.findById(dto.getRackId())
.orElseThrow(() -> new WarehouseException("Rack not found"));
entity.setRack(rack);
}
}
private String generateBinCode(BinLocation binLocation) {
// Format: ZONE-AISLE-LEVEL-BAY-POSITION
// Example: PK-A01-L02-B03-P01
return String.format("%s-%s-L%02d-B%02d-P%02d",
binLocation.getZoneType().toString().substring(0, 2),
binLocation.getAisle() != null ? binLocation.getAisle().getCode() : "XX",
binLocation.getLevelNumber(),
binLocation.getBayNumber(),
binLocation.getPositionNumber()
);
}
private void validateBinCapacity(BinLocation binLocation) {
if (binLocation.getMaxWeightCapacity() > warehouseConfig.getDimensions().getMaxWeight()) {
throw new WarehouseException("Bin weight capacity exceeds warehouse maximum");
}
if (binLocation.getMaxVolumeCapacity() > warehouseConfig.getDimensions().getMaxVolume()) {
throw new WarehouseException("Bin volume capacity exceeds warehouse maximum");
}
}
private Optional<BinLocation> applyPutawayStrategy(List<BinLocation> bins, PutawayRequestDTO request) {
switch (request.getStrategy()) {
case OPTIMAL:
return bins.stream()
.filter(bin -> bin.getZoneType() == BinLocation.ZoneType.PICKING)
.filter(bin -> bin.getUtilizationPercentage() < 70.0)
.filter(BinLocation::getIsFastMoving)
.findFirst();
case CLOSEST:
// Implement distance calculation based on coordinates
return bins.stream()
.sorted((b1, b2) -> Integer.compare(b1.getPickSequence(), b2.getPickSequence()))
.findFirst();
case FEFO:
// First Expiry First Out - prioritize bins with earliest expiry
return bins.stream()
.min((b1, b2) -> compareByExpiry(b1, b2, request.getSku()));
case SAME_SKU:
// Try to put with same SKU first
return bins.stream()
.filter(bin -> hasSameSku(bin, request.getSku()))
.findFirst()
.or(() -> bins.stream().findFirst());
default:
return bins.stream().findFirst();
}
}
private boolean hasSameSku(BinLocation bin, String sku) {
return bin.getInventoryItems().stream()
.anyMatch(item -> item.getSku().equals(sku));
}
private int compareByExpiry(BinLocation b1, BinLocation b2, String sku) {
// Implementation for FEFO comparison
return 0; // Simplified
}
}

Inventory Service

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
@Transactional
public class InventoryService {
@Autowired
private InventoryItemRepository inventoryItemRepository;
@Autowired
private BinLocationRepository binLocationRepository;
@Autowired
private BinLocationService binLocationService;
@Autowired
private InventoryMovementRepository movementRepository;
/**
* Execute putaway operation
*/
public InventoryItem executePutaway(PutawayRequestDTO putawayRequest) {
try {
// Find optimal bin
BinLocation binLocation = binLocationService.findOptimalBin(putawayRequest)
.orElseThrow(() -> new WarehouseException("No available bin found for putaway"));
// Create inventory item
InventoryItem inventoryItem = new InventoryItem();
inventoryItem.setSku(putawayRequest.getSku());
inventoryItem.setQuantity(putawayRequest.getQuantity());
inventoryItem.setBatchNumber(putawayRequest.getBatchNumber());
inventoryItem.setLotNumber(putawayRequest.getLotNumber());
inventoryItem.setWeight(putawayRequest.getWeight());
inventoryItem.setVolume(putawayRequest.getVolume());
inventoryItem.setExpiryDate(putawayRequest.getExpiryDate());
inventoryItem.setManufacturingDate(putawayRequest.getManufacturingDate());
inventoryItem.setReceivedDate(LocalDateTime.now());
inventoryItem.setStatus(InventoryItem.InventoryStatus.AVAILABLE);
inventoryItem.setBinLocation(binLocation);
// Update bin utilization
updateBinUtilization(binLocation, putawayRequest.getWeight(), putawayRequest.getVolume());
InventoryItem savedItem = inventoryItemRepository.save(inventoryItem);
// Record movement
recordInventoryMovement(savedItem, "PUTAWAY", putawayRequest.getReferenceNumber());
log.info("Putaway completed for SKU: {} in bin: {}", 
putawayRequest.getSku(), binLocation.getCode());
return savedItem;
} catch (Exception e) {
log.error("Error executing putaway: {}", e.getMessage(), e);
throw new WarehouseException("Putaway failed: " + e.getMessage());
}
}
/**
* Execute picking operation
*/
public List<InventoryItem> executePicking(PickingRequestDTO pickingRequest) {
try {
// This is a simplified implementation
// In real scenario, you would implement wave picking, batch picking, etc.
List<InventoryItem> pickedItems = pickingRequest.getLines().stream()
.map(line -> pickLineItem(line, pickingRequest.getOrderNumber()))
.toList();
log.info("Picking completed for order: {}", pickingRequest.getOrderNumber());
return pickedItems;
} catch (Exception e) {
log.error("Error executing picking: {}", e.getMessage(), e);
throw new WarehouseException("Picking failed: " + e.getMessage());
}
}
/**
* Move inventory between bins
*/
public InventoryItem moveInventory(MoveRequestDTO moveRequest) {
try {
// Find source inventory
InventoryItem sourceItem = inventoryItemRepository
.findBySkuAndBinLocationIdAndBatchNumberAndLotNumber(
moveRequest.getSku(),
moveRequest.getSourceBinId(),
moveRequest.getBatchNumber(),
moveRequest.getLotNumber()
)
.orElseThrow(() -> new WarehouseException("Source inventory not found"));
if (sourceItem.getAvailableQuantity() < moveRequest.getQuantity()) {
throw new WarehouseException("Insufficient quantity available");
}
// Find destination bin
BinLocation destinationBin = binLocationRepository.findById(moveRequest.getDestinationBinId())
.orElseThrow(() -> new WarehouseException("Destination bin not found"));
if (!destinationBin.canAccommodate(
sourceItem.getWeight() * moveRequest.getQuantity(),
sourceItem.getVolume() * moveRequest.getQuantity())) {
throw new WarehouseException("Destination bin cannot accommodate the inventory");
}
// Create new inventory item in destination
InventoryItem destinationItem = new InventoryItem();
destinationItem.setSku(sourceItem.getSku());
destinationItem.setQuantity(moveRequest.getQuantity());
destinationItem.setBatchNumber(sourceItem.getBatchNumber());
destinationItem.setLotNumber(sourceItem.getLotNumber());
destinationItem.setWeight(sourceItem.getWeight());
destinationItem.setVolume(sourceItem.getVolume());
destinationItem.setExpiryDate(sourceItem.getExpiryDate());
destinationItem.setManufacturingDate(sourceItem.getManufacturingDate());
destinationItem.setReceivedDate(LocalDateTime.now());
destinationItem.setStatus(InventoryItem.InventoryStatus.AVAILABLE);
destinationItem.setBinLocation(destinationBin);
// Update source quantity
sourceItem.setQuantity(sourceItem.getQuantity() - moveRequest.getQuantity());
// Update bin utilizations
updateBinUtilization(sourceItem.getBinLocation(), 
-sourceItem.getWeight() * moveRequest.getQuantity(),
-sourceItem.getVolume() * moveRequest.getQuantity());
updateBinUtilization(destinationBin,
destinationItem.getWeight() * moveRequest.getQuantity(),
destinationItem.getVolume() * moveRequest.getQuantity());
InventoryItem savedItem = inventoryItemRepository.save(destinationItem);
inventoryItemRepository.save(sourceItem);
// Record movement
recordInventoryMovement(savedItem, "MOVE", moveRequest.getReason());
log.info("Inventory moved from bin {} to bin {}", 
sourceItem.getBinLocation().getCode(), 
destinationBin.getCode());
return savedItem;
} catch (Exception e) {
log.error("Error moving inventory: {}", e.getMessage(), e);
throw new WarehouseException("Inventory move failed: " + e.getMessage());
}
}
/**
* Get inventory by location
*/
@Transactional(readOnly = true)
public List<InventoryItem> getInventoryByBin(Long binId) {
try {
return inventoryItemRepository.findByBinLocationIdAndStatus(
binId, InventoryItem.InventoryStatus.AVAILABLE);
} catch (Exception e) {
log.error("Error getting inventory by bin: {}", e.getMessage(), e);
throw new WarehouseException("Failed to get inventory: " + e.getMessage());
}
}
private InventoryItem pickLineItem(PickingRequestDTO.PickingLineDTO line, String orderNumber) {
// Find available inventory for this SKU
List<InventoryItem> availableItems = inventoryItemRepository
.findAvailableBySku(line.getSku(), line.getBatchNumber(), line.getLotNumber());
if (availableItems.isEmpty()) {
throw new WarehouseException("No available inventory for SKU: " + line.getSku());
}
int remainingQuantity = line.getQuantity();
for (InventoryItem item : availableItems) {
if (remainingQuantity <= 0) break;
int pickQuantity = Math.min(remainingQuantity, item.getAvailableQuantity());
// Allocate quantity
item.setAllocatedQuantity(item.getAllocatedQuantity() + pickQuantity);
remainingQuantity -= pickQuantity;
// Update bin utilization
updateBinUtilization(item.getBinLocation(),
-item.getWeight() * pickQuantity,
-item.getVolume() * pickQuantity);
inventoryItemRepository.save(item);
// Record movement
recordInventoryMovement(item, "PICK", orderNumber);
}
if (remainingQuantity > 0) {
throw new WarehouseException("Insufficient inventory for SKU: " + line.getSku());
}
return availableItems.get(0); // Return first item for simplicity
}
private void updateBinUtilization(BinLocation binLocation, Double weightDelta, Double volumeDelta) {
binLocation.setCurrentWeight(binLocation.getCurrentWeight() + weightDelta);
binLocation.setCurrentVolume(binLocation.getCurrentVolume() + volumeDelta);
binLocation.setLastActivityDate(LocalDateTime.now());
binLocationRepository.save(binLocation);
}
private void recordInventoryMovement(InventoryItem item, String movementType, String reference) {
InventoryMovement movement = new InventoryMovement();
movement.setInventoryItem(item);
movement.setMovementType(movementType);
movement.setQuantity(item.getQuantity());
movement.setReferenceNumber(reference);
movement.setMovementDate(LocalDateTime.now());
movementRepository.save(movement);
}
}

6. Repository Interfaces with Custom Queries

Bin Location Repository

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface BinLocationRepository extends JpaRepository<BinLocation, Long> {
Optional<BinLocation> findByCode(String code);
List<BinLocation> findByWarehouseId(Long warehouseId);
List<BinLocation> findByZoneId(Long zoneId);
List<BinLocation> findByStatus(BinLocation.BinStatus status);
List<BinLocation> findByZoneType(BinLocation.ZoneType zoneType);
@Query("SELECT b FROM BinLocation b WHERE " +
"(:warehouseId IS NULL OR b.warehouse.id = :warehouseId) AND " +
"(:zoneId IS NULL OR b.zone.id = :zoneId) AND " +
"(:binType IS NULL OR b.binType = :binType) AND " +
"(:zoneType IS NULL OR b.zoneType = :zoneType) AND " +
"(:status IS NULL OR b.status = :status) AND " +
"(:minUtilization IS NULL OR b.utilizationPercentage >= :minUtilization) AND " +
"(:maxUtilization IS NULL OR b.utilizationPercentage <= :maxUtilization) AND " +
"(:isFastMoving IS NULL OR b.isFastMoving = :isFastMoving)")
Page<BinLocation> findBySearchCriteria(@Param("warehouseId") Long warehouseId,
@Param("zoneId") Long zoneId,
@Param("binType") BinLocation.BinType binType,
@Param("zoneType") BinLocation.ZoneType zoneType,
@Param("status") BinLocation.BinStatus status,
@Param("minUtilization") Double minUtilization,
@Param("maxUtilization") Double maxUtilization,
@Param("isFastMoving") Boolean isFastMoving,
Pageable pageable);
default Page<BinLocation> findBySearchCriteria(BinLocationSearchDTO searchDTO, Pageable pageable) {
return findBySearchCriteria(
searchDTO.getWarehouseId(),
searchDTO.getZoneId(),
searchDTO.getBinType(),
searchDTO.getZoneType(),
searchDTO.getStatus(),
searchDTO.getMinUtilization(),
searchDTO.getMaxUtilization(),
searchDTO.getIsFastMoving(),
pageable
);
}
@Query("SELECT b FROM BinLocation b WHERE " +
"b.status = 'AVAILABLE' AND " +
"(b.currentWeight + :weight) <= b.maxWeightCapacity AND " +
"(b.currentVolume + :volume) <= b.maxVolumeCapacity " +
"ORDER BY b.utilizationPercentage ASC, b.putawaySequence ASC")
List<BinLocation> findAvailableBinsByCapacity(@Param("weight") Double weight,
@Param("volume") Double volume,
@Param("status") BinLocation.BinStatus status);
@Query("SELECT b.zoneType, AVG(b.utilizationPercentage), COUNT(b), " +
"SUM(CASE WHEN b.status = 'AVAILABLE' THEN 1 ELSE 0 END) " +
"FROM BinLocation b WHERE b.warehouse.id = :warehouseId " +
"GROUP BY b.zoneType")
List<Object[]> getBinUtilizationByWarehouse(@Param("warehouseId") Long warehouseId);
@Query("SELECT b FROM BinLocation b JOIN b.inventoryItems i WHERE " +
"i.sku = :sku AND i.status = 'AVAILABLE' AND i.availableQuantity > 0 " +
"ORDER BY " +
"CASE WHEN :strategy = 'FEFO' THEN i.expiryDate END ASC, " +
"CASE WHEN :strategy = 'FIFO' THEN i.receivedDate END ASC, " +
"b.pickSequence ASC")
List<BinLocation> findBinsForPicking(@Param("sku") String sku,
@Param("strategy") String strategy);
}

Inventory Item Repository

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface InventoryItemRepository extends JpaRepository<InventoryItem, Long> {
List<InventoryItem> findBySku(String sku);
List<InventoryItem> findByBinLocationId(Long binLocationId);
List<InventoryItem> findByBinLocationIdAndStatus(Long binLocationId, InventoryItem.InventoryStatus status);
Optional<InventoryItem> findBySkuAndBatchNumberAndLotNumber(String sku, String batchNumber, String lotNumber);
Optional<InventoryItem> findBySkuAndBinLocationIdAndBatchNumberAndLotNumber(
@Param("sku") String sku,
@Param("binLocationId") Long binLocationId,
@Param("batchNumber") String batchNumber,
@Param("lotNumber") String lotNumber);
@Query("SELECT i FROM InventoryItem i WHERE " +
"i.sku = :sku AND " +
"i.status = 'AVAILABLE' AND " +
"i.availableQuantity > 0 AND " +
"(:batchNumber IS NULL OR i.batchNumber = :batchNumber) AND " +
"(:lotNumber IS NULL OR i.lotNumber = :lotNumber) " +
"ORDER BY i.expiryDate ASC, i.receivedDate ASC")
List<InventoryItem> findAvailableBySku(@Param("sku") String sku,
@Param("batchNumber") String batchNumber,
@Param("lotNumber") String lotNumber);
@Query("SELECT i.sku, SUM(i.availableQuantity), b.zoneType " +
"FROM InventoryItem i JOIN i.binLocation b " +
"WHERE i.status = 'AVAILABLE' " +
"GROUP BY i.sku, b.zoneType " +
"ORDER BY SUM(i.availableQuantity) DESC")
List<Object[]> getInventorySummaryBySkuAndZone();
@Query("SELECT i FROM InventoryItem i WHERE i.expiryDate < :date AND i.status = 'AVAILABLE'")
List<InventoryItem> findExpiringItems(@Param("date") LocalDateTime date);
@Query(value = """
SELECT sku, 
SUM(available_quantity) as total_quantity,
COUNT(DISTINCT bin_location_id) as location_count
FROM inventory_items 
WHERE status = 'AVAILABLE'
GROUP BY sku
HAVING SUM(available_quantity) > 0
ORDER BY total_quantity DESC
""", nativeQuery = true)
List<Object[]> getSkuDistribution();
}

7. REST Controllers

Bin Location Controller

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/warehouse/bins")
public class BinLocationController {
@Autowired
private BinLocationService binLocationService;
@Autowired
private InventoryService inventoryService;
/**
* Create new bin location
*/
@PostMapping
public ResponseEntity<?> createBinLocation(@Valid @RequestBody BinLocationDTO binLocationDTO) {
try {
BinLocation binLocation = binLocationService.createBinLocation(binLocationDTO);
return ResponseEntity.ok(binLocation);
} catch (WarehouseException e) {
log.error("Error creating bin location: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
/**
* Search bins with filters
*/
@GetMapping("/search")
public ResponseEntity<?> searchBins(
@ModelAttribute BinLocationSearchDTO searchDTO,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "50") int size) {
try {
Pageable pageable = PageRequest.of(page, size);
Page<BinLocation> bins = binLocationService.searchBins(searchDTO, pageable);
return ResponseEntity.ok(bins);
} catch (WarehouseException e) {
log.error("Error searching bins: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
/**
* Get bin utilization report
*/
@GetMapping("/utilization/{warehouseId}")
public ResponseEntity<?> getBinUtilization(@PathVariable Long warehouseId) {
try {
BinUtilizationReport report = binLocationService.getBinUtilizationReport(warehouseId);
return ResponseEntity.ok(report);
} catch (WarehouseException e) {
log.error("Error getting utilization report: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
/**
* Update bin status
*/
@PutMapping("/{binId}/status")
public ResponseEntity<?> updateBinStatus(
@PathVariable Long binId,
@RequestParam BinLocation.BinStatus status,
@RequestParam String reason) {
try {
BinLocation binLocation = binLocationService.updateBinStatus(binId, status, reason);
return ResponseEntity.ok(binLocation);
} catch (WarehouseException e) {
log.error("Error updating bin status: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
/**
* Get inventory in a bin
*/
@GetMapping("/{binId}/inventory")
public ResponseEntity<?> getBinInventory(@PathVariable Long binId) {
try {
List<InventoryItem> inventory = inventoryService.getInventoryByBin(binId);
return ResponseEntity.ok(inventory);
} catch (WarehouseException e) {
log.error("Error getting bin inventory: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
private Map<String, Object> createErrorResponse(String message) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", true);
errorResponse.put("message", message);
return errorResponse;
}
}

Inventory Controller

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/warehouse/inventory")
public class InventoryController {
@Autowired
private InventoryService inventoryService;
/**
* Execute putaway operation
*/
@PostMapping("/putaway")
public ResponseEntity<?> executePutaway(@Valid @RequestBody PutawayRequestDTO putawayRequest) {
try {
InventoryItem inventoryItem = inventoryService.executePutaway(putawayRequest);
return ResponseEntity.ok(inventoryItem);
} catch (WarehouseException e) {
log.error("Error executing putaway: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
/**
* Execute picking operation
*/
@PostMapping("/picking")
public ResponseEntity<?> executePicking(@Valid @RequestBody PickingRequestDTO pickingRequest) {
try {
var pickedItems = inventoryService.executePicking(pickingRequest);
return ResponseEntity.ok(pickedItems);
} catch (WarehouseException e) {
log.error("Error executing picking: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
/**
* Move inventory between bins
*/
@PostMapping("/move")
public ResponseEntity<?> moveInventory(@Valid @RequestBody MoveRequestDTO moveRequest) {
try {
InventoryItem movedItem = inventoryService.moveInventory(moveRequest);
return ResponseEntity.ok(movedItem);
} catch (WarehouseException e) {
log.error("Error moving inventory: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
private Map<String, Object> createErrorResponse(String message) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", true);
errorResponse.put("message", message);
return errorResponse;
}
}

8. Custom Exception

public class WarehouseException extends RuntimeException {
public WarehouseException(String message) {
super(message);
}
public WarehouseException(String message, Throwable cause) {
super(message, cause);
}
}

This comprehensive warehouse bin location system provides complete functionality for managing storage locations, inventory placement, putaway and picking operations, and optimization algorithms. The system supports multiple warehouse layouts, zone management, and sophisticated inventory tracking with various strategies for space utilization and movement optimization.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper