Skip to main content

Inventory Module Domain Documentation

Overview

This section contains domain documentation for the Inventory Module, which manages all aspects of inventory tracking, warehouse operations, and manufacturing assembly. The module implements Domain-Driven Design (DDD) principles with Clean Architecture, using composition patterns for flexibility and immutable audit trails for compliance.


Core Aggregates

InventoryTransaction Aggregate Complete

Purpose: Unified aggregate for all inventory movements with complete audit trail.

Key Responsibilities:

  • Records all inventory changes across 5 transaction types
  • Maintains immutable audit history
  • Enforces business rules specific to each transaction type
  • Triggers inventory quantity updates via domain events
  • Supports multi-line transactions with validation

Transaction Types:

  1. Purchase - Receiving inventory from vendors
  2. Sale - Shipping inventory to customers
  3. Movement - Transferring stock between locations
  4. Adjustment - Correcting inventory discrepancies
  5. Assembly - Manufacturing finished goods from components

Business Rules:

  • Transactions are immutable after creation
  • All transactions require user, timestamp, and reason
  • Line items must have positive quantities
  • Each transaction type has specific validation rules
  • No duplicate items within a single transaction
  • Supports up to 4 decimal places for quantities

Domain Events:

  • Transaction creation events (one per type)
  • Line item events for inventory updates
  • Status change events (planned for returns/reversals)

Detailed Documentation: inventory-transaction.aggregate.md


InventoryRecord Aggregate Complete

Purpose: Tracks current stock levels per item-location-unit combination.

Key Responsibilities:

  • Maintains real-time quantity on-hand
  • Updates reactively from transaction domain events
  • Supports negative stock (configurable per item)
  • Enables stock availability queries
  • Tracks multi-unit inventories

Business Rules:

  • One record per item-location-unit combination
  • Quantity updates are atomic
  • Negative stock allowed only if item permits
  • Created automatically on first transaction
  • Never deleted (soft delete via IsActive)

Update Trigger:

  • Domain event handlers listen to transaction created events
  • Inventory quantities updated after successful transaction commit
  • Ensures consistency between transactions and inventory

Detailed Documentation: inventory-record.aggregate.md


Item Aggregate Complete

Purpose: Item master data catalog with composition-based stockability.

Key Responsibilities:

  • Item catalog management
  • Stockable vs non-stockable classification
  • Category hierarchy assignment
  • Default unit class configuration
  • Tax group integration
  • Item lifecycle (active/inactive)

Business Rules:

  • Item numbers must be unique
  • Stockable behavior is optional and can be added/removed
  • Cannot deactivate items with active inventory
  • Category assignment is optional
  • Tax group is optional

Composition Pattern:

  • Base Item entity + optional StockableBehavior
  • Stockable items track: AllowNegativeStock, TrackByLocation
  • Non-stockable items are services or intangible products

Domain Events:

  • ItemCreatedDomainEvent
  • StockableBehaviorAddedDomainEvent
  • StockableBehaviorRemovedDomainEvent

Detailed Documentation: item.aggregate.md (Phase 3)


BillOfMaterial Aggregate Complete

Purpose: Defines component relationships for manufacturing and assembly.

Key Responsibilities:

  • BOM definition and lifecycle management
  • Component list management
  • Quantity calculation (explosion) for production
  • Multi-level BOM support (planned)
  • Validation against circular references

Business Rules:

  • Parent item cannot be its own component
  • Component quantities must be positive
  • Each component appears only once per BOM
  • Cannot delete BOM if used in assembly transactions
  • Produced unit of measure defines BOM scale

Key Methods:

  • Explode(quantityToProduce) - calculates required components
  • AddComponent() - adds component to BOM
  • SyncComponents() - bulk component update

Detailed Documentation: bill-of-material.aggregate.md (Phase 2)


Location Aggregate Complete

Purpose: Hierarchical warehouse and storage location management.

Key Responsibilities:

  • Location hierarchy (parent-child)
  • Location type and purpose classification
  • Physical address management
  • Operational status tracking
  • Full path generation for display

Business Rules:

  • Location codes must be unique and uppercase
  • Cannot create child under inactive parent
  • Cannot deactivate location with active inventory
  • Root locations have no parent
  • Operational flag controls availability for transactions

Hierarchy Example:

WAREHOUSE-01 (Root)
├─ RECEIVING
├─ STORAGE-A
│ ├─ AISLE-1
│ └─ AISLE-2
└─ SHIPPING

