Inventory Service - Domain Architecture & Business Capabilities

Service Overview

The Inventory Service serves as the single source of truth for all product stock levels across the fulfillment network. It manages inventory quantities, allocations, reservations, locations, and provides a comprehensive audit trail through an immutable ledger pattern.

Architecture Pattern: Hexagonal Architecture with Domain-Driven Design (DDD) Technology Stack: Spring Boot 3.2, Spring Data MongoDB, Spring Kafka, CloudEvents Integration Pattern: Event-Driven Architecture with Transactional Outbox Pattern


Domain Model & Bounded Context

Bounded Context: Inventory Management

The Inventory Management bounded context encompasses all aspects of tracking, allocating, and managing product stock across the fulfillment network.

Context Boundaries

Responsibilities (What’s IN):

External Dependencies (What’s OUT):

Ubiquitous Language

Core Domain Terms:


Subdomain Classification

Core Domain: Inventory Tracking & Allocation

Strategic Importance: HIGH - Critical business capability

This is the heart of the inventory service and provides significant business value through:

Subdomains:

1. Inventory Allocation & Promising (Core)

2. Multi-Location Inventory Management (Core)

3. Inventory Audit & Compliance (Supporting)

4. Inventory Adjustments (Supporting)


Domain Model

Aggregates

1. ProductStock (Aggregate Root)

Description: Manages all stock information for a product (SKU) including quantities, allocations, and reservations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@AggregateRoot
public class ProductStock {
    private String sku; // Aggregate ID
    private StockLevel stockLevel;
    private SafetyStockLevel safetyStock;
    private List<Allocation> allocations;
    private int version; // Optimistic locking

    // Business methods
    public AllocationResult allocate(String orderId, int quantity, Duration ttl);
    public void deallocate(String allocationId);
    public int calculateAvailableToPromise();
    public void adjustStock(int quantityChange, AdjustmentReason reason);
    public boolean hasSufficientATP(int requestedQuantity);

    // Invariants
    private void ensureQuantityOnHandNotNegative();
    private void ensureAllocatedDoesNotExceedOnHand();
}

Invariants:

Domain Events:


2. StockLocation (Aggregate Root)

Description: Manages inventory at a specific physical location within the warehouse, including physical reservations for picking.

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
@AggregateRoot
public class StockLocation {
    private String locationId; // Composite: SKU + Location
    private String sku;
    private String warehouseId;
    private String zone;
    private String aisle;
    private String bin;
    private int quantityAtLocation;
    private List<PhysicalReservation> reservations;

    // Business methods
    public PhysicalReservation createReservation(
        String orderId,
        int quantity,
        LocalDateTime expiresAt
    );

    public void fulfillReservation(String reservationId, int quantityPicked);
    public void cancelReservation(String reservationId);
    public int calculateAvailableAtLocation();

    // Invariants
    private void ensureReservationsDoNotExceedQuantity();
    private void ensureLocationExists();
}

Invariants:

Domain Events:


3. InventoryLedgerEntry (Aggregate Root - Event Sourced)

Description: Immutable record of inventory transaction for complete audit trail.

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
@AggregateRoot
public class InventoryLedgerEntry {
    private String entryId; // Aggregate ID
    private String sku;
    private TransactionType transactionType;
    private int quantityBefore;
    private int quantityAfter;
    private int quantityChange;
    private AdjustmentReason reasonCode;
    private LocalDateTime timestamp;
    private String userId;
    private String referenceId; // Order ID, Receipt ID, etc.
    private Map<String, String> metadata;

    // Immutable - no business methods that change state
    // Only factory method for creation

    public static InventoryLedgerEntry create(
        String sku,
        TransactionType type,
        int quantityBefore,
        int quantityAfter,
        AdjustmentReason reason,
        String userId,
        String referenceId
    );
}

Invariants:

Domain Events:


Entities

Allocation

