Bill of Materials (BOM) Domain
Overview
The Bill of Materials (BOM) domain manages the structural relationships between parent items and their component items, defining the "recipes" used in manufacturing and assembly operations. It provides the foundation for material planning, component explosion, assembly transactions, and production costing.
Core Concepts
Bill of Materials (BOM)
A BOM is a formal definition of the components required to produce one unit of a parent item. It answers the question: "What do I need to make this product?"
Key Characteristics:
- Defines parent-child item relationships
- Specifies exact quantities of each component
- Associates with specific unit of measure for output
- Used for material requirements planning
- Drives assembly transaction validation
Component Explosion
Component explosion is the process of calculating total component requirements based on desired production quantity:
BOM Definition:
Parent: Widget (1 EA)
├── Steel Plate: 2.5 kg
├── Bolt M10: 4 EA
└── Paint: 0.1 L
Production Order: 10 Widgets
Exploded Requirements:
├── Steel Plate: 25 kg (2.5 × 10)
├── Bolt M10: 40 EA (4 × 10)
└── Paint: 1.0 L (0.1 × 10)
Single-Level vs Multi-Level BOMs
Current Implementation: Single-Level BOMs
- Parent item has components
- Components are leaf items (not further expanded)
- Sufficient for most manufacturing scenarios
Future Enhancement: Multi-Level BOMs
- Components can themselves have BOMs (sub-assemblies)
- Recursive explosion through entire product structure
- Full manufacturing hierarchy support
Domain Structure
Aggregates
bom/
├── aggregates/
│ └── bill-of-material.aggregate.md # BOM aggregate documentation
Entities
bom/
└── entities/
├── bill-of-material.md # BOM entity (aggregate root)
└── bill-of-material-line.md # Component line entity
Value Objects
bom/
└── value-objects/
└── required-component.md # Exploded component requirement
Key Aggregates
BillOfMaterial
Aggregate Root: BillOfMaterial
Purpose: Defines the component structure and quantities required to produce a parent item.
Key Characteristics:
- Associates with one parent item (finished good/assembly)
- Specifies produced unit of measure
- Contains collection of component lines
- Supports component explosion calculations
- Validates component integrity
Relationships:
- References one parent
Item(required) - References one
UnitOfMeasurefor produced output (required) - Contains many
BillOfMaterialLineentities (1 to many) - Each line references one component
Item - Each line references one component
UnitOfMeasure
Core Operations:
- Create BOM for parent item
- Add component items with quantities
- Update BOM header information
- Sync components (add/update/remove)
- Explode components for production quantity
- Validate component relationships
BillOfMaterialLine
Entity: Component line within a BOM
Purpose: Represents a single component requirement within the BOM.
Key Characteristics:
- Immutable after creation
- Defines component item and quantity
- Specifies unit of measure for component
- Part of parent BOM aggregate
Properties:
ComponentItemId: Reference to component itemQuantity: Amount required per unit of parentUnitOfMeasureId: Unit for component quantity
Business Rules
BOM Creation Rules
- Parent Item Required: BOM must be associated with a valid parent item
- Produced Unit Required: BOM must specify which unit of measure is produced
- Name Required: BOM must have a descriptive name
- Description Optional: Additional details can be provided
Component Rules
- No Circular References: Parent item cannot be its own component
- Unique Components: Each component item can appear only once in a BOM
- Positive Quantities: Component quantities must be greater than zero
- Valid Units: Component units must be valid for the component item
- Component Item Required: Each line must reference a valid item
Explosion Rules
- Positive Production Quantity: Cannot explode for zero or negative quantity
- Empty BOM Handling: BOMs with no components return empty explosion
- Quantity Multiplication: Component quantity × production quantity = required quantity
- Unit Preservation: Exploded components retain their specified units
Update Rules
- Header Updates: Name, description, and produced unit can be updated
- Component Sync: SyncComponents() method handles add/update/remove in single operation
- Validation Consistency: All component validations apply during updates
- Immutable Lines: Lines are replaced rather than modified (immutable pattern)
Integration Points
With Item Module
- Parent Item Validation: Verifies parent item exists and is valid
- Component Item Validation: Verifies component items exist and are stockable
- Unit Class Compatibility: Validates units match item's unit class
- Stockable Requirement: Typically both parent and components are stockable items
With Transaction Module (Assembly)
- Assembly Validation: Assembly transactions validate BOM exists for parent item
- Component Verification: Verifies required components are available
- Quantity Explosion: Calculates component requirements for assembly quantity
- Inventory Consumption: Assembly transactions consume components per BOM
- Production Output: Assembly transactions produce parent item per BOM
With Inventory Module
- Availability Checking: Verifies sufficient component inventory before assembly
- Component Reservation: Future enhancement for reserving components
- Multi-Location Support: Components can be sourced from multiple locations
- Backflush Processing: Future enhancement for automated component consumption
With Finance Module (Future)
- Standard Costing: BOM used to calculate standard product costs
- Variance Analysis: Actual vs standard component usage
- COGS Calculation: Component costs roll up to finished goods cost
- Material Overhead: BOM quantities drive overhead allocations
Common Patterns
Creating a Simple BOM
// 1. Create BOM header
var bom = BillOfMaterial.Create(
parentItemId: finishedWidgetId,
producedUnitOfMeasureId: eachUnitId,
name: "Standard Widget Assembly",
description: "Primary assembly process for standard widget production");
// 2. Add component items
bom.AddComponent(
componentItemId: steelPlateItemId,
quantity: 2.5m,
unitOfMeasureId: kilogramUnitId);
bom.AddComponent(
componentItemId: boltItemId,
quantity: 4m,
unitOfMeasureId: eachUnitId);
bom.AddComponent(
componentItemId: paintItemId,
quantity: 0.1m,
unitOfMeasureId: literUnitId);
// 3. Save BOM
await bomRepository.AddAsync(bom);
await unitOfWork.CommitAsync();
Component Explosion for Production
// 1. Load BOM
var bom = await bomRepository.GetByParentItemAsync(widgetItemId);
// 2. Explode for production quantity
decimal productionQuantity = 100m; // Produce 100 widgets
var requiredComponents = bom.Explode(productionQuantity);
// 3. Process exploded requirements
foreach (var component in requiredComponents)
{
Console.WriteLine($"Need {component.Quantity} {component.UnitOfMeasureId} " +
$"of item {component.ComponentItemId}");
// Check inventory availability
var available = await inventoryService
.GetAvailableQuantityAsync(component.ComponentItemId, locationId);
if (available < component.Quantity)
{
throw new InsufficientInventoryException(
$"Insufficient inventory for component {component.ComponentItemId}");
}
}
// Result for 100 widgets:
// - Steel Plate: 250 kg (2.5 × 100)
// - Bolt M10: 400 EA (4 × 100)
// - Paint: 10 L (0.1 × 100)
Updating BOM Components (Sync Pattern)
// 1. Load existing BOM
var bom = await bomRepository.GetByIdAsync(bomId);
// 2. Define desired component state
var desiredComponents = new[]
{
(steelPlateItemId, 3.0m, kilogramUnitId), // Updated quantity
(boltItemId, 4m, eachUnitId), // Unchanged
(paintItemId, 0.15m, literUnitId), // Updated quantity
(newLabelItemId, 1m, eachUnitId) // New component
// Old component removed (not in desired state)
};
// 3. Sync components in single operation
bom.SyncComponents(desiredComponents);
// 4. Save changes
await unitOfWork.CommitAsync();
// Result:
// - Updates existing components with new quantities
// - Adds new components
// - Removes components not in desired state
// - All in single atomic operation
Updating BOM Header
var bom = await bomRepository.GetByIdAsync(bomId);
bom.UpdateHeader(
name: "Updated Widget Assembly - Version 2",
description: "Revised assembly process with efficiency improvements",
producedUnitOfMeasureId: null); // Keep existing unit
await unitOfWork.CommitAsync();
Assembly Transaction Using BOM
// 1. Load BOM for assembly
var bom = await bomRepository.GetByParentItemAsync(parentItemId);
// 2. Explode for desired production quantity
var componentsNeeded = bom.Explode(assemblyQuantity);
// 3. Create assembly transaction
var transaction = InventoryTransaction.CreateAssembly(
transactionDate: DateTime.UtcNow,
reason: "Production run #1234",
billOfMaterialId: bom.Id,
producedItemId: parentItemId,
producedQuantity: assemblyQuantity,
producedUnitOfMeasureId: bom.ProducedUnitOfMeasureId,
assemblyLocationId: productionLocationId);
// 4. Add transaction lines for each component
foreach (var component in componentsNeeded)
{
transaction.AddLineItem(
itemId: component.ComponentItemId,
quantity: component.Quantity,
unitOfMeasureId: component.UnitOfMeasureId);
}
// 5. Process transaction
await transactionRepository.AddAsync(transaction);
await unitOfWork.CommitAsync();
// Result:
// - Components consumed from inventory
// - Finished goods added to inventory
// - Complete audit trail maintained
Repository Pattern
IBillOfMaterialRepository
Query Methods:
GetByIdAsync(Guid id): Get BOM with all lines and related itemsGetAllAsync(): Get all active BOMsGetByParentItemAsync(Guid parentItemId): Find BOM for specific parent itemGetByComponentItemAsync(Guid componentItemId): Find BOMs using specific component
Specialized Query Methods:
GetBOMsWithComponentsAsync(): Get BOMs with full component details loadedExistsByParentItemAsync(Guid parentItemId): Check if parent item has BOMGetParentItemsWithBOMsAsync(): Get list of items that have BOMs defined
Validation Methods:
HasBOMAsync(Guid parentItemId): Verify BOM exists for itemIsComponentUsedAsync(Guid componentItemId): Check if item is used as componentCanDeleteBOMAsync(Guid bomId): Validate BOM can be safely deleted
Command Methods:
AddAsync(BillOfMaterial bom): Create new BOMUpdate(BillOfMaterial bom): Update existing BOMArchive(BillOfMaterial bom): Soft delete BOMUnArchive(BillOfMaterial bom): Restore archived BOM
Domain Services
IBOMService (Future Enhancement)
Purpose: Coordinates complex BOM operations and multi-level explosions.
Planned Methods:
Multi-Level Explosion:
Task<IEnumerable<RequiredComponent>> ExplodeMultiLevelAsync(Guid parentItemId, decimal quantity)
- Recursively explodes BOMs through entire product structure
- Handles sub-assemblies automatically
- Aggregates component requirements across levels
Validation Services:
Task<ValidationResult> ValidateBOMIntegrityAsync(Guid bomId)
- Validates all components exist and are stockable
- Checks for circular references in multi-level BOMs
- Verifies unit compatibility
Cost Roll-Up:
Task<decimal> CalculateStandardCostAsync(Guid parentItemId)
- Calculates total component cost
- Includes material overhead
- Supports standard costing
Testing Strategy
Unit Tests
- Factory Method Tests: BOM creation with various configurations
- Component Addition Tests: Add components with validation
- Explosion Tests: Verify explosion calculations for various quantities
- Circular Reference Tests: Prevent parent as component
- Duplicate Component Tests: Prevent same component twice
- Sync Tests: Verify add/update/remove logic in SyncComponents
Integration Tests
- Repository Tests: Database operations and query correctness
- Line Persistence: BOM lines correctly saved and loaded
- Item Relationships: Parent and component item navigation
- Explosion Integration: Full BOM explosion with database data
- Transaction Integration: Assembly transactions use BOM correctly
Assembly Integration Tests
- BOM-Based Assembly: Create assembly transaction using BOM
- Component Validation: Verify component availability
- Inventory Updates: Verify inventory correctly updated
- Multi-Component Assembly: Test BOMs with multiple components
Performance Considerations
Optimization Strategies
- Eager Loading: Load BOM lines and items in single query
- Explosion Caching: Cache exploded results for repeated assemblies
- Query Optimization: Indexed queries on parent and component items
- Component Lookup: Efficient component existence checks
Scalability Metrics
- BOM Explosion: <10ms for single-level BOM with 10 components
- Multi-Level Explosion: <50ms for 3-level BOM (future)
- Component Search: <5ms to find BOMs using component
- Memory Footprint: ~1KB per BOM + 200 bytes per line
Database Optimization
- Primary Keys: Clustered index on BOM Id
- Foreign Keys: Indexed on ParentItemId, ComponentItemId
- Line Queries: Composite index on BOMId + ComponentItemId
- Explosion Support: Optimized for read-heavy explosion operations
Future Enhancements
Planned Features
- Multi-Level BOMs: Recursive component explosion through sub-assemblies
- BOM Versions: Version control for BOM changes over time
- Effective Dating: Start/end dates for BOM validity
- Alternate BOMs: Multiple BOMs for same parent item (different production methods)
- Co-Products: Support for processes that produce multiple outputs
- By-Products: Handle processes with secondary outputs
- Scrap Factor: Built-in scrap/waste calculations
- Operation Routing: Link BOMs to production routings
- Substitutions: Define component substitutes
Multi-Level BOM Design
// Future: Recursive explosion
public class BOMService
{
public async Task<IEnumerable<RequiredComponent>> ExplodeMultiLevelAsync(
Guid parentItemId,
decimal quantity,
int maxDepth = 10)
{
var components = new List<RequiredComponent>();
await ExplodeRecursiveAsync(parentItemId, quantity, components, 0, maxDepth);
// Aggregate duplicate components
return components
.GroupBy(c => new { c.ComponentItemId, c.UnitOfMeasureId })
.Select(g => new RequiredComponent(
g.Key.ComponentItemId,
g.Sum(c => c.Quantity),
g.Key.UnitOfMeasureId));
}
private async Task ExplodeRecursiveAsync(...)
{
// Recursive explosion logic
}
}
BOM Versioning Design
// Future: Version control
public class BillOfMaterial
{
public int Version { get; private set; }
public DateTime EffectiveFrom { get; private set; }
public DateTime? EffectiveTo { get; private set; }
public bool IsActive { get; private set; }
public BillOfMaterial CreateNewVersion(string reason)
{
// Creates new version, marks current as inactive
}
}
Exceptions
BOMCircularReferenceException
Purpose: Thrown when attempting to add parent item as its own component.
Usage:
throw new BOMCircularReferenceException(
"A BOM's parent item cannot be one of its own components.");
Prevention: Validate componentItemId ≠ parentItemId before adding component.
Related Documentation
Domain Documentation
- BOM Aggregate - Detailed aggregate documentation
- BOM Line Entity - Component line details
- Required Component - Explosion result value object
API Documentation
- BOM API - REST endpoints for BOM management
Concept Documentation
- Assembly Workflow - End-to-end assembly process
- Component Explosion - Explosion calculation details
- Multi-Level BOMs - Future multi-level design
Architecture Documentation
- BOM Domain Architecture - Architectural decisions
Last Updated: 2025-10-24 | Status: Active Development | Version: 1.0