WMS/WES Service Decoupling Plan

PakLog Warehouse Management System Architectural Refactoring

Document Version: 1.0.0 Created: 2025-01-18 Status: DRAFT Estimated Duration: 6 months Risk Level: HIGH - Core system refactoring


Executive Summary

This document outlines the comprehensive plan to decouple warehouse management functionality into separate WMS (Warehouse Management System) and WES (Warehouse Execution System) services. The current architecture mixes strategic planning and execution concerns, creating deployment coupling, scalability issues, and unclear ownership boundaries.

Key Goals

  1. Clear Separation of Concerns: WMS for strategic planning, WES for real-time execution
  2. Independent Scalability: Allow WES to scale independently for high-throughput operations
  3. Team Autonomy: Clear service ownership and boundaries
  4. Future Readiness: Prepare for automation, robotics, and multi-agent orchestration (MAO)

Success Criteria


Current State Analysis

Existing Service Architecture

1
2
3
4
5
6
paklog/
├── order-management/        ✅ Correctly aligned with WMS
├── inventory/              ✅ Correctly aligned with WMS
├── product-catalog/        ✅ Separate concern
├── cartonization/         ✅ Separate concern
└── shipment-transportation/ ✅ Separate concern

Key Problems

  1. Mixed Responsibilities: Strategic planning coupled with execution
  2. Deployment Coupling: Cannot deploy WMS changes without affecting WES
  3. Scaling Limitations: Cannot scale execution independently from planning
  4. Team Bottlenecks: Multiple teams working on same codebase

Target State Architecture

Service Decomposition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
paklog/
├── wms-services/
│   ├── order-management/        (existing - no change)
│   ├── inventory-management/    (existing - no change)
│   ├── wave-planning/           ✨ NEW - Strategic wave planning
│   ├── location-master/         ✨ NEW - Location configuration
│   └── workload-planning/       ✨ NEW - Capacity and labor planning
│
├── wes-services/
│   ├── task-execution/          ✨ NEW - Task orchestration and assignment
│   ├── pick-execution/          ✨ NEW - Picking operations + put wall
│   ├── pack-ship/              ✨ NEW - Packing + quality + shipping
│   ├── physical-tracking/       ✨ NEW - License plates + location state
│   └── material-handling/       ✨ NEW - Future automation (Phase 2)
│
├── shared-services/
│   ├── quality-management/      ✨ NEW - Quality inspection workflows
│   └── location-tracking/       ✨ NEW - Real-time location state
│
└── common/
    ├── paklog-events/           ✨ NEW - Shared event definitions
    ├── paklog-domain/           ✨ NEW - Common domain models
    └── paklog-integration/      ✨ NEW - Integration patterns

Migration Strategy

Approach: Strangler Fig Pattern

  1. Create new services alongside existing monolith
  2. Implement proxy/facade pattern for gradual migration
  3. Use feature flags for traffic routing
  4. Shadow mode for validation before cutover
  5. Gradual decommissioning of old code

Risk Mitigation


Project 1: Wave Planning Service (WMS)

Overview

Create a dedicated WMS service for wave planning and workload management to handle strategic planning decisions.

Scope

In Scope:

Out of Scope:

Technical Implementation

1.1 Service Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
wave-planning-service/
├── src/main/java/com/paklog/wms/wave/
   ├── domain/
      ├── aggregate/
         ├── Wave.java
         ├── WavePlan.java
         └── WaveStrategy.java
      ├── entity/
         ├── WaveMetrics.java
         └── CarrierCutoff.java
      ├── valueobject/
         ├── WaveId.java
         ├── WaveStatus.java
         └── Priority.java
      ├── service/
         ├── WavePlanningService.java
         ├── CapacityCalculationService.java
         └── WorkloadForecastingService.java
      └── repository/
          └── WaveRepository.java
   
   ├── application/
      ├── command/
         ├── CreateWaveCommand.java
         ├── ReleaseWaveCommand.java
         └── CancelWaveCommand.java
      ├── query/
         ├── GetWaveStatusQuery.java
         └── ListPendingWavesQuery.java
      └── saga/
          └── WaveReleaseSaga.java
   
   ├── infrastructure/
      ├── persistence/
         └── MongoWaveRepository.java
      ├── messaging/
         ├── KafkaEventPublisher.java
         └── OrderEventListener.java
      └── rest/
          └── WaveController.java
   
   └── integration/
       ├── inventory/
          └── InventoryServiceClient.java
       └── order/
           └── OrderServiceClient.java