Detailed Documentation: location.aggregate.md (Phase 3)


Category Aggregate Complete

Purpose: Hierarchical product classification system.

Key Responsibilities:

  • Category hierarchy management
  • Item classification
  • Hierarchical reporting support
  • Category path generation

Business Rules:

  • Category codes and names must be unique
  • Cannot delete category with children
  • Cannot delete category with assigned items
  • Hierarchy path displayed as breadcrumb

Detailed Documentation: category.aggregate.md (Phase 3)


Value Objects

Transaction Type-Specific Details Complete

Each transaction type uses composition to include type-specific data:

MovementDetails

Purpose: Captures source and destination for stock transfers.

public class MovementDetails
{
public Guid SourceLocationId { get; private set; }
public Guid DestinationLocationId { get; private set; }
public string ExternalReference { get; private set; }
}

Validation: Source ≠ Destination

Documentation: movement-details.md


AdjustmentDetails

Purpose: Captures location and reason for inventory corrections.

public class AdjustmentDetails
{
public Guid LocationId { get; private set; }
public decimal CurrentQuantity { get; private set; }
public decimal NewQuantity { get; private set; }
public string Reason { get; private set; }

public decimal AdjustmentQuantity => NewQuantity - CurrentQuantity;
public bool IsPositive => AdjustmentQuantity > 0;
}

Calculation: Adjustment = NewQuantity - CurrentQuantity

Documentation: adjustment-details.md


AssemblyDetails

Purpose: Captures BOM and production information for manufacturing.

public class AssemblyDetails
{
public Guid BillOfMaterialId { get; private set; }
public Guid ProducedItemId { get; private set; }
public decimal QuantityProduced { get; private set; }
public Guid ProducedUnitOfMeasureId { get; private set; }
public Guid AssemblyLocationId { get; private set; }
}

Business Logic: Components consumed based on BOM × quantity

Documentation: assembly-details.md


SalesDetails

Purpose: Captures customer and invoice information for sales.

public class SalesDetails
{
public Guid CustomerId { get; private set; }
public Guid ShippingLocationId { get; private set; }
public string InvoiceNumber { get; private set; }
public DateTime InvoiceDate { get; private set; }
public Guid SalesOrderId { get; private set; }
public decimal UnitCost { get; private set; } // For COGS
}

Integration: Created from Finance module sales invoice events

Documentation: sales-details.md


PurchaseDetails

Purpose: Captures vendor and receiving information for purchases.

public class PurchaseDetails
{
public Guid VendorId { get; private set; }
public Guid ReceivingLocationId { get; private set; }
public string PurchaseOrderNumber { get; private set; }
public string InvoiceNumber { get; private set; }
public DateTime ReceiptDate { get; private set; }
public decimal UnitCost { get; private set; }
}

Integration: Created from Finance module purchase invoice events

Documentation: purchase-details.md


TransactionLineItem

Purpose: Individual item line within a transaction.

public class TransactionLineItem
{
public Guid ItemId { get; private set; }
public decimal Quantity { get; private set; }
public Guid UnitOfMeasureId { get; private set; }
public string Notes { get; private set; }

// Validation
public const decimal MaxQuantity = 999_999_999m;
public const int MaxDecimalPlaces = 4;
}

Validation:

  • Quantity > 0 and ≤ 999,999,999
  • Up to 4 decimal places
  • Valid item and unit of measure required

Domain Services

LocationHierarchyService Complete

Purpose: Validates location hierarchy operations.

Operations:

Task ValidateParentAsync(Guid? parentId, Guid currentLocationId)
Task<bool> CanSetAsParentAsync(Guid potentialParentId, Guid childId)
Task<List<Location>> GetDescendantsAsync(Guid locationId)

Business Logic:

  • Prevents circular references in hierarchy
  • Validates parent exists and is active
  • Ensures location cannot be its own ancestor

CategoryTreeService Complete

Purpose: Manages category hierarchy operations.

Operations:

Task<List<Category>> GetTreeAsync()
Task<List<Category>> GetDescendantsAsync(Guid categoryId)
Task<string> GetHierarchyPathAsync(Guid categoryId)

Business Logic:

  • Builds category tree for UI display
  • Generates breadcrumb paths
  • Validates hierarchy integrity

UnitConversionService Complete

Purpose: Performs unit of measure conversions.

Operations:

Task<decimal> ConvertAsync(
decimal quantity,
Guid fromUnitId,
Guid toUnitId)
Task<UnitConversion> GetConversionAsync(Guid fromUnitId, Guid toUnitId)