Description: Represents allocation of inventory for a specific order.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
public class Allocation {
    private String allocationId;
    private String orderId;
    private int quantityAllocated;
    private AllocationStatus status;
    private LocalDateTime allocatedAt;
    private LocalDateTime expiresAt;
    private Priority priority;

    public boolean isExpired();
    public void expire();
    public void fulfill();
}

PhysicalReservation

Description: Location-specific reservation for picking.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
public class PhysicalReservation {
    private String reservationId;
    private String orderId;
    private String pickListId;
    private int quantityReserved;
    private int quantityPicked;
    private ReservationStatus status;
    private LocalDateTime reservedAt;
    private LocalDateTime expiresAt;

    public void recordPick(int quantity);
    public boolean isFullyPicked();
    public void cancel(String reason);
}

Value Objects

StockLevel

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
@ValueObject
public class StockLevel {
    private int quantityOnHand;
    private int quantityAllocated;
    private int quantityReserved; // Physical reservations

    public int calculateAvailable() {
        return quantityOnHand - quantityAllocated;
    }

    public StockLevel adjustQuantityOnHand(int adjustment) {
        return new StockLevel(
            quantityOnHand + adjustment,
            quantityAllocated,
            quantityReserved
        );
    }

    public StockLevel allocate(int quantity) {
        if (quantity > calculateAvailable()) {
            throw new InsufficientInventoryException();
        }
        return new StockLevel(
            quantityOnHand,
            quantityAllocated + quantity,
            quantityReserved
        );
    }
}

SafetyStockLevel

1
2
3
4
5
6
7
8
9
@ValueObject
public class SafetyStockLevel {
    private int minimumLevel;
    private int reorderPoint;
    private int maximumLevel;

    public boolean isBelowSafety(int currentQuantity);
    public boolean shouldReorder(int currentQuantity);
}

Location

1
2
3
4
5
6
7
8
9
10
@ValueObject
public class Location {
    private String warehouseId;
    private String zone;
    private String aisle;
    private String bin;

    public String toLocationCode(); // e.g., "WH01-Z01-A05-B12"
    public boolean isInZone(String zoneId);
}

Domain Services

AllocationService

Responsibility: Orchestrate inventory allocation across locations and handle concurrency.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@DomainService
public class AllocationService {

    public AllocationResult allocateInventory(
        String sku,
        String orderId,
        int quantity,
        AllocationStrategy strategy
    );

    public void deallocateInventory(String sku, String allocationId);

    public void handleAllocationExpiration(String sku, String allocationId);

    public List<StockLocation> findLocationsWithAvailableStock(
        String sku,
        int requiredQuantity
    );
}

InventoryReconciliationService

Responsibility: Reconcile physical inventory counts with system records.

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

    public ReconciliationResult reconcile(
        String sku,
        String locationId,
        int physicalCount,
        String countedBy
    );

    public void processVariance(
        String sku,
        int systemQuantity,
        int physicalQuantity,
        VarianceApproval approval
    );

    public BigDecimal calculateInventoryAccuracy(
        LocalDate from,
        LocalDate to
    );
}

ATP CalculationService

Responsibility: Calculate Available-to-Promise across locations and time horizons.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@DomainService
public class ATPCalculationService {

    public int calculateCurrentATP(String sku);

    public int calculateLocationATP(String sku, String warehouseId);

    public Map<LocalDate, Integer> calculateFutureATP(
        String sku,
        int daysAhead,
        List<PlannedReceipt> plannedReceipts
    );

    public boolean canFulfill(String sku, int quantity, String warehouseId);
}

Application Layer

Ports (Interfaces)

Input Ports (Use Cases)

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
// Commands
public interface AllocateInventoryCommand {
    AllocationResult execute(AllocateInventoryRequest request);
}

public interface DeallocateInventoryCommand {
    void execute(DeallocateInventoryRequest request);
}

public interface AdjustStockLevelCommand {
    void execute(StockAdjustmentRequest request);
}