├── src/main/resources/
   ├── application.yml
   └── db/migration/
       └── V1__create_wave_tables.sql

└── src/test/java/
    ├── unit/
    ├── integration/
    └── contract/

1.2 Domain Model Migration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Core Wave Aggregate
@AggregateRoot
@Document(collection = "waves")
public class Wave {
    @Id
    private String waveId;
    private WaveStatus status;
    private List<String> orderIds;
    private WavePriority priority;
    private WaveStrategy strategy;
    private LocalDateTime plannedReleaseTime;
    private LocalDateTime actualReleaseTime;
    private String assignedWarehouseId;
    private String assignedZone;
    private WaveMetrics metrics;
    private int version; // Optimistic locking

    // State transitions
    public void plan(List<String> orderIds, WaveStrategy strategy) {
        // Business logic for wave planning
        validateOrders(orderIds);
        this.orderIds = orderIds;
        this.strategy = strategy;
        this.status = WaveStatus.PLANNED;
        registerEvent(new WavePlannedEvent(this));
    }

    public void release() {
        ensureStatus(WaveStatus.PLANNED);
        ensureInventoryAllocated();
        this.status = WaveStatus.RELEASED;
        this.actualReleaseTime = LocalDateTime.now();
        registerEvent(new WaveReleasedEvent(this));
    }

    public void complete() {
        ensureStatus(WaveStatus.IN_PROGRESS);
        this.status = WaveStatus.COMPLETED;
        registerEvent(new WaveCompletedEvent(this));
    }
}

1.3 Event Definitions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Events Published
@CloudEvent
public class WavePlannedEvent {
    private String waveId;
    private List<String> orderIds;
    private String warehouseId;
    private String strategy;
    private LocalDateTime plannedReleaseTime;
}

@CloudEvent
public class WaveReleasedEvent {
    private String waveId;
    private List<String> orderIds;
    private String warehouseId;
    private String assignedZone;
    private Priority priority;
}

// Events Consumed
- FulfillmentOrderValidatedEvent (from Order Management)
- InventoryAllocatedEvent (from Inventory)
- PickingCompletedEvent (from Pick Execution - future)

1.4 API Contracts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
openapi: 3.0.0
paths:
  /api/v1/waves:
    post:
      summary: Create new wave
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWaveRequest'

  /api/v1/waves/{waveId}/release:
    post:
      summary: Release wave for execution
      parameters:
        - name: waveId
          in: path
          required: true

  /api/v1/waves/{waveId}:
    get:
      summary: Get wave details
      parameters:
        - name: waveId
          in: path
          required: true

1.5 Migration Steps

Phase 1.1: Setup (Week 1-2)

1
2
3
4
5
6
7
8
9
10
# Create new repository
git init wave-planning-service
cd wave-planning-service

# Setup Spring Boot project
spring init --dependencies=web,data-mongodb,kafka,actuator \
  --groupId=com.paklog.wms --artifactId=wave-planning-service

# Create domain models
mkdir -p ./src/main/java/com/paklog/wms/wave/domain

