Architecture Overview
The solution adopts a micro-service-oriented architecture that cleanly separates concerns between the warehouse-edge layer and the cloud-control layer. The edge layer runs on industrial PCs attached to each storage zone and is implemented with Spring Boot 2.7.x. The cloud layer is a lightweight Vue 3 SPA served through Nginx and communicates with the edge services through REST and WebSocket channels.
Key design goals:
- Zero-downtime horizontal scaling of edge nodes
- Real-time inventory accuracy via RFID + computer-vision fusion
- Rule-based autonomous task assignment for AGVs and robotic arms
- End-to-end traceability with immutable audit logs
Runtime Environment
| Compnoent | Version | Purpose |
|---|---|---|
| OpenJDK | 17 LTS | JVM runtime |
| Spring Boot | 2.7.12 | Edge service framework |
| MySQL | 8.0.33 | Persistent store |
| Redis | 7.0 | Cache & message broker |
| Node.js | 18.x | Vue build toolchain |
| Nginx | 1.24 | Reverse proxy & static hosting |
Domain Model
-- Core tables
CREATE TABLE warehouse_zone (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(16) UNIQUE NOT NULL,
layout JSON NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE sku (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
barcode VARCHAR(64) UNIQUE NOT NULL,
name VARCHAR(128) NOT NULL,
length_mm INT NOT NULL,
width_mm INT NOT NULL,
height_mm INT NOT NULL,
weight_g INT NOT NULL,
INDEX idx_barcode (barcode)
);
CREATE TABLE bin (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
zone_id BIGINT NOT NULL,
code VARCHAR(16) NOT NULL,
x INT NOT NULL,
y INT NOT NULL,
z INT NOT NULL,
max_weight_g INT NOT NULL,
UNIQUE KEY uk_zone_code (zone_id, code),
FOREIGN KEY (zone_id) REFERENCES warehouse_zone(id)
);
CREATE TABLE inventory_txn (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sku_id BIGINT NOT NULL,
bin_id BIGINT NOT NULL,
delta INT NOT NULL,
txn_type ENUM('INBOUND','OUTBOUND','MOVE','ADJUST') NOT NULL,
operator VARCHAR(32) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sku_id) REFERENCES sku(id),
FOREIGN KEY (bin_id) REFERENCES bin(id)
);
Edge Service Implementation
File Storage Endpoint
package io.warehouse.edge.api;
import io.warehouse.edge.config.StorageProps;
import io.warehouse.edge.model.FileRecord;
import io.warehouse.edge.service.FileManager;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Path;
@RestController
@RequestMapping("/api/v1/assets")
@RequiredArgsConstructor
public class AssetController {
private final FileManager fileManager;
private final StorageProps props;
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public FileRecord upload(@RequestPart("file") MultipartFile file,
@RequestParam(defaultValue = "false") boolean temp) throws IOException {
Path target = fileManager.store(file, temp);
return FileRecord.builder()
.name(file.getOriginalFilename())
.path(props.getUriPath(target))
.size(file.getSize())
.build();
}
@GetMapping("/{fileName}")
public ResponseEntity<InputStreamResource> download(@PathVariable String fileName) throws IOException {
Path file = fileManager.locate(fileName);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.contentLength(fileManager.size(file))
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(new InputStreamResource(fileManager.open(file)));
}
}
Inventory Movement API
package io.warehouse.edge.api;
import io.warehouse.edge.command.MoveCommand;
import io.warehouse.edge.service.InventoryService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/inventory")
@RequiredArgsConstructor
public class InventoryController {
private final InventoryService inventoryService;
@PostMapping("/move")
public ResponseEntity<Void> move(@RequestBody @Validated MoveCommand cmd) {
inventoryService.move(cmd);
return ResponseEntity.accepted().build();
}
@GetMapping("/bin/{binCode}")
public BinSnapshot getBin(@PathVariable String binCode) {
return inventoryService.snapshot(binCode);
}
public record BinSnapshot(String binCode, List<SkuQty> items) {}
public record SkuQty(String barcode, int qty) {}
}
Front-End Highlights
Vue 3 Composition API with Pinia state management:
// stores/zone.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { api } from '@/lib/axios';
export interface Zone {
id: number;
code: string;
layout: any;
}
export const useZoneStore = defineStore('zone', () => {
const zones = ref<Zone[]>([]);
async function load() {
const { data } = await api.get<Zone[]>('/zones');
zones.value = data;
}
return { zones, load };
});
// components/BinHeatMap.vue
<template>
<svg :viewBox="viewBox">
<rect
v-for="bin in heatData"
:key="bin.code"
:x="bin.x * cellSize"
:y="bin.y * cellSize"
:width="cellSize"
:height="cellSize"
:fill="colorScale(bin.utilization)"
@click="selectBin(bin)"
/>
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useZoneStore } from '@/stores/zone';
const props = defineProps<{ zoneId: number }>();
const zoneStore = useZoneStore();
const heatData = computed(() => zoneStore.zones.find(z => z.id === props.zoneId)?.layout.bins);
const colorScale = (u: number) => `hsl(${120 - u * 120}, 80%, 50%)`;
</script>
Autonomous Task Assignmant
The rule engine is implemented using Spring’s @Component and RuleEngine from Easy Rules:
package io.warehouse.edge.rules;
import org.jeasy.rules.annotation.*;
@Rule(name = "High-Priority Outbound", description = "Fast lane for urgent outbound")
public class PriorityOutboundRule {
@Condition
public boolean isUrgent(@Fact("task") Task task) {
return task.getType() == TaskType.OUTBOUND && task.getPriority() > 80;
}
@Action
public void assignAGV(@Fact("ctx") AssignmentContext ctx) {
ctx.allocateNearestAGV();
}
}
Testing Strategy
- Unit: JUnit 5 + AssertJ for domain logic
- Integration: Testcontainers for MySQL and Redis
- Contract: OpenAPI-driven tests for REST endpoints
- End-to-End: Cypress flows covering operator workflows
Sample integration test:
@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class InventoryIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0.33");
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", mysql::getJdbcUrl);
r.add("spring.datasource.username", mysql::getUsername);
r.add("spring.datasource.password", mysql::getPassword);
}
@Test
void shouldRecordInboundTransaction() {
var txn = InboundTxn.builder()
.sku("SKU-123")
.bin("A-01-03")
.qty(50)
.build();
web.post()
.uri("/api/v1/inventory/inbound")
.bodyValue(txn)
.exchange()
.expectStatus().isAccepted();
assertThat(repo.count()).isEqualTo(1);
}
}
Deployment Pipeline
- Maven build produces layered Docker image (
paketobuildpacks) - Helm chart deploys edge service as
Deployment+Service - Canary analysis via Argo Rollouts based on error-rate SLO
- Front-end assets pushed to S3 + CloudFront invalidation
# helm/values.yaml
edge:
image:
repository: warehouse/edge
tag: "{{ .Values.tag }}"
resources:
requests:
cpu: 200m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilization: 60