public interface CreatePhysicalReservationCommand {
    PhysicalReservation execute(CreateReservationRequest request);
}

// Queries
public interface GetATPQuery {
    int execute(String sku);
}

public interface GetStockLevelQuery {
    StockLevel execute(String sku);
}

public interface GetInventoryLedgerQuery {
    List<InventoryLedgerEntry> execute(String sku, DateRange dateRange);
}

Output Ports (Dependencies)

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
// Repository ports
public interface ProductStockRepository {
    Optional<ProductStock> findBySku(String sku);
    void save(ProductStock productStock);
    List<ProductStock> findBySkus(List<String> skus);
}

public interface StockLocationRepository {
    Optional<StockLocation> findBySkuAndLocation(String sku, String locationId);
    List<StockLocation> findBySku(String sku);
    void save(StockLocation stockLocation);
}

public interface InventoryLedgerRepository {
    void append(InventoryLedgerEntry entry);
    List<InventoryLedgerEntry> findBySku(String sku, Pageable pageable);
}

// External service ports
public interface ProductCatalogClient {
    boolean productExists(String sku);
    ProductInfo getProductInfo(String sku);
}

public interface EventPublisher {
    void publish(DomainEvent event);
}

// Outbox for reliable event publishing
public interface OutboxRepository {
    void save(OutboxEvent event);
    List<OutboxEvent> findUnpublished(int batchSize);
    void markAsPublished(String eventId);
}

Infrastructure Layer

Adapters

Inbound Adapters

REST Controller

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
@RestController
@RequestMapping("/api/v1/inventory")
public class InventoryController {

    @GetMapping("/{sku}/atp")
    public ResponseEntity<ATPResponse> getATP(@PathVariable String sku);

    @PostMapping("/allocate")
    public ResponseEntity<AllocationResponse> allocate(
        @RequestBody AllocateRequest request
    );

    @PostMapping("/deallocate")
    public ResponseEntity<Void> deallocate(
        @RequestBody DeallocateRequest request
    );

    @PostMapping("/{sku}/adjust")
    public ResponseEntity<Void> adjustStock(
        @PathVariable String sku,
        @RequestBody StockAdjustmentRequest request
    );

    @GetMapping("/{sku}/ledger")
    public ResponseEntity<List<LedgerEntryResponse>> getLedger(
        @PathVariable String sku,
        @RequestParam(required = false) LocalDate from,
        @RequestParam(required = false) LocalDate to
    );
}

Event Listener

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

    @KafkaListener(topics = "fulfillment.order.v1.events")
    public void handleOrderValidated(FulfillmentOrderValidatedEvent event) {
        // Allocate inventory for validated order
    }

    @KafkaListener(topics = "fulfillment.order.v1.events")
    public void handleOrderCancelled(FulfillmentOrderCancelledEvent event) {
        // Deallocate inventory for cancelled order
    }
}

@Component
public class WarehouseEventListener {

    @KafkaListener(topics = "fulfillment.warehouse.v1.events")
    public void handleItemPicked(ItemPickedEvent event) {
        // Decrement inventory and allocation
    }
}

Outbound Adapters

MongoDB Adapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Repository
public class MongoProductStockRepository implements ProductStockRepository {

    private final MongoTemplate mongoTemplate;

    @Override
    public Optional<ProductStock> findBySku(String sku) {
        return Optional.ofNullable(
            mongoTemplate.findById(sku, ProductStock.class)
        );
    }

    @Override
    public void save(ProductStock productStock) {
        mongoTemplate.save(productStock);
    }
}

Transactional Outbox Adapter

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
@Component
public class OutboxEventPublisher implements EventPublisher {

    private final OutboxRepository outboxRepository;

    @Override
    @Transactional
    public void publish(DomainEvent event) {
        // Save event to outbox in same transaction as domain change
        OutboxEvent outboxEvent = new OutboxEvent(
            event.getId(),
            event.getType(),
            event.toJson(),
            OutboxStatus.PENDING
        );
        outboxRepository.save(outboxEvent);
    }
}