Business Logic:

  • Finds conversion path between units
  • Applies conversion factors
  • Validates units are in same class
  • Handles bidirectional conversions

Base Classes

Entity

Purpose: Base class for all domain entities.

public abstract class Entity
{
public Guid Id { get; protected set; }
public DateTime CreatedDate { get; protected set; }
public DateTime ModifiedDate { get; protected set; }
public bool IsActive { get; protected set; }

public virtual void Activate() => IsActive = true;
public virtual void Deactivate() => IsActive = false;
}

Features:

  • Unique ID (Guid)
  • Audit timestamps (automatic via DbContext)
  • Soft delete support (IsActive flag)
  • Equality based on ID

AggregateRoot

Purpose: Base class for aggregate roots with domain events.

public abstract class AggregateRoot : Entity
{
private readonly List<IDomainEvent> _domainEvents = new();

public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

protected void AddDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}

public void ClearDomainEvents() => _domainEvents.Clear();

// Optimistic concurrency support
public int Version { get; protected set; }
public void IncrementVersion() => Version++;
}

Features:

  • Domain event collection
  • Event publishing support
  • Optimistic locking via version
  • Automatic event dispatch after SaveChangesAsync

ValueObject

Purpose: Base class for value objects with value equality.

public abstract class ValueObject
{
protected abstract IEnumerable<object> GetEqualityComponents();

public override bool Equals(object obj)
{
// Equality by value, not identity
}

public override int GetHashCode()
{
// Hash based on all equality components
}
}

Features:

  • Immutable design
  • Equality by value
  • No identity (ID)
  • Cannot be modified after creation

Domain Events

Transaction Domain Events Complete

Events raised by InventoryTransaction aggregate:

// Base transaction created event
public abstract class TransactionCreatedDomainEvent : IDomainEvent
{
public Guid TransactionId { get; }
public TransactionType Type { get; }
public DateTime Timestamp { get; }
public Guid UserId { get; }
}

// Type-specific events inherit from base
public class MovementTransactionCreatedEvent : TransactionCreatedDomainEvent
{
public Guid SourceLocationId { get; }
public Guid DestinationLocationId { get; }
public IReadOnlyList<TransactionLineItem> LineItems { get; }
}

public class AdjustmentTransactionCreatedEvent : TransactionCreatedDomainEvent
{
public Guid LocationId { get; }
public TransactionLineItem LineItem { get; }
public decimal AdjustmentQuantity { get; }
}

// Similar for Assembly, Sale, Purchase

Handling:

  • Events published after UnitOfWork.CommitAsync()
  • Inventory handlers update quantities
  • Additional handlers can be added for reporting, notifications, etc.

Measurement Domain Events Complete

public class UnitConversionCreatedEvent : IDomainEvent
{
public Guid FromUnitId { get; }
public Guid ToUnitId { get; }
public decimal Factor { get; }
}

public class BaseUnitSetEvent : IDomainEvent
{
public Guid UnitClassId { get; }
public Guid BaseUnitId { get; }
}

Handling:

  • Cache invalidation for unit lookups
  • Recalculation of dependent conversions

Key Business Rules

Transaction Immutability

Rule: Inventory transactions cannot be modified after creation.

Rationale:

  • Complete audit trail for compliance
  • Historical accuracy preserved
  • Prevents data manipulation

Enforcement:

public class InventoryTransaction : AggregateRoot
{
// Private constructor prevents external instantiation
private InventoryTransaction() { }

// Only factory methods can create transactions
public static InventoryTransaction CreateMovement(...)
{
// Validation and creation logic
return new InventoryTransaction(...);
}

// NO update methods - transactions are immutable
}

Corrections: Use new adjustment transaction to correct errors


Stockable Behavior Composition

Rule: Only items with StockableBehavior can have inventory tracked.

Rationale:

  • Services and intangible items don't need stock tracking
  • Composition more flexible than inheritance
  • Behavior can be added/removed as needed

Enforcement:

public class Item : AggregateRoot
{
public StockableBehavior? StockableBehavior { get; private set; }

public bool IsStockable => StockableBehavior != null;

public void AddStockableBehavior(bool allowNegativeStock, bool trackByLocation)
{
if (StockableBehavior != null)
throw new DomainException("Item is already stockable");

StockableBehavior = new StockableBehavior(
allowNegativeStock,
trackByLocation);
}

public void RemoveStockableBehavior()
{
// Validate no active inventory exists
if (HasActiveInventory())
throw new DomainException("Cannot remove stockable behavior while inventory exists");

StockableBehavior = null;
}
}

