Measurements Domain
Overview
The Measurements domain manages units of measure, unit classes, measurement systems, and unit conversions for the inventory system. It provides the foundational measurement infrastructure that enables flexible quantity tracking, unit conversions, and multi-system support (Metric, Imperial) across all inventory operations.
Core Concepts
Three-Tier Measurement Architecture
The Measurements domain uses a three-tier structure for maximum flexibility:
Measurement System (Metric, Imperial, Custom)
↓
Unit Class (Weight, Length, Volume, Count, Time)
↓
Unit of Measure (kg, lb, m, ft, L, gal, EA, hr)
Measurement System
Purpose: Top-level categorization of units (Metric vs Imperial vs Custom)
Examples:
- Metric: Kilograms, meters, liters
- Imperial: Pounds, feet, gallons
- Custom: Business-specific units (pallets, cases, boxes)
Unit Class
Purpose: Categorizes units by what they measure (dimension of measurement)
Examples:
- Weight: kg, lb, g, oz, ton
- Length: m, ft, cm, in, km
- Volume: L, gal, mL, fl oz, m³
- Count: Each, Dozen, Pair
- Time: Hour, Day, Week
Key Feature: Items reference UnitClass (not specific units), allowing transaction-time unit flexibility.
Unit of Measure
Purpose: Specific measurement unit with symbol, name, and precision
Properties:
- Symbol: Short code (kg, lb, m)
- Name: Full name (Kilogram, Pound, Meter)
- Decimal Precision: Number of decimal places (0-10)
- Unit Class: Which class it belongs to
- Measurement System: Which system it belongs to
Unit Conversions
Purpose: Define conversion factors between units in the same class
Structure: Direct conversions between unit pairs
- kg → lb: 2.20462
- lb → kg: 0.453592
- m → ft: 3.28084
- ft → m: 0.3048
Business Rules:
- Only convert within same unit class
- Bidirectional conversions typically both defined
- Conversion factors can be updated
- No conversion to self
Domain Structure
Aggregates
measurements/
├── aggregates/
│ ├── unit-of-measure.aggregate.md # UnitOfMeasure aggregate
│ ├── unit-class.aggregate.md # UnitClass aggregate
│ └── measurement-system.aggregate.md # MeasurementSystem aggregate
Entities
measurements/
├── entities/
│ ├── unit-of-measure.md # Unit entity
│ ├── unit-class.md # Class entity
│ ├── unit-conversion.md # Conversion entity
│ └── measurement-system.md # System entity
Key Aggregates
UnitOfMeasure
Aggregate Root: UnitOfMeasure
Purpose: Represents a specific unit of measurement with conversion capabilities.
Key Characteristics:
- Has unique symbol (kg, lb, m)
- Belongs to one UnitClass
- Belongs to one MeasurementSystem
- Contains conversion collection
- Defines decimal precision
Relationships:
- Belongs to one
UnitClass - Belongs to one
MeasurementSystem - Has many
UnitConversioninstances (from this unit) - Referenced by many Items, Transactions, BOM lines
Core Operations:
- Create unit with class and system
- Update unit properties
- Add conversion to another unit
- Remove conversion
- Update conversion factor
- Get conversion to specific unit
UnitClass
Entity: UnitClass
Purpose: Categorizes units by dimension of measurement.
Key Characteristics:
- Groups related units (all weight units, all length units, etc.)
- Defines base unit for the class
- Defines preferred display unit
- Contains collection of units in class
Core Operations:
- Create unit class
- Set base unit of measure
- Set preferred display unit
- Update class information
MeasurementSystem
Entity: MeasurementSystem
Purpose: Top-level classification (Metric, Imperial, Custom).
Key Characteristics:
- Organizational categorization
- Supports multiple measurement systems
- Contains collection of units in system
UnitConversion
Entity: UnitConversion
Purpose: Defines conversion factor between two units in same class.
Key Properties:
FromUnitId: Source unitToUnitId: Target unitFactor: Multiplication factor for conversionUnitClassId: Must be same class for both units
Conversion Formula:
Target Quantity = Source Quantity × Factor
Example:
10 kg × 2.20462 = 22.0462 lb
Business Rules
Unit Creation Rules
- Symbol Required: Unit symbol cannot be empty
- Name Required: Unit name cannot be empty
- Unit Class Required: Every unit must belong to a class
- Measurement System Required: Every unit must belong to a system
- Precision Range: Decimal precision must be 0-10
- Symbol Uniqueness: Symbols should be unique (validated at app level)
Unit Class Rules
- Name Required: Unit class name cannot be empty
- Base Unit: Base unit must belong to the class
- Preferred Unit: Preferred display unit must belong to the class
- Unit Collection: Class maintains collection of its units
Conversion Rules
- Same Class Only: Can only convert between units in same class
- No Self-Conversion: Cannot create conversion from unit to itself
- Positive Factors: Conversion factors must be positive
- Bidirectional: Typically define both directions (kg→lb and lb→kg)
- Update Support: Conversion factors can be updated
Precision Rules
- Range: Decimal precision 0-10
- Application: Applied when displaying quantities
- Rounding: Quantities rounded to specified precision
- Storage: Full precision stored, rounded on display
Integration Points
With Item Module
- Unit Class Reference: Items reference UnitClass (not specific unit)
- Transaction Flexibility: Allows any unit in class for transactions
- Default Unit: Items can specify preferred unit class
- Validation: Ensures transaction units match item's unit class
With Transaction Module
- Unit Specification: Every transaction line specifies unit
- Unit Validation: Validates unit matches item's unit class
- Conversion Support: Future support for automatic conversions
- Precision Application: Quantities rounded per unit precision
With BOM Module
- Component Units: BOM lines specify component units
- Produced Unit: BOMs specify produced unit
- Explosion: Units preserved through explosion
- Unit Compatibility: Validates units match item classes
With Inventory Module
- Quantity Storage: Inventory quantities stored with units
- Unit Consistency: Tracks quantities in specified units
- Conversion Queries: Future support for unit-converted queries
- Display Preferences: Respects preferred display units
Common Patterns
Creating Measurement Infrastructure
// 1. Create measurement systems
var metric = MeasurementSystem.Create("Metric",
"International System of Units (SI)");
var imperial = MeasurementSystem.Create("Imperial",
"Imperial/US customary units");
// 2. Create unit classes
var weightClass = UnitClass.Create("Weight",
"Units of mass/weight");
var lengthClass = UnitClass.Create("Length",
"Units of distance/length");
var countClass = UnitClass.Create("Count",
"Counting units");
// 3. Save systems and classes
await measurementSystemRepo.AddAsync(metric);
await measurementSystemRepo.AddAsync(imperial);
await unitClassRepo.AddAsync(weightClass);
await unitClassRepo.AddAsync(lengthClass);
await unitClassRepo.AddAsync(countClass);
await unitOfWork.CommitAsync();
Creating Units of Measure
// Create metric weight units
var kilogram = UnitOfMeasure.Create(
symbol: "kg",
name: "Kilogram",
unitClass: weightClass,
measurementSystem: metric,
decimalPrecision: 3);
var gram = UnitOfMeasure.Create(
symbol: "g",
name: "Gram",
unitClass: weightClass,
measurementSystem: metric,
decimalPrecision: 2);
// Create imperial weight units
var pound = UnitOfMeasure.Create(
symbol: "lb",
name: "Pound",
unitClass: weightClass,
measurementSystem: imperial,
decimalPrecision: 2);
var ounce = UnitOfMeasure.Create(
symbol: "oz",
name: "Ounce",
unitClass: weightClass,
measurementSystem: imperial,
decimalPrecision: 2);
// Create count unit (system-agnostic)
var each = UnitOfMeasure.Create(
symbol: "EA",
name: "Each",
unitClass: countClass,
measurementSystem: metric, // Or custom system
decimalPrecision: 0);
await unitRepo.AddAsync(kilogram);
await unitRepo.AddAsync(gram);
await unitRepo.AddAsync(pound);
await unitRepo.AddAsync(ounce);
await unitRepo.AddAsync(each);
await unitOfWork.CommitAsync();
Setting Up Unit Class Defaults
// Set base unit for weight class (kilogram)
weightClass.SetBaseUnitOfMeasure(kilogram);
// Set preferred display unit (kilogram)
weightClass.SetPreferredDisplayUnit(kilogram);
await unitOfWork.CommitAsync();
Creating Bidirectional Conversions
// kg ↔ lb conversions
kilogram.AddConversion(pound, 2.20462m); // 1 kg = 2.20462 lb
pound.AddConversion(kilogram, 0.453592m); // 1 lb = 0.453592 kg
// kg ↔ g conversions
kilogram.AddConversion(gram, 1000m); // 1 kg = 1000 g
gram.AddConversion(kilogram, 0.001m); // 1 g = 0.001 kg
// lb ↔ oz conversions
pound.AddConversion(ounce, 16m); // 1 lb = 16 oz
ounce.AddConversion(pound, 0.0625m); // 1 oz = 0.0625 lb
await unitOfWork.CommitAsync();
// Domain events fired for each conversion creation
Using Unit Conversions
// 1. Load source unit
var kg = await unitRepo.GetBySymbolAsync("kg");
// 2. Get conversion to target unit
var poundUnitId = await unitRepo.GetIdBySymbolAsync("lb");
var conversion = kg.GetConversionTo(poundUnitId);
if (conversion != null)
{
// 3. Convert quantity
decimal sourceQuantity = 50m; // 50 kg
decimal targetQuantity = sourceQuantity * conversion.Factor;
// Result: 50 × 2.20462 = 110.231 lb
Console.WriteLine($"{sourceQuantity} kg = {targetQuantity} lb");
}
else
{
throw new ConversionNotFoundException(
"No conversion found from kg to lb");
}
Getting Compatible Units for Item
// 1. Load item
var item = await itemRepo.GetByIdAsync(itemId);
if (item.DefaultUnitClassId.HasValue)
{
// 2. Load unit class
var unitClass = await unitClassRepo.GetByIdAsync(
item.DefaultUnitClassId.Value);
// 3. Get all units in class
var compatibleUnits = unitClass.UnitsInClass;
// 4. Present to user for transaction
foreach (var unit in compatibleUnits)
{
Console.WriteLine($"{unit.Symbol} - {unit.Name}");
}
// User can select any unit in the class for transaction
// Example output for Weight class:
// kg - Kilogram
// g - Gram
// lb - Pound
// oz - Ounce
// ton - Ton
}
Updating Conversion Factor
// Load unit
var kg = await unitRepo.GetBySymbolAsync("kg");
// Update existing conversion
var lbUnitId = await unitRepo.GetIdBySymbolAsync("lb");
kg.UpdateConversion(lbUnitId, 2.20463m); // Updated factor
await unitOfWork.CommitAsync();
// Conversion factor updated for future conversions
Repository Pattern
IUnitOfMeasureRepository
Query Methods:
GetByIdAsync(Guid id): Get unit with conversionsGetAllAsync(): Get all active unitsGetBySymbolAsync(string symbol): Find unit by symbolGetByClassAsync(Guid unitClassId): Get units in classGetBySystemAsync(Guid systemId): Get units in system
Conversion Methods:
GetConversionAsync(Guid fromUnitId, Guid toUnitId): Get specific conversionGetAllConversionsForUnitAsync(Guid unitId): Get all conversions from unitHasConversionAsync(Guid fromUnitId, Guid toUnitId): Check conversion exists
Validation Methods:
ExistsBySymbolAsync(string symbol): Check symbol existsIsSymbolUniqueAsync(string symbol, Guid? excludeId): Validate uniqueness
Command Methods:
AddAsync(UnitOfMeasure unit): Create unitUpdate(UnitOfMeasure unit): Update unitArchive(UnitOfMeasure unit): Soft deleteUnArchive(UnitOfMeasure unit): Restore
IUnitClassRepository
Query Methods:
GetByIdAsync(Guid id): Get class with unitsGetAllAsync(): Get all classesGetByNameAsync(string name): Find by nameGetWithUnitsAsync(Guid id): Load class with all units
Command Methods:
AddAsync(UnitClass unitClass): Create classUpdate(UnitClass unitClass): Update classArchive(UnitClass unitClass): Soft delete
IMeasurementSystemRepository
Query Methods:
GetByIdAsync(Guid id): Get systemGetAllAsync(): Get all systemsGetByNameAsync(string name): Find by name
Command Methods:
AddAsync(MeasurementSystem system): Create systemUpdate(MeasurementSystem system): Update system
Domain Services
UnitConversionService (Future Enhancement)
Purpose: Provides advanced conversion operations including multi-step conversions.
Planned Methods:
Task<decimal> ConvertAsync(
decimal quantity,
Guid fromUnitId,
Guid toUnitId)
Task<ConversionPath> FindConversionPathAsync(
Guid fromUnitId,
Guid toUnitId)
Task<IEnumerable<UnitOfMeasure>> GetCompatibleUnitsAsync(
Guid unitId)
Domain Events
Unit Events
- UnitOfMeasureCreatedEvent: Raised when new unit created
- UnitOfMeasureUpdatedEvent: Raised when unit properties updated
- UnitOfMeasureArchivedEvent: Raised when unit archived
Conversion Events
- UnitConversionCreatedEvent: Raised when conversion added
- Contains: FromUnit, ToUnit, Factor, UnitClass
- Enables: Conversion cache updates, validation triggers
Class Events
- BaseUnitSetEvent: Raised when base unit assigned to class
- BaseUnitUnsetEvent: Raised when base unit removed from class
Testing Strategy
Unit Tests
- Factory Method Tests: Unit creation with various combinations
- Conversion Tests: Add/update/remove conversions
- Validation Tests: Business rule enforcement
- Precision Tests: Decimal precision handling
Integration Tests
- Repository Tests: Database operations
- Conversion Persistence: Conversions correctly saved/loaded
- Class Relationships: Unit class associations
- System Relationships: Measurement system associations
Performance Considerations
Optimization Strategies
- Conversion Caching: Cache frequently used conversions
- Unit Class Loading: Eager load units in class
- Symbol Lookups: Indexed symbol searches
- Conversion Graphs: Pre-compute conversion paths (future)
Database Optimization
- Primary Keys: Clustered indexes on IDs
- Symbol Index: Unique index on Symbol
- Conversion Indexes: Composite index on (FromUnit, ToUnit)
- Class Indexes: Foreign key indexes for queries
Future Enhancements
Planned Features
- Multi-Step Conversions: kg → g → oz (through intermediate units)
- Conversion Validation: Verify conversion accuracy
- Historical Conversions: Track conversion factor history
- Currency Units: Support for monetary units
- Custom Formulas: Complex conversions beyond simple factors
- Unit Localization: Display units in user's locale
Related Documentation
Domain Documentation
API Documentation
Concept Documentation
Last Updated: 2025-10-24 | Status: Production Ready | Version: 1.0