Skip to main content

Overview

InventoryEntry is the aggregate root that maps a product to its instances and resources. It manages the relationship between product instances and availability resources. Package: com.softwarearchetypes.inventory Source: /inventory/src/main/java/com/softwarearchetypes/inventory/InventoryEntry.java:34

Responsibilities

  • Instance management: Track all instances of a product
  • Resource mapping: Map instances to availability resources
  • Lock handling: Delegate lock operations to availability facade
  • Version control: Optimistic locking support

Structure

  • instances: Set of all InstanceIds belonging to this entry
  • instanceToResource: Maps instances to resources for availability tracking
Note: Not all instances need a resource mapping (e.g., instances not yet made available). Actual availability is managed by AvailabilityFacade.

Constructor

InventoryEntry(InventoryEntryId id,
              InventoryProduct product,
              Set<InstanceId> instances,
              Map<InstanceId, ResourceId> instanceToResource,
              AvailabilityFacade availabilityFacade,
              Version version)
id
InventoryEntryId
required
Unique inventory entry identifier
product
InventoryProduct
required
The product this entry manages
instances
Set<InstanceId>
Set of instance IDs (defaults to empty)
instanceToResource
Map<InstanceId, ResourceId>
Instance-to-resource mapping (defaults to empty)
availabilityFacade
AvailabilityFacade
required
Facade for managing availability
version
Version
required
Version for optimistic locking

Factory Method

create

static InventoryEntry create(InventoryProduct product,
                             AvailabilityFacade availabilityFacade)
Creates a new empty inventory entry.
product
InventoryProduct
required
The product
availabilityFacade
AvailabilityFacade
required
Availability facade
return
InventoryEntry
New inventory entry with initial version
Example:
InventoryProduct product = // ... product definition
AvailabilityFacade availabilityFacade = // ... injected

InventoryEntry entry = InventoryEntry.create(product, availabilityFacade);
assert entry.isEmpty();

Accessor Methods

id

InventoryEntryId id()
Returns the inventory entry identifier.
return
InventoryEntryId
The inventory entry ID

product

InventoryProduct product()
Returns the product.
return
InventoryProduct
The inventory product

productId

ProductIdentifier productId()
Returns the product identifier.
return
ProductIdentifier
The product identifier

instances

Set<InstanceId> instances()
Returns all instance IDs.
return
Set<InstanceId>
Immutable set of instance IDs

instanceCount

int instanceCount()
Returns the number of instances.
return
int
Instance count

isEmpty

boolean isEmpty()
Checks if this entry has no instances.
return
boolean
true if no instances, false otherwise

version

Version version()
Returns the version for optimistic locking.
return
Version
The version

Instance Management

addInstance

void addInstance(Instance instance)
Adds an instance to this entry.
instance
Instance
required
The instance to add
Example:
Instance phone = ProductInstance.builder()
    .productId(entry.productId())
    .serialNumber(new ImeiSerialNumber("123456789012345"))
    .build();

entry.addInstance(phone);
assert entry.hasInstance(phone.id());

removeInstance

void removeInstance(InstanceId instanceId)
Removes an instance and its resource mapping if any.
instanceId
InstanceId
required
The instance ID to remove

hasInstance

boolean hasInstance(InstanceId instanceId)
Checks if an instance exists in this entry.
instanceId
InstanceId
required
The instance ID to check
return
boolean
true if instance exists, false otherwise

Resource Mapping

mapInstanceToResource

void mapInstanceToResource(InstanceId instanceId, ResourceId resourceId)
Maps an instance to an availability resource. If the instance doesn’t exist, it will be added.
instanceId
InstanceId
required
The instance ID
resourceId
ResourceId
required
The resource ID
Example:
// Map phone instance to individual resource
entry.mapInstanceToResource(phoneInstanceId, ResourceId.of("RES-PHONE-001"));

// Map milk batch instance to pool resource
entry.mapInstanceToResource(milkInstanceId, ResourceId.of("RES-MILK-POOL"));

unmapInstanceFromResource

void unmapInstanceFromResource(InstanceId instanceId)
Removes the resource mapping for an instance.
instanceId
InstanceId
required
The instance ID

resourceFor

Optional<ResourceId> resourceFor(InstanceId instanceId)
Finds the resource mapped to an instance.
instanceId
InstanceId
required
The instance ID
return
Optional<ResourceId>
The resource ID if mapped, empty otherwise

instanceFor

Optional<InstanceId> instanceFor(ResourceId resourceId)
Finds the instance mapped to a resource.
resourceId
ResourceId
required
The resource ID
return
Optional<InstanceId>
The instance ID if mapped, empty otherwise

instanceToResourceMap

Map<InstanceId, ResourceId> instanceToResourceMap()
Returns the complete instance-to-resource mapping.
return
Map<InstanceId, ResourceId>
Immutable map of instance to resource mappings

resourceIds

List<ResourceId> resourceIds()
Returns all resource IDs.
return
List<ResourceId>
List of resource IDs

hasResource

boolean hasResource(ResourceId resourceId)
Checks if a resource is mapped to any instance.
resourceId
ResourceId
required
The resource ID to check
return
boolean
true if resource is mapped, false otherwise

Lock Handling

handle

Result<String, List<BlockadeId>> handle(LockCommand cmd)
Handles a lock command by delegating to the appropriate handler based on resource specification.
cmd
LockCommand
required
The lock command containing product ID, quantity, owner, and resource specification
return
Result<String, List<BlockadeId>>
Success with list of blockade IDs, or failure with error message
Supported resource specifications:
  • TemporalSpecification: For time-based resources (hotel rooms, appointment slots)
  • IndividualSpecification: For specific instances (rental car, unique equipment)
  • QuantitySpecification: For pool resources (fuel, milk, bulk goods)