Phase 1.2: Shadow Mode (Week 3-4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// In existing service
@EventListener
public void onWaveCreated(WaveCreatedEvent event) {
    // Existing logic continues
    processWaveLocally(event);

    // Also forward to new service (shadow mode)
    wavePlanningServiceClient.createWave(event);

    // Compare results
    metricsCollector.compareWaveResults(
        localResult,
        remoteResult
    );
}

Phase 1.3: Gradual Cutover (Week 5-6)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Feature flag based routing
@Service
public class WaveServiceRouter {
    @Value("${feature.wave-planning.percentage:0}")
    private int routingPercentage;

    public WaveResponse processWave(WaveRequest request) {
        if (shouldRouteToNew(request.getCustomerId())) {
            return wavePlanningService.process(request);
        } else {
            return legacyWaveService.process(request);
        }
    }

    private boolean shouldRouteToNew(String customerId) {
        return customerId.hashCode() % 100 < routingPercentage;
    }
}

Success Metrics

Timeline


Project 2: Task Execution Service (WES)

Overview

Create a unified task execution service that consolidates work management from multiple contexts into a single WES service responsible for all task orchestration and assignment.

Scope

In Scope:

Out of Scope:

Technical Implementation

2.1 Service Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
task-execution-service/
├── src/main/java/com/paklog/wes/task/
   ├── domain/
      ├── aggregate/
         ├── WorkTask.java
         └── TaskAssignment.java
      ├── entity/
         ├── TaskQueue.java
         └── Associate.java
      ├── valueobject/
         ├── TaskType.java
         ├── TaskStatus.java
         └── Priority.java
      ├── service/
         ├── TaskOrchestrationService.java
         ├── TaskAssignmentService.java
         └── TaskOptimizationService.java
      └── factory/
          ├── PickTaskFactory.java
          ├── PackTaskFactory.java
          └── ReplenishTaskFactory.java
   
   ├── application/
      ├── command/
         ├── CreateTaskCommand.java
         ├── AssignTaskCommand.java
         └── CompleteTaskCommand.java
      ├── query/
         ├── GetAssignedTasksQuery.java
         └── GetTaskQueueQuery.java
      └── handler/
          ├── WaveReleasedHandler.java
          └── InventoryEventHandler.java
   
   └── infrastructure/
       ├── api/
          ├── TaskController.java
          └── MobileTaskController.java
       └── messaging/
           └── TaskEventPublisher.java

2.2 Unified Task Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@AggregateRoot
@Document(collection = "work_tasks")
public class WorkTask {
    @Id
    private String taskId;
    private TaskType type; // PICK, PACK, PUTAWAY, COUNT, REPLENISH
    private TaskStatus status;
    private Priority priority;
    private String assignedTo;
    private LocalDateTime createdAt;
    private LocalDateTime assignedAt;
    private LocalDateTime startedAt;
    private LocalDateTime completedAt;
    private String referenceId; // Wave, Order, etc.
    private Map<String, Object> taskContext; // Type-specific data
    private Duration estimatedDuration;
    private Location taskLocation;

    // Polymorphic task creation
    public static WorkTask createPickTask(
        String waveId,
        List<PickInstruction> instructions
    ) {
        WorkTask task = new WorkTask();
        task.type = TaskType.PICK;
        task.referenceId = waveId;
        task.taskContext = Map.of("instructions", instructions);
        task.priority = calculatePickPriority(instructions);
        return task;
    }

    // Task lifecycle
    public void assign(String associateId) {
        ensureStatus(TaskStatus.PENDING);
        validateAssociate(associateId);
        this.assignedTo = associateId;
        this.assignedAt = LocalDateTime.now();
        this.status = TaskStatus.ASSIGNED;
        registerEvent(new TaskAssignedEvent(this));
    }

    public void start() {
        ensureStatus(TaskStatus.ASSIGNED);
        this.startedAt = LocalDateTime.now();
        this.status = TaskStatus.IN_PROGRESS;
        registerEvent(new TaskStartedEvent(this));
    }

    public void complete(Map<String, Object> results) {
        ensureStatus(TaskStatus.IN_PROGRESS);
        this.completedAt = LocalDateTime.now();
        this.status = TaskStatus.COMPLETED;
        this.taskContext.putAll(results);
        registerEvent(new TaskCompletedEvent(this));
    }
}

2.3 Task Assignment Engine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@Service
public class TaskAssignmentService {

    public TaskAssignment assignTask(WorkTask task) {
        // Get available associates
        List<Associate> availableAssociates = associateRepository
            .findByStatusAndSkills(
                AssociateStatus.AVAILABLE,
                task.getRequiredSkills()
            );

        // Score each associate
        List<AssociateScore> scores = availableAssociates.stream()
            .map(associate -> scoreAssociate(associate, task))
            .sorted(Comparator.reverseOrder())
            .collect(Collectors.toList());

        // Assign to best match
        Associate selected = scores.get(0).getAssociate();
        task.assign(selected.getId());

        // Update associate status
        selected.assignTask(task.getTaskId());

        // Send notification
        notificationService.notifyAssociate(selected, task);

        return new TaskAssignment(task, selected);
    }

    private AssociateScore scoreAssociate(
        Associate associate,
        WorkTask task
    ) {
        double score = 0;

        // Distance to task location
        score += scoreDistance(
            associate.getCurrentLocation(),
            task.getTaskLocation()
        );

        // Skill match
        score += scoreSkillMatch(
            associate.getSkills(),
            task.getRequiredSkills()
        );

        // Current workload
        score += scoreWorkload(
            associate.getCurrentTasks()
        );

        // Historical performance
        score += scorePerformance(
            associate.getProductivityMetrics(),
            task.getType()
        );

        return new AssociateScore(associate, score);
    }
}

2.4 Event Integration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class WaveEventHandler {

    @EventHandler
    public void handle(WaveReleasedEvent event) {
        // Generate pick tasks from wave
        List<WorkTask> pickTasks = pickTaskFactory
            .createTasksFromWave(event);

        // Save tasks
        pickTasks.forEach(taskRepository::save);

        // Assign tasks
        pickTasks.forEach(taskAssignmentService::assignTask);

        // Publish task creation events
        pickTasks.forEach(task ->
            eventPublisher.publish(new TaskCreatedEvent(task))
        );
    }
}

Migration Strategy

  1. Create unified Work Management Context
  2. Unify task models from Pick, Pack, and other contexts
  3. Consolidate task assignment logic
  4. Create mobile API for associates
  5. Implement real-time tracking

Timeline


Project 3: Location Services Split

Overview

Split the current Location context into two services: Location Master (WMS) for configuration and Physical Tracking (WES) for real-time state.

Scope

3.1 Location Master Service (WMS)

In Scope:

Domain Model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@AggregateRoot
@Document(collection = "location_masters")
public class LocationMaster {
    @Id
    private String locationId;
    private String warehouseId;
    private String zone;
    private String aisle;
    private String bay;
    private String bin;
    private LocationType type; // PICK, RESERVE, STAGE, DOCK
    private Dimensions dimensions;
    private WeightCapacity weightCapacity;
    private VolumeCapacity volumeCapacity;
    private List<String> restrictions; // HAZMAT, FROZEN, etc.
    private SlottingClass slottingClass; // A, B, C
    private boolean active;

    // Configuration methods
    public void configureCapacity(Capacity newCapacity) {
        validateCapacity(newCapacity);
        this.weightCapacity = newCapacity.getWeight();
        this.volumeCapacity = newCapacity.getVolume();
        registerEvent(new LocationCapacityConfiguredEvent(this));
    }

    public void assignSlottingClass(SlottingClass newClass) {
        this.slottingClass = newClass;
        registerEvent(new LocationSlottingChangedEvent(this));
    }
}

3.2 Physical Tracking Service (WES)

In Scope:

Domain Model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@AggregateRoot
@Document(collection = "location_states")
public class LocationState {
    @Id
    private String locationId; // Links to LocationMaster
    private OccupancyStatus occupancyStatus;
    private int currentItemCount;
    private double currentWeight;
    private double currentVolume;
    private List<String> licensePlates;
    private LocationStatus status; // AVAILABLE, OCCUPIED, BLOCKED
    private String blockedReason;
    private LocalDateTime lastMovement;
    private LocalDateTime lastCycleCount;

    // State management
    public void recordMovement(Movement movement) {
        updateOccupancy(movement);
        this.lastMovement = LocalDateTime.now();
        registerEvent(new LocationMovementEvent(this, movement));
    }

    public void blockLocation(String reason) {
        this.status = LocationStatus.BLOCKED;
        this.blockedReason = reason;
        registerEvent(new LocationBlockedEvent(this));
    }
}

@AggregateRoot
@Document(collection = "license_plates")
public class LicensePlate {
    @Id
    private String licensePlateId;
    private LicensePlateType type; // PALLET, CASE, CARTON, TOTE
    private List<LPItem> contents;
    private String currentLocationId;
    private LPStatus status;
    private String parentLicensePlateId; // For nested LPs
    private LocalDateTime createdAt;
    private LocalDateTime lastMovedAt;

    public void moveTo(String newLocationId) {
        validateLocation(newLocationId);
        String oldLocation = this.currentLocationId;
        this.currentLocationId = newLocationId;
        this.lastMovedAt = LocalDateTime.now();
        registerEvent(new LicensePlateMoved(this, oldLocation, newLocationId));
    }
}

Integration Pattern

1
2
3
4
5
6
7
8
9
10
11
12
// Bi-directional synchronization
Location Master Service (WMS) ←→ Physical Tracking Service (WES)

// Events from Location Master → Physical Tracking
- LocationCreatedEvent
- LocationCapacityChangedEvent
- LocationDeactivatedEvent

// Events from Physical Tracking → Location Master
- LocationCapacityExceededEvent
- LocationBlockedEvent
- AbnormalInventoryDetectedEvent

Migration Steps

  1. Data Model Separation: Split location table into master and state
  2. Dual Write Phase: Both services write during transition
  3. Read Migration: Gradually migrate reads to appropriate service
  4. Event Synchronization: Implement bi-directional sync
  5. Legacy Cleanup: Remove old location context

Timeline


Project 4: Pick Execution Service (WES)

Overview

Extract picking and put wall operations into a dedicated WES service for pick execution optimization.

Scope

In Scope:

Technical Implementation

4.1 Domain Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@AggregateRoot
@Document(collection = "pick_sessions")
public class PickSession {
    @Id
    private String sessionId;
    private String pickListId;
    private String pickerId;
    private List<PickInstruction> instructions;
    private PickStrategy strategy; // DISCRETE, BATCH, ZONE, CLUSTER
    private PickPath optimizedPath;
    private SessionStatus status;
    private List<PickConfirmation> confirmations;
    private LocalDateTime startedAt;
    private LocalDateTime completedAt;
    private PickMetrics metrics;

    public void optimizePath(LocationGraph graph) {
        this.optimizedPath = pathOptimizer.optimize(
            instructions,
            graph,
            strategy
        );
        reorderInstructions();
    }

    public void confirmPick(
        String instructionId,
        int quantity,
        String barcode
    ) {
        PickInstruction instruction = findInstruction(instructionId);
        validateBarcode(barcode, instruction.getSku());

        if (quantity < instruction.getQuantity()) {
            handleShortPick(instruction, quantity);
        }

        confirmations.add(new PickConfirmation(
            instructionId,
            quantity,
            LocalDateTime.now()
        ));

        if (allInstructionsComplete()) {
            complete();
        }
    }
}

@Entity
public class PutWallSlot {
    private String slotId;
    private int slotNumber;
    private String assignedOrderId;
    private SlotStatus status;
    private List<SlotItem> items;
    private int expectedItemCount;
    private int currentItemCount;

    public void addItem(String sku, int quantity) {
        validateSku(sku);
        items.add(new SlotItem(sku, quantity));
        currentItemCount += quantity;

        if (currentItemCount >= expectedItemCount) {
            status = SlotStatus.COMPLETE;
            registerEvent(new PutWallOrderCompleteEvent(
                assignedOrderId,
                slotId
            ));
        }
    }
}

4.2 Path Optimization Engine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Service
public class PickPathOptimizer {

    public PickPath optimize(
        List<PickInstruction> instructions,
        LocationGraph warehouse,
        PickStrategy strategy
    ) {
        return switch (strategy) {
            case DISCRETE -> optimizeDiscrete(instructions, warehouse);
            case BATCH -> optimizeBatch(instructions, warehouse);
            case ZONE -> optimizeZone(instructions, warehouse);
            case CLUSTER -> optimizeCluster(instructions, warehouse);
        };
    }

    private PickPath optimizeBatch(
        List<PickInstruction> instructions,
        LocationGraph warehouse
    ) {
        // Group by proximity
        List<LocationCluster> clusters = clusterLocations(
            instructions.stream()
                .map(PickInstruction::getLocation)
                .collect(Collectors.toList())
        );

        // Solve TSP for each cluster
        List<Location> optimizedSequence = new ArrayList<>();
        for (LocationCluster cluster : clusters) {
            List<Location> clusterPath = solveTSP(
                cluster.getLocations(),
                warehouse
            );
            optimizedSequence.addAll(clusterPath);
        }

        return new PickPath(
            optimizedSequence,
            calculateDistance(optimizedSequence),
            estimateTime(optimizedSequence)
        );
    }

    private List<Location> solveTSP(
        List<Location> locations,
        LocationGraph warehouse
    ) {
        // Nearest neighbor heuristic with 2-opt improvement
        List<Location> tour = nearestNeighbor(locations, warehouse);
        return twoOptImprovement(tour, warehouse);
    }
}

4.3 Mobile API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@RestController
@RequestMapping("/api/v1/mobile/picking")
public class MobilePickingController {

    @GetMapping("/next-task")
    public PickTaskResponse getNextTask(
        @AuthenticationPrincipal Associate picker
    ) {
        PickSession activeSession = sessionRepository
            .findActiveByPicker(picker.getId())
            .orElseGet(() -> assignNewSession(picker));

        PickInstruction nextInstruction = activeSession
            .getNextInstruction();

        return PickTaskResponse.builder()
            .instruction(nextInstruction)
            .navigation(generateNavigation(
                picker.getCurrentLocation(),
                nextInstruction.getLocation()
            ))
            .build();
    }

    @PostMapping("/confirm")
    public ConfirmationResponse confirmPick(
        @RequestBody PickConfirmationRequest request,
        @AuthenticationPrincipal Associate picker
    ) {
        PickSession session = sessionRepository
            .findActiveByPicker(picker.getId())
            .orElseThrow();

        session.confirmPick(
            request.getInstructionId(),
            request.getQuantity(),
            request.getBarcode()
        );

        sessionRepository.save(session);

        return ConfirmationResponse.builder()
            .success(true)
            .nextInstruction(session.getNextInstruction())
            .build();
    }
}

Timeline


Project 5: Pack & Ship Service (WES)

Overview

Consolidate packing, quality control, and shipping preparation into a unified WES service.

Scope

In Scope:

Implementation Highlights

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@AggregateRoot
public class PackingSession {
    private String sessionId;
    private String orderId;
    private String packerId;
    private String stationId;
    private List<ItemToScan> itemsToScan;
    private String selectedCarton;
    private Weight actualWeight;
    private PackingStatus status;
    private QualityCheck qualityCheck;

    public void scanItem(String barcode) {
        ItemToScan item = findByBarcode(barcode);
        item.markScanned();

        if (requiresQualityCheck(item)) {
            initiateQualityCheck(item);
        }

        if (allItemsScanned()) {
            readyForCartonSelection();
        }
    }

    public void performQualityCheck(QualityCheckResult result) {
        this.qualityCheck = new QualityCheck(result);

        if (!result.passed()) {
            handleQualityFailure(result);
        }
    }

    public void weighAndClose(Weight weight) {
        validateWeight(weight);
        this.actualWeight = weight;
        this.status = PackingStatus.READY_TO_SHIP;
        registerEvent(new PackingCompletedEvent(this));
    }
}

Timeline


Project 6: Physical Tracking Service (WES)

Overview

Create a dedicated service for real-time physical tracking including license plates and movements.

Scope

In Scope:

Key Features

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
public class MovementTrackingService {

    public void trackMovement(MovementRequest request) {
        // Validate movement
        LocationState fromLocation = locationRepository
            .findById(request.getFromLocationId());
        LocationState toLocation = locationRepository
            .findById(request.getToLocationId());

        // Update license plate
        LicensePlate lp = licensePlateRepository
            .findById(request.getLicensePlateId());
        lp.moveTo(request.getToLocationId());

        // Update location states
        fromLocation.removeLP(lp.getId());
        toLocation.addLP(lp.getId());

        // Create movement record
        Movement movement = new Movement(
            request,
            LocalDateTime.now()
        );
        movementRepository.save(movement);

        // Publish event
        eventPublisher.publish(new PhysicalMovementEvent(movement));
    }
}

Timeline


Common Components

Shared Event Library

1
2
3
4
5
6
7
8
9
10
11
12
13
paklog-events/
├── src/main/java/com/paklog/events/
   ├── wms/
      ├── WaveReleasedEvent.java
      ├── InventoryAllocatedEvent.java
      └── LocationConfiguredEvent.java
   ├── wes/
      ├── TaskCompletedEvent.java
      ├── PickingCompletedEvent.java
      └── MovementCompletedEvent.java
   └── common/
       ├── CloudEventBuilder.java
       └── EventMetadata.java

Integration Patterns Library

1
2
3
4
5
6
7
8
9
10
11
paklog-integration/
├── src/main/java/com/paklog/integration/
   ├── saga/
      ├── SagaOrchestrator.java
      └── CompensationManager.java
   ├── outbox/
      ├── OutboxProcessor.java
      └── TransactionalOutbox.java
   └── reconciliation/
       ├── ReconciliationService.java
       └── VarianceDetector.java

Integration Testing Strategy

Contract Testing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootTest
@AutoConfigureMockMvc
class WaveReleaseContractTest {

    @Test
    void whenWaveReleased_thenTasksGenerated() {
        // Given: Wave Planning Service publishes event
        WaveReleasedEvent event = createWaveReleasedEvent();

        // When: Event is published
        kafkaTemplate.send("wms.wave.events", event);

        // Then: Task Execution Service creates tasks
        await().atMost(5, SECONDS).until(() -> {
            List<WorkTask> tasks = taskRepository
                .findByReferenceId(event.getWaveId());
            return tasks.size() == event.getOrderIds().size();
        });
    }
}

End-to-End Testing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@E2ETest
class FullFulfillmentFlowTest {

    @Test
    void completeOrderFulfillmentFlow() {
        // 1. Create order (WMS)
        Order order = orderService.createOrder(orderRequest);

        // 2. Plan wave (WMS)
        Wave wave = waveService.planWave(Arrays.asList(order));

        // 3. Release wave (WMS → WES)
        waveService.release(wave);

        // 4. Generate tasks (WES)
        await().until(() -> taskService.hasTasks(wave));

        // 5. Execute picking (WES)
        PickSession session = pickService.startSession(wave);
        pickService.complete(session);

        // 6. Pack order (WES)
        PackingSession packing = packService.pack(order);

        // 7. Verify completion
        assertThat(order.getStatus()).isEqualTo(PACKED);
    }
}

Monitoring & Observability

Key Metrics

Service Health Metrics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
metrics:
  wms:
    wave_planning:
      - wave_creation_rate
      - wave_release_latency
      - wave_size_distribution
    location_master:
      - configuration_change_rate
      - slotting_optimization_time

  wes:
    task_execution:
      - task_creation_rate
      - task_assignment_latency
      - task_completion_rate
    pick_execution:
      - pick_rate_per_hour
      - pick_accuracy
      - path_optimization_time

Integration Metrics

1
2
3
4
5
6
7
8
9
integration:
  event_bus:
    - message_throughput
    - message_lag
    - failed_message_rate
  api_gateway:
    - request_latency
    - error_rate
    - circuit_breaker_trips

Distributed Tracing

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class TracingConfiguration {

    @Bean
    public Tracer tracer() {
        return OpenTelemetry.builder()
            .addSpanProcessor(JaegerSpanExporter.builder()
                .setEndpoint("http://jaeger:14250")
                .build())
            .build()
            .getTracer("paklog");
    }
}

Rollback Strategy

Service Level Rollback

1
2
3
4
5
6
7
8
9
10
11
12
13
deployment:
  strategy: blue-green
  rollback:
    triggers:
      - error_rate > 5%
      - latency_p95 > 1000ms
      - health_check_failures > 3

    procedure:
      1. Route traffic back to blue environment
      2. Investigate issues in green environment
      3. Fix and redeploy to green
      4. Retry cutover

Data Rollback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
-- Maintain audit trail for rollback
CREATE TABLE migration_audit (
    id SERIAL PRIMARY KEY,
    service_name VARCHAR(100),
    migration_step VARCHAR(100),
    old_value JSONB,
    new_value JSONB,
    timestamp TIMESTAMP,
    rolled_back BOOLEAN DEFAULT FALSE
);

-- Rollback procedure
CREATE PROCEDURE rollback_migration(
    p_service_name VARCHAR,
    p_from_timestamp TIMESTAMP
)
AS $$
BEGIN
    -- Restore old values from audit
    UPDATE target_table t
    SET data = a.old_value
    FROM migration_audit a
    WHERE a.service_name = p_service_name
      AND a.timestamp >= p_from_timestamp
      AND a.rolled_back = FALSE;

    -- Mark as rolled back
    UPDATE migration_audit
    SET rolled_back = TRUE
    WHERE service_name = p_service_name
      AND timestamp >= p_from_timestamp;
END;
$$;

Risk Register

Risk Probability Impact Mitigation
Data inconsistency during migration High High Reconciliation service, audit trail
Performance degradation Medium High Shadow mode testing, gradual rollout
Integration failures Medium High Circuit breakers, fallback mechanisms
Team knowledge gaps High Medium Training, documentation, pair programming
Rollback complexity Low High Blue-green deployment, data versioning

Success Criteria

Technical Success

Business Success


Timeline Summary

gantt
    title WMS/WES Decoupling Timeline
    dateFormat  YYYY-MM-DD
    section Phase 1
    Wave Planning Service     :2025-02-01, 6w
    Task Execution Service    :2025-02-15, 8w
    section Phase 2
    Location Services Split   :2025-03-15, 8w
    Pick Execution Service   :2025-04-01, 7w
    section Phase 3
    Pack & Ship Service      :2025-05-01, 7w
    Physical Tracking       :2025-05-15, 6w
    section Cleanup
    Legacy Decommission     :2025-06-15, 4w
    Documentation          :2025-07-01, 2w

Appendix A: Database Migration Scripts

Wave Planning Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- Create wave planning database
CREATE DATABASE wave_planning_db;

-- Create wave tables
CREATE TABLE waves (
    wave_id VARCHAR(50) PRIMARY KEY,
    status VARCHAR(20),
    order_ids JSON,
    priority VARCHAR(20),
    strategy VARCHAR(50),
    planned_release_time TIMESTAMP,
    actual_release_time TIMESTAMP,
    metrics JSON
);

Location Split

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- Location Master (WMS)
CREATE TABLE location_master (
    location_id VARCHAR(50) PRIMARY KEY,
    warehouse_id VARCHAR(20),
    zone VARCHAR(10),
    aisle VARCHAR(10),
    bay VARCHAR(10),
    bin VARCHAR(10),
    type VARCHAR(20),
    capacity_weight DECIMAL,
    capacity_volume DECIMAL,
    slotting_class CHAR(1),
    active BOOLEAN
);

-- Location State (WES)
CREATE TABLE location_state (
    location_id VARCHAR(50) PRIMARY KEY,
    occupancy_status VARCHAR(20),
    current_item_count INT,
    current_weight DECIMAL,
    current_volume DECIMAL,
    status VARCHAR(20),
    blocked_reason VARCHAR(255),
    last_movement TIMESTAMP,
    FOREIGN KEY (location_id) REFERENCES location_master(location_id)
);

Appendix B: API Documentation

Wave Planning Service API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
openapi: 3.0.0
info:
  title: Wave Planning Service API
  version: 1.0.0
servers:
  - url: https://api.paklog.com/wms/wave-planning/v1
paths:
  /waves:
    post:
      summary: Create new wave
      operationId: createWave
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                orderIds:
                  type: array
                  items:
                    type: string
                strategy:
                  type: string
                  enum: [TIME_BASED, CARRIER_BASED, ZONE_BASED]
                priority:
                  type: string
                  enum: [STANDARD, HIGH, CRITICAL]
      responses:
        201:
          description: Wave created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Wave'

Appendix C: Event Catalog

WMS Events

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "WaveReleasedEvent": {
    "source": "wave-planning-service",
    "type": "com.paklog.wms.wave.released.v1",
    "data": {
      "waveId": "string",
      "orderIds": ["string"],
      "warehouseId": "string",
      "priority": "string",
      "releasedAt": "datetime"
    }
  }
}

WES Events

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "TaskCompletedEvent": {
    "source": "task-execution-service",
    "type": "com.paklog.wes.task.completed.v1",
    "data": {
      "taskId": "string",
      "taskType": "string",
      "completedBy": "string",
      "completedAt": "datetime",
      "metrics": {}
    }
  }
}

Document Control

Version Date Author Changes
1.0.0 2025-01-18 Architecture Team Initial plan
       

Approval

Role Name Date Signature
CTO      
VP Engineering      
Lead Architect      
WMS Team Lead      
WES Team Lead      

END OF DOCUMENT