Negative Stock Control

Rule: Items can allow or disallow negative inventory based on configuration.

Configuration: Per-item via StockableBehavior.AllowNegativeStock

Enforcement:

public class InventoryRecord : AggregateRoot
{
public void UpdateQuantity(decimal newQuantity, Item item)
{
if (newQuantity < 0 &&
item.StockableBehavior?.AllowNegativeStock == false)
{
throw new DomainException(
$"Item {item.ItemNumber} does not allow negative stock");
}

Quantity = newQuantity;
}
}

Business Decision:

  • Made-to-order items: Allow negative stock
  • Shelf stock items: Disallow negative stock

BOM Circular Reference Prevention

Rule: An item cannot be a component of itself (directly or indirectly).

Enforcement:

public class BillOfMaterial : AggregateRoot
{
public void AddComponent(Guid componentItemId, decimal quantity, Guid unitId)
{
if (componentItemId == ParentItemId)
throw new BOMCircularReferenceException(
"An item cannot be a component of itself");

// TODO: Check for indirect circular references (multi-level BOMs)

var component = new BillOfMaterialLine(componentItemId, quantity, unitId);
_lines.Add(component);
}
}

Cross-Aggregate Validation

Inventory Transaction Creation

Scenario: Creating a movement transaction requires valid items and locations.

Implementation:

public class CreateBulkMovementCommandHandler : IRequestHandler<CreateBulkMovementCommand, MovementDto>
{
public async Task<MovementDto> Handle(CreateBulkMovementCommand request, ...)
{
// 1. Validate locations exist
var sourceLocation = await _locationRepo.GetByIdAsync(request.SourceLocationId);
var destLocation = await _locationRepo.GetByIdAsync(request.DestinationLocationId);

// 2. Validate all items exist and are stockable
foreach (var line in request.LineItems)
{
var item = await _itemRepo.GetByIdAsync(line.ItemId);
if (!item.IsStockable)
throw new DomainException($"Item {item.ItemNumber} is not stockable");
}

// 3. Create transaction (aggregate enforces its own rules)
var transaction = InventoryTransaction.CreateMovement(
userId, sourceLocationId, destLocationId, lineItems, reason);

await _transactionRepo.AddAsync(transaction);
await _unitOfWork.CommitAsync(); // Publishes domain events

return MapToDto(transaction);
}
}

Pattern: Application layer coordinates cross-aggregate validation, domain layer enforces invariants


Integration Points

Finance Module Integration Active

Purchase Invoice Processing:

public class PurchaseInvoiceInventoryConsumer : BaseEventConsumer<PurchaseInvoiceInventoryPostingRequestedIntegrationEvent>
{
protected override async Task ProcessEvent(
PurchaseInvoiceInventoryPostingRequestedIntegrationEvent @event, ...)
{
try
{
// Create purchase transaction from finance event
var transaction = InventoryTransaction.CreatePurchaseTransaction(
userId: Guid.Parse(@event.UserId),
vendorId: @event.VendorId,
receivingLocationId: @event.LocationId,
lineItems: MapLineItems(@event.LineItems),
purchaseOrderNumber: @event.PurchaseOrderNumber,
invoiceNumber: @event.InvoiceNumber,
receiptDate: @event.PostingDate,
reason: "Purchase invoice posting");

await _transactionRepo.AddAsync(transaction);
await _unitOfWork.CommitAsync();

// Publish success event back to Finance
await _publisher.Publish(new PurchaseInvoiceInventoryPostedIntegrationEvent
{
InvoiceId = @event.InvoiceId,
TransactionId = transaction.Id,
PostedAt = DateTime.UtcNow
});
}
catch (Exception ex)
{
// Publish failure event back to Finance
await _publisher.Publish(new PurchaseInvoiceInventoryPostingFailedIntegrationEvent
{
InvoiceId = @event.InvoiceId,
ErrorMessage = ex.Message
});
}
}
}

Pattern: Event-driven, eventually consistent integration


Performance Considerations

Repository Caching

Strategy: Cache frequently accessed master data (items, locations, units)

public class CachedItemRepository : IItemRepository
{
private readonly IMemoryCache _cache;
private readonly IItemRepository _innerRepository;

public async Task<Item> GetByIdAsync(Guid id)
{
return await _cache.GetOrCreateAsync(
$"item:{id}",
async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
return await _innerRepository.GetByIdAsync(id);
});
}
}

