google-site-verification: google61fe8ba583a51912.html
Inventory Management System in Java: Complete Enterprise Solution

A comprehensive inventory management system for tracking stock levels, orders, sales, and suppliers across multiple warehouses and locations.


System Architecture Overview

Core Modules:

  • Product Catalog - Manage products, categories, and variants
  • Inventory Tracking - Real-time stock level monitoring
  • Warehouse Management - Multi-location inventory
  • Order Management - Purchase orders and sales orders
  • Supplier Management - Vendor and supplier information
  • Reporting & Analytics - Stock levels, movements, and forecasting

Database Schema & Dependencies

1. Maven Dependencies
<properties> <spring-boot.version>3.1.0</spring-boot.version> <lombok.version>1.18.28</lombok.version> <mapstruct.version>1.5.5.Final</mapstruct.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> <!-- Database --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>test</scope> </dependency> <!-- Utilities --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <optional>true</optional> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${mapstruct.version}</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>
2. Database Entities
@Entity @Table(name = "products") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String sku; @Column(nullable = false) private String name; private String description; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id") private Category category; @Column(nullable = false) private BigDecimal price; private BigDecimal costPrice; @Enumerated(EnumType.STRING) private ProductStatus status; private String imageUrl; @Column(nullable = false) private Integer weight; // in grams @Column(nullable = false) private Dimensions dimensions; @OneToMany(mappedBy = "product", cascade = CascadeType.ALL) private List<ProductVariant> variants = new ArrayList<>(); @OneToMany(mappedBy = "product", cascade = CascadeType.ALL) private List<InventoryItem> inventoryItems = new ArrayList<>(); @CreationTimestamp private LocalDateTime createdAt; @UpdateTimestamp private LocalDateTime updatedAt; } @Embeddable @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Dimensions { private Integer length; // mm private Integer width; // mm private Integer height; // mm public Integer getVolume() { return length * width * height; } } @Entity @Table(name = "categories") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; private String description; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private Category parent; @OneToMany(mappedBy = "parent") private List<Category> children = new ArrayList<>(); @CreationTimestamp private LocalDateTime createdAt; } @Entity @Table(name = "product_variants") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class ProductVariant { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id", nullable = false) private Product product; @Column(nullable = false) private String variantSku; @Column(nullable = false) private String size; private String color; private String material; @Column(nullable = false) private BigDecimal priceAdjustment; @CreationTimestamp private LocalDateTime createdAt; } @Entity @Table(name = "warehouses") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Warehouse { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String code; @Column(nullable = false) private String name; @Embedded private Address address; private String contactPhone; private String contactEmail; @Enumerated(EnumType.STRING) private WarehouseStatus status; @Column(nullable = false) private Integer totalCapacity; // in cubic meters private Integer usedCapacity; @OneToMany(mappedBy = "warehouse") private List<InventoryItem> inventoryItems = new ArrayList<>(); @OneToMany(mappedBy = "warehouse") private List<WarehouseZone> zones = new ArrayList<>(); @CreationTimestamp private LocalDateTime createdAt; } @Entity @Table(name = "warehouse_zones") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class WarehouseZone { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "warehouse_id", nullable = false) private Warehouse warehouse; @Column(nullable = false) private String zoneCode; // e.g., "A-01", "B-02" @Column(nullable = false) private String name; @Enumerated(EnumType.STRING) private ZoneType zoneType; // STORAGE, PICKING, RECEIVING, SHIPPING private Integer capacity; @OneToMany(mappedBy = "zone") private List<InventoryItem> inventoryItems = new ArrayList<>(); } @Entity @Table(name = "inventory_items") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class InventoryItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id", nullable = false) private Product product; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "warehouse_id", nullable = false) private Warehouse warehouse; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "zone_id") private WarehouseZone zone; @Column(nullable = false) private String batchNumber; private LocalDate expiryDate; @Column(nullable = false) private Integer quantityOnHand; @Column(nullable = false) private Integer quantityReserved; @Column(nullable = false) private Integer quantityAvailable; @Column(nullable = false) private Integer minimumStockLevel; @Column(nullable = false) private Integer maximumStockLevel; @Column(nullable = false) private Integer reorderPoint; @Enumerated(EnumType.STRING) private StockStatus stockStatus; @Version private Long version; // For optimistic locking @CreationTimestamp private LocalDateTime createdAt; @UpdateTimestamp private LocalDateTime updatedAt; @PreUpdate @PrePersist public void calculateQuantities() { this.quantityAvailable = this.quantityOnHand - this.quantityReserved; updateStockStatus(); } private void updateStockStatus() { if (quantityOnHand <= 0) { this.stockStatus = StockStatus.OUT_OF_STOCK; } else if (quantityOnHand <= reorderPoint) { this.stockStatus = StockStatus.LOW_STOCK; } else if (quantityOnHand >= maximumStockLevel) { this.stockStatus = StockStatus.OVERSTOCKED; } else { this.stockStatus = StockStatus.IN_STOCK; } } } @Entity @Table(name = "inventory_movements") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class InventoryMovement { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "inventory_item_id", nullable = false) private InventoryItem inventoryItem; @Enumerated(EnumType.STRING) private MovementType movementType; @Column(nullable = false) private Integer quantity; private String referenceNumber; // PO number, Sales Order number, etc. @Enumerated(EnumType.STRING) private MovementReason reason; private String notes; @Column(nullable = false) private Integer quantityBefore; @Column(nullable = false) private Integer quantityAfter; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "created_by") private User createdBy; @CreationTimestamp private LocalDateTime createdAt; } @Entity @Table(name = "suppliers") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Supplier { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String code; @Column(nullable = false) private String name; @Embedded private Address address; private String contactPerson; private String contactEmail; private String contactPhone; @Enumerated(EnumType.STRING) private SupplierStatus status; private String paymentTerms; private Integer leadTimeDays; @OneToMany(mappedBy = "supplier") private List<PurchaseOrder> purchaseOrders = new ArrayList<>(); @CreationTimestamp private LocalDateTime createdAt; } @Entity @Table(name = "purchase_orders") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class PurchaseOrder { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String poNumber; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "supplier_id", nullable = false) private Supplier supplier; @Enumerated(EnumType.STRING) private POStatus status; @Column(nullable = false) private LocalDate orderDate; private LocalDate expectedDeliveryDate; private LocalDate actualDeliveryDate; @OneToMany(mappedBy = "purchaseOrder", cascade = CascadeType.ALL) private List<PurchaseOrderItem> items = new ArrayList<>(); @Column(nullable = false) private BigDecimal totalAmount; private String notes; @CreationTimestamp private LocalDateTime createdAt; } @Entity @Table(name = "purchase_order_items") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class PurchaseOrderItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "purchase_order_id", nullable = false) private PurchaseOrder purchaseOrder; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id", nullable = false) private Product product; @Column(nullable = false) private Integer quantityOrdered; @Column(nullable = false) private Integer quantityReceived; @Column(nullable = false) private BigDecimal unitCost; @Column(nullable = false) private BigDecimal lineTotal; } // Enums public enum ProductStatus { ACTIVE, INACTIVE, DISCONTINUED } public enum WarehouseStatus { ACTIVE, INACTIVE, MAINTENANCE } public enum ZoneType { STORAGE, PICKING, RECEIVING, SHIPPING, QUARANTINE } public enum StockStatus { IN_STOCK, LOW_STOCK, OUT_OF_STOCK, OVERSTOCKED } public enum MovementType { INBOUND, OUTBOUND, TRANSFER, ADJUSTMENT } public enum MovementReason { PURCHASE, SALE, RETURN, DAMAGED, EXPIRED, COUNT_ADJUSTMENT, TRANSFER } public enum SupplierStatus { ACTIVE, INACTIVE, BLACKLISTED } public enum POStatus { DRAFT, PENDING, APPROVED, ORDERED, PARTIALLY_RECEIVED, RECEIVED, CANCELLED }

Core Services Implementation

1. Inventory Service
public interface InventoryService { InventoryItem getInventoryItem(Long productId, Long warehouseId); List<InventoryItem> getInventoryByProduct(Long productId); List<InventoryItem> getInventoryByWarehouse(Long warehouseId); InventoryMovement addStock(StockAdjustmentRequest request); InventoryMovement removeStock(StockAdjustmentRequest request); InventoryMovement transferStock(StockTransferRequest request); List<InventoryItem> getLowStockItems(); List<InventoryItem> getOutOfStockItems(); InventorySummary getInventorySummary(); } @Service @Transactional @Slf4j public class InventoryServiceImpl implements InventoryService { private final InventoryItemRepository inventoryItemRepository; private final InventoryMovementRepository movementRepository; private final ProductRepository productRepository; private final WarehouseRepository warehouseRepository; @Override public InventoryMovement addStock(StockAdjustmentRequest request) { InventoryItem item = getOrCreateInventoryItem( request.getProductId(), request.getWarehouseId(), request.getZoneId() ); // Optimistic locking check if (!item.getVersion().equals(request.getVersion())) { throw new OptimisticLockingFailureException("Inventory item was modified by another transaction"); } int quantityBefore = item.getQuantityOnHand(); item.setQuantityOnHand(quantityBefore + request.getQuantity()); InventoryItem savedItem = inventoryItemRepository.save(item); // Record movement InventoryMovement movement = createMovement( savedItem, MovementType.INBOUND, MovementReason.PURCHASE, request.getQuantity(), quantityBefore, savedItem.getQuantityOnHand(), request.getReferenceNumber(), request.getNotes() ); log.info("Stock added: Product {}, Warehouse {}, Quantity {}", request.getProductId(), request.getWarehouseId(), request.getQuantity()); return movement; } @Override public InventoryMovement removeStock(StockAdjustmentRequest request) { InventoryItem item = inventoryItemRepository .findByProductIdAndWarehouseId(request.getProductId(), request.getWarehouseId()) .orElseThrow(() -> new InventoryNotFoundException( "Inventory item not found for product: " + request.getProductId() + " in warehouse: " + request.getWarehouseId())); if (!item.getVersion().equals(request.getVersion())) { throw new OptimisticLockingFailureException("Inventory item was modified by another transaction"); } if (item.getQuantityAvailable() < request.getQuantity()) { throw new InsufficientStockException( "Insufficient stock. Available: " + item.getQuantityAvailable() + ", Requested: " + request.getQuantity()); } int quantityBefore = item.getQuantityOnHand(); item.setQuantityOnHand(quantityBefore - request.getQuantity()); InventoryItem savedItem = inventoryItemRepository.save(item); InventoryMovement movement = createMovement( savedItem, MovementType.OUTBOUND, MovementReason.SALE, -request.getQuantity(), quantityBefore, savedItem.getQuantityOnHand(), request.getReferenceNumber(), request.getNotes() ); log.info("Stock removed: Product {}, Warehouse {}, Quantity {}", request.getProductId(), request.getWarehouseId(), request.getQuantity()); return movement; } @Override public InventoryMovement transferStock(StockTransferRequest request) { // Remove from source warehouse StockAdjustmentRequest removeRequest = StockAdjustmentRequest.builder() .productId(request.getProductId()) .warehouseId(request.getSourceWarehouseId()) .quantity(request.getQuantity()) .referenceNumber(request.getTransferNumber()) .notes("Transfer to warehouse: " + request.getDestinationWarehouseId()) .build(); InventoryMovement outboundMovement = removeStock(removeRequest); // Add to destination warehouse StockAdjustmentRequest addRequest = StockAdjustmentRequest.builder() .productId(request.getProductId()) .warehouseId(request.getDestinationWarehouseId()) .zoneId(request.getDestinationZoneId()) .quantity(request.getQuantity()) .referenceNumber(request.getTransferNumber()) .notes("Transfer from warehouse: " + request.getSourceWarehouseId()) .build(); InventoryMovement inboundMovement = addStock(addRequest); log.info("Stock transferred: Product {}, From {}, To {}, Quantity {}", request.getProductId(), request.getSourceWarehouseId(), request.getDestinationWarehouseId(), request.getQuantity()); return inboundMovement; } @Override public List<InventoryItem> getLowStockItems() { return inventoryItemRepository.findLowStockItems(); } @Override public List<InventoryItem> getOutOfStockItems() { return inventoryItemRepository.findOutOfStockItems(); } @Override public InventorySummary getInventorySummary() { return inventoryItemRepository.getInventorySummary(); } private InventoryItem getOrCreateInventoryItem(Long productId, Long warehouseId, Long zoneId) { return inventoryItemRepository .findByProductIdAndWarehouseId(productId, warehouseId) .orElseGet(() -> createNewInventoryItem(productId, warehouseId, zoneId)); } private InventoryItem createNewInventoryItem(Long productId, Long warehouseId, Long zoneId) { Product product = productRepository.findById(productId) .orElseThrow(() -> new ProductNotFoundException("Product not found: " + productId)); Warehouse warehouse = warehouseRepository.findById(warehouseId) .orElseThrow(() -> new WarehouseNotFoundException("Warehouse not found: " + warehouseId)); WarehouseZone zone = null; if (zoneId != null) { // Fetch zone if provided } return InventoryItem.builder() .product(product) .warehouse(warehouse) .zone(zone) .quantityOnHand(0) .quantityReserved(0) .quantityAvailable(0) .minimumStockLevel(10) // Default values .maximumStockLevel(1000) .reorderPoint(25) .stockStatus(StockStatus.OUT_OF_STOCK) .batchNumber("BATCH-" + System.currentTimeMillis()) .build(); } private InventoryMovement createMovement(InventoryItem item, MovementType type, MovementReason reason, Integer quantity, Integer quantityBefore, Integer quantityAfter, String reference, String notes) { InventoryMovement movement = InventoryMovement.builder() .inventoryItem(item) .movementType(type) .movementReason(reason) .quantity(quantity) .quantityBefore(quantityBefore) .quantityAfter(quantityAfter) .referenceNumber(reference) .notes(notes) .build(); return movementRepository.save(movement); } }
2. Purchase Order Service
@Service @Transactional @Slf4j public class PurchaseOrderService { private final PurchaseOrderRepository poRepository; private final PurchaseOrderItemRepository poItemRepository; private final SupplierRepository supplierRepository; private final ProductRepository productRepository; private final InventoryService inventoryService; public PurchaseOrder createPurchaseOrder(CreatePurchaseOrderRequest request) { Supplier supplier = supplierRepository.findById(request.getSupplierId()) .orElseThrow(() -> new SupplierNotFoundException("Supplier not found: " + request.getSupplierId())); PurchaseOrder po = PurchaseOrder.builder() .poNumber(generatePONumber()) .supplier(supplier) .status(POStatus.DRAFT) .orderDate(LocalDate.now()) .expectedDeliveryDate(request.getExpectedDeliveryDate()) .totalAmount(BigDecimal.ZERO) .notes(request.getNotes()) .build(); List<PurchaseOrderItem> items = new ArrayList<>(); BigDecimal totalAmount = BigDecimal.ZERO; for (PurchaseOrderItemRequest itemRequest : request.getItems()) { Product product = productRepository.findById(itemRequest.getProductId()) .orElseThrow(() -> new ProductNotFoundException("Product not found: " + itemRequest.getProductId())); BigDecimal lineTotal = itemRequest.getUnitCost().multiply( BigDecimal.valueOf(itemRequest.getQuantity())); PurchaseOrderItem item = PurchaseOrderItem.builder() .purchaseOrder(po) .product(product) .quantityOrdered(itemRequest.getQuantity()) .quantityReceived(0) .unitCost(itemRequest.getUnitCost()) .lineTotal(lineTotal) .build(); items.add(item); totalAmount = totalAmount.add(lineTotal); } po.setItems(items); po.setTotalAmount(totalAmount); return poRepository.save(po); } public PurchaseOrder approvePurchaseOrder(Long poId) { PurchaseOrder po = poRepository.findById(poId) .orElseThrow(() -> new PurchaseOrderNotFoundException("Purchase order not found: " + poId)); if (po.getStatus() != POStatus.DRAFT && po.getStatus() != POStatus.PENDING) { throw new InvalidOperationException("Only DRAFT or PENDING orders can be approved"); } po.setStatus(POStatus.APPROVED); return poRepository.save(po); } public PurchaseOrder receivePurchaseOrder(Long poId, List<ReceiveItemRequest> receivedItems) { PurchaseOrder po = poRepository.findByIdWithItems(poId) .orElseThrow(() -> new PurchaseOrderNotFoundException("Purchase order not found: " + poId)); if (po.getStatus() != POStatus.APPROVED && po.getStatus() != POStatus.ORDERED) { throw new InvalidOperationException("Only APPROVED or ORDERED orders can be received"); } boolean fullyReceived = true; for (ReceiveItemRequest receivedItem : receivedItems) { PurchaseOrderItem poItem = po.getItems().stream() .filter(item -> item.getId().equals(receivedItem.getPoItemId())) .findFirst() .orElseThrow(() -> new PurchaseOrderItemNotFoundException( "Purchase order item not found: " + receivedItem.getPoItemId())); int totalReceived = poItem.getQuantityReceived() + receivedItem.getQuantityReceived(); if (totalReceived > poItem.getQuantityOrdered()) { throw new InvalidOperationException( "Received quantity exceeds ordered quantity for item: " + poItem.getId()); } poItem.setQuantityReceived(totalReceived); // Add to inventory if (receivedItem.getQuantityReceived() > 0) { StockAdjustmentRequest stockRequest = StockAdjustmentRequest.builder() .productId(poItem.getProduct().getId()) .warehouseId(receivedItem.getWarehouseId()) .zoneId(receivedItem.getZoneId()) .quantity(receivedItem.getQuantityReceived()) .referenceNumber(po.getPoNumber()) .notes("Purchase order receipt") .build(); inventoryService.addStock(stockRequest); } if (totalReceived < poItem.getQuantityOrdered()) { fullyReceived = false; } } po.setStatus(fullyReceived ? POStatus.RECEIVED : POStatus.PARTIALLY_RECEIVED); po.setActualDeliveryDate(LocalDate.now()); return poRepository.save(po); } public List<PurchaseOrder> getPendingOrders() { return poRepository.findByStatusIn(List.of(POStatus.PENDING, POStatus.APPROVED, POStatus.ORDERED)); } private String generatePONumber() { String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); String random = String.valueOf(ThreadLocalRandom.current().nextInt(1000, 9999)); return "PO-" + timestamp + "-" + random; } }
3. Stock Reservation Service
@Service @Transactional @Slf4j public class StockReservationService { private final InventoryItemRepository inventoryItemRepository; private final StockReservationRepository reservationRepository; public StockReservation reserveStock(ReserveStockRequest request) { // Find available inventory across warehouses List<InventoryItem> availableItems = inventoryItemRepository .findByProductIdAndQuantityAvailableGreaterThanEqualOrderByQuantityAvailableDesc( request.getProductId(), request.getQuantity()); if (availableItems.isEmpty()) { throw new InsufficientStockException("Insufficient stock for product: " + request.getProductId()); } int remainingQuantity = request.getQuantity(); List<StockReservation> reservations = new ArrayList<>(); for (InventoryItem item : availableItems) { if (remainingQuantity <= 0) break; int reservableQuantity = Math.min(item.getQuantityAvailable(), remainingQuantity); // Create reservation StockReservation reservation = StockReservation.builder() .inventoryItem(item) .orderNumber(request.getOrderNumber()) .quantityReserved(reservableQuantity) .expiresAt(LocalDateTime.now().plusHours(24)) // 24-hour reservation .status(ReservationStatus.ACTIVE) .build(); reservations.add(reservationRepository.save(reservation)); // Update inventory reserved quantity item.setQuantityReserved(item.getQuantityReserved() + reservableQuantity); inventoryItemRepository.save(item); remainingQuantity -= reservableQuantity; } if (remainingQuantity > 0) { // Cancel all reservations if we couldn't reserve full quantity reservations.forEach(this::cancelReservation); throw new InsufficientStockException("Could not reserve full quantity. Only reserved: " + (request.getQuantity() - remainingQuantity)); } return reservations.get(0); // Return first reservation as reference } public void cancelReservation(Long reservationId) { StockReservation reservation = reservationRepository.findById(reservationId) .orElseThrow(() -> new ReservationNotFoundException("Reservation not found: " + reservationId)); cancelReservation(reservation); } public void cancelReservation(StockReservation reservation) { if (reservation.getStatus() == ReservationStatus.CANCELLED) { return; } InventoryItem item = reservation.getInventoryItem(); item.setQuantityReserved(item.getQuantityReserved() - reservation.getQuantityReserved()); inventoryItemRepository.save(item); reservation.setStatus(ReservationStatus.CANCELLED); reservationRepository.save(reservation); log.info("Stock reservation cancelled: {}", reservation.getId()); } public void commitReservation(Long reservationId) { StockReservation reservation = reservationRepository.findById(reservationId) .orElseThrow(() -> new ReservationNotFoundException("Reservation not found: " + reservationId)); if (reservation.getStatus() != ReservationStatus.ACTIVE) { throw new InvalidOperationException("Only active reservations can be committed"); } // Mark reservation as committed reservation.setStatus(ReservationStatus.COMMITTED); reservationRepository.save(reservation); log.info("Stock reservation committed: {}", reservation.getId()); } @Scheduled(fixedRate = 3600000) // Run every hour public void cleanupExpiredReservations() { List<StockReservation> expiredReservations = reservationRepository .findExpiredReservations(LocalDateTime.now()); for (StockReservation reservation : expiredReservations) { cancelReservation(reservation); log.info("Expired reservation cleaned up: {}", reservation.getId()); } } }
4. Reporting Service
@Service @Transactional(readOnly = true) @Slf4j public class InventoryReportingService { private final InventoryItemRepository inventoryItemRepository; private final InventoryMovementRepository movementRepository; private final PurchaseOrderRepository poRepository; public InventoryReport generateInventoryReport(ReportRequest request) { List<InventoryItem> items = inventoryItemRepository.findAll(); InventoryReport report = new InventoryReport(); report.setGeneratedAt(LocalDateTime.now()); report.setTotalProducts(items.stream().map(InventoryItem::getProduct).distinct().count()); report.setTotalValue(calculateTotalInventoryValue(items)); report.setLowStockCount(countLowStockItems(items)); report.setOutOfStockCount(countOutOfStockItems(items)); // Group by warehouse Map<String, InventorySummary> warehouseSummary = items.stream() .collect(Collectors.groupingBy( item -> item.getWarehouse().getName(), Collectors.collectingAndThen(Collectors.toList(), this::createWarehouseSummary) )); report.setWarehouseSummary(warehouseSummary); return report; } public MovementReport generateMovementReport(MovementReportRequest request) { List<InventoryMovement> movements = movementRepository.findByCreatedAtBetween( request.getStartDate().atStartOfDay(), request.getEndDate().atTime(23, 59, 59) ); MovementReport report = new MovementReport(); report.setPeriodStart(request.getStartDate()); report.setPeriodEnd(request.getEndDate()); report.setTotalMovements(movements.size()); // Group by movement type Map<MovementType, Long> movementsByType = movements.stream() .collect(Collectors.groupingBy(InventoryMovement::getMovementType, Collectors.counting())); report.setMovementsByType(movementsByType); // Calculate net change int netChange = movements.stream() .mapToInt(InventoryMovement::getQuantity) .sum(); report.setNetQuantityChange(netChange); return report; } public List<ReplenishmentRecommendation> getReplenishmentRecommendations() { List<InventoryItem> lowStockItems = inventoryItemRepository.findLowStockItems(); return lowStockItems.stream() .map(this::createReplenishmentRecommendation) .collect(Collectors.toList()); } private InventorySummary createWarehouseSummary(List<InventoryItem> items) { BigDecimal totalValue = items.stream() .map(item -> item.getProduct().getPrice().multiply(BigDecimal.valueOf(item.getQuantityOnHand()))) .reduce(BigDecimal.ZERO, BigDecimal::add); long lowStockCount = items.stream() .filter(item -> item.getStockStatus() == StockStatus.LOW_STOCK) .count(); long outOfStockCount = items.stream() .filter(item -> item.getStockStatus() == StockStatus.OUT_OF_STOCK) .count(); return InventorySummary.builder() .totalItems(items.size()) .totalValue(totalValue) .lowStockCount(lowStockCount) .outOfStockCount(outOfStockCount) .build(); } private ReplenishmentRecommendation createReplenishmentRecommendation(InventoryItem item) { int recommendedQuantity = item.getMaximumStockLevel() - item.getQuantityOnHand(); ReplenishmentPriority priority = calculateReplenishmentPriority(item); return ReplenishmentRecommendation.builder() .product(item.getProduct()) .warehouse(item.getWarehouse()) .currentStock(item.getQuantityOnHand()) .reorderPoint(item.getReorderPoint()) .recommendedQuantity(recommendedQuantity) .priority(priority) .urgency(getUrgencyLevel(item)) .build(); } private ReplenishmentPriority calculateReplenishmentPriority(InventoryItem item) { double stockRatio = (double) item.getQuantityOnHand() / item.getReorderPoint(); if (stockRatio <= 0.1) return ReplenishmentPriority.CRITICAL; if (stockRatio <= 0.3) return ReplenishmentPriority.HIGH; if (stockRatio <= 0.6) return ReplenishmentPriority.MEDIUM; return ReplenishmentPriority.LOW; } private String getUrgencyLevel(InventoryItem item) { int daysOfStock = (int) (item.getQuantityOnHand() / getAverageDailyUsage(item.getProduct().getId())); if (daysOfStock <= 1) return "IMMEDIATE"; if (daysOfStock <= 3) return "URGENT"; if (daysOfStock <= 7) return "SOON"; return "NORMAL"; } private double getAverageDailyUsage(Long productId) { // Implementation to calculate average daily sales/usage return 10.0; // Default value } private BigDecimal calculateTotalInventoryValue(List<InventoryItem> items) { return items.stream() .map(item -> item.getProduct().getPrice().multiply(BigDecimal.valueOf(item.getQuantityOnHand()))) .reduce(BigDecimal.ZERO, BigDecimal::add); } private long countLowStockItems(List<InventoryItem> items) { return items.stream() .filter(item -> item.getStockStatus() == StockStatus.LOW_STOCK) .count(); } private long countOutOfStockItems(List<InventoryItem> items) { return items.stream() .filter(item -> item.getStockStatus() == StockStatus.OUT_OF_STOCK) .count(); } }

REST Controllers

1. Inventory Controller
@RestController @RequestMapping("/api/inventory") @Validated @Slf4j public class InventoryController { private final InventoryService inventoryService; private final StockReservationService reservationService; @PostMapping("/stock/add") public ResponseEntity<InventoryMovement> addStock(@Valid @RequestBody StockAdjustmentRequest request) { InventoryMovement movement = inventoryService.addStock(request); return ResponseEntity.ok(movement); } @PostMapping("/stock/remove") public ResponseEntity<InventoryMovement> removeStock(@Valid @RequestBody StockAdjustmentRequest request) { InventoryMovement movement = inventoryService.removeStock(request); return ResponseEntity.ok(movement); } @PostMapping("/stock/transfer") public ResponseEntity<InventoryMovement> transferStock(@Valid @RequestBody StockTransferRequest request) { InventoryMovement movement = inventoryService.transferStock(request); return ResponseEntity.ok(movement); } @PostMapping("/reservations") public ResponseEntity<StockReservation> reserveStock(@Valid @RequestBody ReserveStockRequest request) { StockReservation reservation = reservationService.reserveStock(request); return ResponseEntity.ok(reservation); } @DeleteMapping("/reservations/{reservationId}") public ResponseEntity<Void> cancelReservation(@PathVariable Long reservationId) { reservationService.cancelReservation(reservationId); return ResponseEntity.noContent().build(); } @GetMapping("/products/{productId}/stock") public ResponseEntity<List<InventoryItem>> getProductInventory(@PathVariable Long productId) { List<InventoryItem> inventory = inventoryService.getInventoryByProduct(productId); return ResponseEntity.ok(inventory); } @GetMapping("/warehouses/{warehouseId}/stock") public ResponseEntity<List<InventoryItem>> getWarehouseInventory(@PathVariable Long warehouseId) { List<InventoryItem> inventory = inventoryService.getInventoryByWarehouse(warehouseId); return ResponseEntity.ok(inventory); } @GetMapping("/alerts/low-stock") public ResponseEntity<List<InventoryItem>> getLowStockAlerts() { List<InventoryItem> lowStockItems = inventoryService.getLowStockItems(); return ResponseEntity.ok(lowStockItems); } @GetMapping("/alerts/out-of-stock") public ResponseEntity<List<InventoryItem>> getOutOfStockAlerts() { List<InventoryItem> outOfStockItems = inventoryService.getOutOfStockItems(); return ResponseEntity.ok(outOfStockItems); } }
2. Purchase Order Controller
@RestController @RequestMapping("/api/purchase-orders") @Validated @Slf4j public class PurchaseOrderController { private final PurchaseOrderService purchaseOrderService; @PostMapping public ResponseEntity<PurchaseOrder> createPurchaseOrder( @Valid @RequestBody CreatePurchaseOrderRequest request) { PurchaseOrder po = purchaseOrderService.createPurchaseOrder(request); return ResponseEntity.status(HttpStatus.CREATED).body(po); } @PutMapping("/{poId}/approve") public ResponseEntity<PurchaseOrder> approvePurchaseOrder(@PathVariable Long poId) { PurchaseOrder po = purchaseOrderService.approvePurchaseOrder(poId); return ResponseEntity.ok(po); } @PutMapping("/{poId}/receive") public ResponseEntity<PurchaseOrder> receivePurchaseOrder( @PathVariable Long poId, @Valid @RequestBody List<ReceiveItemRequest> receivedItems) { PurchaseOrder po = purchaseOrderService.receivePurchaseOrder(poId, receivedItems); return ResponseEntity.ok(po); } @GetMapping("/pending") public ResponseEntity<List<PurchaseOrder>> getPendingOrders() { List<PurchaseOrder> orders = purchaseOrderService.getPendingOrders(); return ResponseEntity.ok(orders); } @GetMapping("/{poId}") public ResponseEntity<PurchaseOrder> getPurchaseOrder(@PathVariable Long poId) { PurchaseOrder po = purchaseOrderService.getPurchaseOrder(poId); return ResponseEntity.ok(po); } }
3. Reporting Controller
@RestController @RequestMapping("/api/reports") @Slf4j public class ReportingController { private final InventoryReportingService reportingService; @GetMapping("/inventory") public ResponseEntity<InventoryReport> generateInventoryReport( @RequestParam(defaultValue = "false") boolean includeDetails) { ReportRequest request = new ReportRequest(includeDetails); InventoryReport report = reportingService.generateInventoryReport(request); return ResponseEntity.ok(report); } @GetMapping("/movements") public ResponseEntity<MovementReport> generateMovementReport( @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { MovementReportRequest request = new MovementReportRequest(startDate, endDate); MovementReport report = reportingService.generateMovementReport(request); return ResponseEntity.ok(report); } @GetMapping("/replenishment") public ResponseEntity<List<ReplenishmentRecommendation>> getReplenishmentRecommendations() { List<ReplenishmentRecommendation> recommendations = reportingService.getReplenishmentRecommendations(); return ResponseEntity.ok(recommendations); } }

Configuration & Cache

1. Cache Configuration
@Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); cacheManager.setCacheNames(Arrays.asList( "products", "inventory", "warehouses", "categories", "suppliers" )); return cacheManager; } }
2. Application Configuration
@Configuration @EnableAsync @EnableScheduling public class AppConfig { @Bean public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(25); executor.setThreadNamePrefix("inventory-"); return executor; } @Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { return builder -> { builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss"); builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); builder.serializers(new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)); }; } }

Testing

1. Service Unit Tests
@ExtendWith(MockitoExtension.class) class InventoryServiceTest { @Mock private InventoryItemRepository inventoryItemRepository; @Mock private InventoryMovementRepository movementRepository; @Mock private ProductRepository productRepository; @Mock private WarehouseRepository warehouseRepository; @InjectMocks private InventoryServiceImpl inventoryService; @Test void whenAddStock_thenInventoryUpdated() { // Given Long productId = 1L; Long warehouseId = 1L; StockAdjustmentRequest request = StockAdjustmentRequest.builder() .productId(productId) .warehouseId(warehouseId) .quantity(10) .build(); InventoryItem existingItem = InventoryItem.builder() .id(1L) .quantityOnHand(5) .quantityReserved(0) .version(1L) .build(); when(inventoryItemRepository.findByProductIdAndWarehouseId(productId, warehouseId)) .thenReturn(Optional.of(existingItem)); // When inventoryService.addStock(request); // Then verify(inventoryItemRepository).save(argThat(item -> item.getQuantityOnHand() == 15)); verify(movementRepository).save(any(InventoryMovement.class)); } @Test void whenInsufficientStock_thenThrowException() { // Given StockAdjustmentRequest request = StockAdjustmentRequest.builder() .productId(1L) .warehouseId(1L) .quantity(20) .build(); InventoryItem item = InventoryItem.builder() .quantityOnHand(5) .quantityReserved(0) .quantityAvailable(5) .version(1L) .build(); when(inventoryItemRepository.findByProductIdAndWarehouseId(any(), any())) .thenReturn(Optional.of(item)); // When & Then assertThrows(InsufficientStockException.class, () -> { inventoryService.removeStock(request); }); } }

Best Practices Implemented

  1. Optimistic Locking: Prevent race conditions with @Version
  2. Caching: Improve performance for frequently accessed data
  3. Async Processing: Handle background tasks efficiently
  4. Comprehensive Logging: Track all inventory movements
  5. Validation: Ensure data integrity with Bean Validation
  6. Error Handling: Custom exceptions for business logic errors
  7. Scheduled Tasks: Automated cleanup and maintenance
  8. Reporting: Comprehensive analytics and insights

This inventory management system provides a robust foundation that can scale from small businesses to enterprise-level operations with multiple warehouses, complex product catalogs, and sophisticated reporting requirements.

Java Observability, Logging Intelligence & AI-Driven Monitoring (APM, Tracing, Logs & Anomaly Detection)

https://macronepal.com/blog/beyond-metrics-observing-serverless-and-traditional-java-applications-with-thundra-apm/
Explains using Thundra APM to observe both serverless and traditional Java applications by combining tracing, metrics, and logs into a unified observability platform for faster debugging and performance insights.

https://macronepal.com/blog/dynatrace-oneagent-in-java-2/
Explains Dynatrace OneAgent for Java, which automatically instruments JVM applications to capture metrics, traces, and logs, enabling full-stack monitoring and root-cause analysis with minimal configuration.

https://macronepal.com/blog/lightstep-java-sdk-distributed-tracing-and-observability-implementation/
Explains Lightstep Java SDK for distributed tracing, helping developers track requests across microservices and identify latency issues using OpenTelemetry-based observability.

https://macronepal.com/blog/honeycomb-io-beeline-for-java-complete-guide-2/
Explains Honeycomb Beeline for Java, which provides high-cardinality observability and deep query capabilities to understand complex system behavior and debug distributed systems efficiently.

https://macronepal.com/blog/lumigo-for-serverless-in-java-complete-distributed-tracing-guide-2/
Explains Lumigo for Java serverless applications, offering automatic distributed tracing, log correlation, and error tracking to simplify debugging in cloud-native environments. (Lumigo Docs)

https://macronepal.com/blog/from-noise-to-signals-implementing-log-anomaly-detection-in-java-applications/
Explains how to detect anomalies in Java logs using behavioral patterns and machine learning techniques to separate meaningful incidents from noisy log data and improve incident response.

https://macronepal.com/blog/ai-powered-log-analysis-in-java-from-reactive-debugging-to-proactive-insights/
Explains AI-driven log analysis for Java applications, shifting from manual debugging to predictive insights that identify issues early and improve system reliability using intelligent log processing.

https://macronepal.com/blog/titliel-java-logging-best-practices/
Explains best practices for Java logging, focusing on structured logs, proper log levels, performance optimization, and ensuring logs are useful for debugging and observability systems.

https://macronepal.com/blog/seeking-a-loguru-for-java-the-quest-for-elegant-and-simple-logging/
Explains the search for simpler, more elegant logging frameworks in Java, comparing modern logging approaches that aim to reduce complexity while improving readability and developer experience.

Leave a Reply

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


Macro Nepal Helper