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:
- Purchase - Receiving inventory from vendors
- Sale - Shipping inventory to customers
- Movement - Transferring stock between locations
- Adjustment - Correcting inventory discrepancies
- 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
Itementity + optionalStockableBehavior - Stockable items track: AllowNegativeStock, TrackByLocation
- Non-stockable items are services or intangible products
Domain Events:
ItemCreatedDomainEventStockableBehaviorAddedDomainEventStockableBehaviorRemovedDomainEvent
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 componentsAddComponent()- adds component to BOMSyncComponents()- 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
Related Documentation
Implementation References
- API Documentation - REST endpoints exposing domain operations
- Transaction Types Concept - Detailed transaction workflows
- Architecture Patterns - System design patterns
Cross-Module References
- Finance Module Integration - Purchase/sales integration
- ERP Integration Overview - Complete ERP system context
Last Updated: 2025-10-23 | Version: 1.0 | Status: Phase 1 - Transaction Focus Complete
Priority Items:
- Complete InventoryTransaction aggregate documentation ✓
- Complete all 5 type-specific value object documentation
- Complete InventoryRecord aggregate documentation ✓
- Add integration event flow diagrams
- Performance optimization for high-volume transactions