@Component
@Scheduled(fixedDelay = 1000) // Poll every second
public class OutboxProcessor {

    public void processOutbox() {
        List<OutboxEvent> pending = outboxRepository.findUnpublished(100);

        for (OutboxEvent event : pending) {
            try {
                kafkaTemplate.send(topicName, event.getPayload());
                outboxRepository.markAsPublished(event.getId());
            } catch (Exception e) {
                // Retry logic with exponential backoff
            }
        }
    }
}

Cache Adapter

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

    private final RedisTemplate<String, Object> redisTemplate;

    public Optional<Integer> getCachedATP(String sku) {
        String key = "atp:" + sku;
        return Optional.ofNullable(
            (Integer) redisTemplate.opsForValue().get(key)
        );
    }

    public void cacheATP(String sku, int atp, Duration ttl) {
        String key = "atp:" + sku;
        redisTemplate.opsForValue().set(key, atp, ttl);
    }

    public void invalidateATP(String sku) {
        String key = "atp:" + sku;
        redisTemplate.delete(key);
    }
}

Business Capabilities

L1: Inventory Management

Single source of truth for all product stock levels across the fulfillment network.


L2: Stock Level Management

L3.1: Quantity on Hand Tracking

L3.2: Quantity Allocated Tracking

L3.3: Available-to-Promise (ATP) Calculation

L3.4: Safety Stock Management

L3.5: Stock Level Adjustments


L2: Inventory Allocation & Reservation

L3.6: Inventory Allocation

L3.7: Inventory Deallocation

L3.8: Physical Reservation Management

L3.9: Allocation Expiration Management


L2: Multi-Location Inventory Management

L3.10: Location-Based Stock Tracking

L3.11: Inter-Location Stock Transfers

L3.12: Location-Based ATP Calculation


L2: Inventory Audit & Compliance

L3.13: Immutable Inventory Ledger

L3.14: Transaction History Query

L3.15: Inventory Reconciliation

L3.16: Audit Trail Reporting


L2: Event-Driven Integration

L3.17: Transactional Outbox Pattern

L3.18: Inventory Domain Event Publishing

L3.19: Event Consumption


Integration Patterns

Context Mapping

Product Catalog (Upstream - Customer/Supplier)

Order Management (Upstream - Published Language)

Warehouse Operations (Bi-directional - Partnership)


Event Schemas

InventoryAllocatedEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "specversion": "1.0",
  "type": "com.paklog.inventory.allocated",
  "source": "inventory-service",
  "id": "alloc-12345",
  "time": "2025-10-18T10:30:00Z",
  "datacontenttype": "application/json",
  "data": {
    "allocationId": "alloc-12345",
    "sku": "PROD-123",
    "orderId": "ORD-67890",
    "quantityAllocated": 5,
    "allocatedAt": "2025-10-18T10:30:00Z",
    "expiresAt": "2025-10-19T10:30:00Z",
    "warehouseId": "WH01"
  }
}

StockLevelChangedEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  "specversion": "1.0",
  "type": "com.paklog.inventory.stock.changed",
  "source": "inventory-service",
  "id": "stock-change-456",
  "time": "2025-10-18T10:30:00Z",
  "datacontenttype": "application/json",
  "data": {
    "sku": "PROD-123",
    "quantityBefore": 100,
    "quantityAfter": 95,
    "quantityChange": -5,
    "transactionType": "PICK",
    "reasonCode": "ORDER_FULFILLMENT",
    "referenceId": "ORD-67890",
    "warehouseId": "WH01",
    "userId": "picker-001"
  }
}

Quality Attributes

Consistency

Performance

Reliability


Summary

The Inventory Service is a critical bounded context providing:

Business Impact: 99.5%+ accuracy, real-time ATP, 100% traceability, <50ms query latency.