Bulk Transaction Processing

Optimization: Process multiple transactions in a single database roundtrip

public async Task CreateBulkTransactions(List<MovementRequest> requests)
{
var transactions = new List<InventoryTransaction>();

foreach (var request in requests)
{
var transaction = InventoryTransaction.CreateMovement(...);
transactions.Add(transaction);
}

await _transactionRepo.AddRangeAsync(transactions);
await _unitOfWork.CommitAsync(); // Single commit for all
}

Inventory Query Optimization

Indexing: Strategic indexes for common queries

-- Index for item-location lookups
CREATE INDEX idx_inventory_item_location
ON inventory_records (item_id, location_id);

-- Index for location-based queries
CREATE INDEX idx_inventory_location
ON inventory_records (location_id)
WHERE is_active = true;

Testing Strategies

Aggregate Testing

[TestFixture]
public class InventoryTransactionTests
{
[Test]
public void CreateMovement_ValidInputs_CreatesTransaction()
{
// Arrange
var userId = Guid.NewGuid();
var sourceId = Guid.NewGuid();
var destId = Guid.NewGuid();
var lineItems = CreateValidLineItems();

// Act
var transaction = InventoryTransaction.CreateMovement(
userId, sourceId, destId, lineItems, "Test movement");

// Assert
Assert.That(transaction.Type, Is.EqualTo(TransactionType.Movement));
Assert.That(transaction.MovementDetails.SourceLocationId, Is.EqualTo(sourceId));
Assert.That(transaction.MovementDetails.DestinationLocationId, Is.EqualTo(destId));
Assert.That(transaction.LineItems, Has.Count.EqualTo(lineItems.Count));
}

[Test]
public void CreateMovement_SameSourceAndDestination_ThrowsException()
{
// Arrange
var sameLocationId = Guid.NewGuid();

// Act & Assert
Assert.Throws<DomainException>(() =>
InventoryTransaction.CreateMovement(
Guid.NewGuid(),
sameLocationId,
sameLocationId, // Same as source
CreateValidLineItems(),
"Invalid movement"));
}
}

Domain Event Testing

[TestFixture]
public class TransactionDomainEventTests
{
[Test]
public async Task CreateMovement_RaisesDomainEvent()
{
// Arrange
var transaction = InventoryTransaction.CreateMovement(...);

// Act - domain events are in collection
var events = transaction.DomainEvents;

// Assert
Assert.That(events, Has.Count.EqualTo(1));
Assert.That(events.First(), Is.TypeOf<MovementTransactionCreatedEvent>());
}

[Test]
public async Task CommitAsync_PublishesEventsToInventoryHandler()
{
// Integration test with real DbContext and MediatR
// Verify inventory quantities updated after transaction commit
}
}

Integration Event Testing

[TestFixture]
public class PurchaseInvoiceConsumerTests
{
[Test]
public async Task ProcessEvent_ValidInvoice_CreatesTransaction()
{
// Arrange
var consumer = new PurchaseInvoiceInventoryConsumer(...);
var @event = CreateValidPurchaseInvoiceEvent();

// Act
await consumer.Consume(@event);

// Assert
var transaction = await _transactionRepo.GetByIdAsync(...);
Assert.That(transaction.Type, Is.EqualTo(TransactionType.Purchase));
}
}

Future Enhancements

Advanced Features

  • Multi-level BOM: Support for nested bills of materials
  • Serial Number Tracking: Individual item serialization
  • Lot/Batch Tracking: Batch number management for traceability
  • Expiration Date Tracking: For perishable items
  • Quality Control Integration: Hold/release inventory based on QC
  • Consignment Inventory: Track vendor-owned stock

Performance Optimizations

  • Event Sourcing: Store all changes as events for complete history
  • Read Models: Separate read models for complex queries
  • Caching Strategy: Redis for distributed caching
  • Bulk Operations: Optimized bulk transaction processing

Implementation References

Cross-Module References


Last Updated: 2025-10-23 | Version: 1.0 | Status: Phase 1 - Transaction Focus Complete

Priority Items:

  1. Complete InventoryTransaction aggregate documentation ✓
  2. Complete all 5 type-specific value object documentation
  3. Complete InventoryRecord aggregate documentation ✓
  4. Add integration event flow diagrams
  5. Performance optimization for high-volume transactions