Example:
// Lock a hotel room for 3 nights
LockCommand hotelCmd = LockCommand.builder()
    .productId(hotelRoomProductId)
    .owner(OwnerId.of("RESERVATION-123"))
    .resourceSpecification(
        ResourceSpecification.temporal(
            TimeSlot.of(LocalDate.of(2024, 3, 10), LocalDate.of(2024, 3, 11)),
            TimeSlot.of(LocalDate.of(2024, 3, 11), LocalDate.of(2024, 3, 12)),
            TimeSlot.of(LocalDate, 3, 12), LocalDate.of(2024, 3, 13))
        )
    )
    .build();

Result<String, List<BlockadeId>> result = entry.handle(hotelCmd);

if (result.success()) {
    List<BlockadeId> blockades = result.getSuccess();
    // Successfully locked for 3 nights
} else {
    System.out.println("Lock failed: " + result.getFailure());
}

// Lock a specific rental car
LockCommand carCmd = LockCommand.builder()
    .productId(carProductId)
    .owner(OwnerId.of("RENTAL-456"))
    .resourceSpecification(
        ResourceSpecification.individual(carInstanceId)
    )
    .build();

result = entry.handle(carCmd);

// Lock quantity of fuel
LockCommand fuelCmd = LockCommand.builder()
    .productId(fuelProductId)
    .quantity(Quantity.of(50, Unit.liters()))
    .owner(OwnerId.of("ORDER-789"))
    .resourceSpecification(
        ResourceSpecification.quantity()
    )
    .build();

result = entry.handle(fuelCmd);

Lock Strategies

Temporal Lock

Used for time-slotted resources like hotel rooms, meeting rooms, or appointment slots. Current implementation: Takes first available resource. Alternative strategies:
  • Find resource available for all slots
  • Use customer preference
  • Load balancing
Rollback: If locking fails midway, previously locked slots are automatically released.

Individual Lock

Used for specific, uniquely identifiable items. Example: Renting a specific car with VIN ABC123.

Quantity Lock

Used for pool resources where specific identity doesn’t matter. Examples: Fuel, milk, bulk chemicals. Current implementation: Takes first pool resource. Alternative strategies:
  • Distribute across multiple pools
  • FIFO allocation
  • Geographic proximity

Complete Usage Example

// Create inventory entry for hotel rooms
InventoryProduct hotelRoom = InventoryProduct.builder()
    .productId(hotelRoomProductId)
    .trackingStrategy(ProductTrackingStrategy.TEMPORAL)
    .build();

InventoryEntry entry = InventoryEntry.create(hotelRoom, availabilityFacade);

// Add room instances
Instance room101 = ProductInstance.builder()
    .productId(hotelRoomProductId)
    .serialNumber(new TextualSerialNumber("ROOM-101"))
    .build();

Instance room102 = ProductInstance.builder()
    .productId(hotelRoomProductId)
    .serialNumber(new TextualSerialNumber("ROOM-102"))
    .build();

entry.addInstance(room101);
entry.addInstance(room102);

assert entry.instanceCount() == 2;

// Map instances to temporal resources
ResourceId resource101 = ResourceId.of("TEMPORAL-ROOM-101");
ResourceId resource102 = ResourceId.of("TEMPORAL-ROOM-102");

entry.mapInstanceToResource(room101.id(), resource101);
entry.mapInstanceToResource(room102.id(), resource102);

// Lock a room for 2 nights
LockCommand cmd = LockCommand.builder()
    .productId(hotelRoomProductId)
    .owner(OwnerId.of("RESERVATION-123"))
    .resourceSpecification(
        ResourceSpecification.temporal(
            TimeSlot.of(LocalDate.of(2024, 3, 10), LocalDate.of(2024, 3, 11)),
            TimeSlot.of(LocalDate.of(2024, 3, 11), LocalDate.of(2024, 3, 12))
        )
    )
    .build();

Result<String, List<BlockadeId>> result = entry.handle(cmd);

if (result.success()) {
    List<BlockadeId> blockades = result.getSuccess();
    System.out.println("Locked " + blockades.size() + " time slots");
    
    // Later: unlock
    for (BlockadeId blockade : blockades) {
        availabilityFacade.handle(
            UnlockRequest.of(OwnerId.of("RESERVATION-123"), blockade)
        );
    }
}

// Query resource mappings
Optional<ResourceId> resource = entry.resourceFor(room101.id());
assert resource.isPresent();
assert resource.get().equals(resource101);

Optional<InstanceId> instance = entry.instanceFor(resource101);
assert instance.isPresent();
assert instance.get().equals(room101.id());

Error Handling

Lock operations can fail for several reasons: “No resources mapped for product”
  • No instances have been mapped to resources
  • Solution: Map instances to resources first
“No resource mapped for instance”
  • Specific instance not mapped to a resource
  • Solution: Map the instance to a resource
Availability failures
  • Resource already locked
  • Insufficient quantity
  • Time slot not available
Result<String, List<BlockadeId>> result = entry.handle(cmd);

if (result.failure()) {
    String error = result.getFailure();
    
    if (error.contains("No resources")) {
        // Need to map instances to resources
    } else if (error.contains("already locked")) {
        // Resource unavailable
    }
}

Build docs developers (and LLMs) love