Skip to main content

Adopting Composition Over Inheritance for Item Stockability

Architectural Decision Record: Composition Over Inheritance for Item Stockability

1. Executive Summary

This document formalizes the architectural decision to use composition over inheritance for managing item stockability in the Inventory Management Module. Instead of creating separate StockableItem and NonStockableItem classes through inheritance, we maintain a single unified Item aggregate that can optionally compose a StockableBehavior value object.

This decision enables:

  • Maximum flexibility in item lifecycle (items can transition from non-stockable to stockable)
  • Cleaner aggregate boundaries without discriminator bloat
  • Better alignment with Domain-Driven Design principles
  • Simplified persistence and querying
  • Support for future behavioral extensions

This composition-based approach has been adopted as the strategic pattern for behavior management across the entire ERP system, providing consistency and proven scalability.

2. Context: The Problem Space

In any inventory system, items fall into two fundamental categories:

Stockable Items:

  • Physical goods tracked in inventory
  • Have quantities at locations
  • Participate in inventory transactions
  • Examples: Raw materials, finished goods, components

Non-Stockable Items:

  • Services or intangibles not tracked in inventory
  • No location-based quantities
  • May appear on purchase orders or sales orders but don't affect inventory levels
  • Examples: Consulting services, warranties, software licenses

The architectural challenge is how to model this distinction while maintaining system flexibility and clean domain boundaries.

3. The Architectural Debate: Inheritance vs. Composition

3.1. Inheritance Approach (Rejected)

public abstract class Item : AggregateRoot
{
public string ItemNumber { get; protected set; }
public string Name { get; protected set; }
public Guid CategoryId { get; protected set; }
}

public class StockableItem : Item
{
public bool AllowNegativeStock { get; private set; }
public bool TrackByLocation { get; private set; }
public decimal OnHandQuantity { get; private set; }

public void AdjustQuantity(decimal quantity) { /* ... */ }
}

public class NonStockableItem : Item
{
// No inventory-specific properties or methods
}

Arguments For Inheritance:

  • Compile-time type safety: A StockableItem is guaranteed to have quantity tracking
  • Clear distinction between item types
  • Polymorphic behavior through method overriding
  • Follows traditional OOP patterns familiar to many developers

Arguments Against Inheritance (Why We Rejected It):

  1. Rigidity: Cannot change item type after creation without complex migration logic
  2. Repository Complexity: Requires separate repositories or complex queries to handle both types
  3. Query Limitations: Difficult to query "all items" without union queries or discriminators
  4. Business Reality: Items often start as non-stockable and later become stockable (or vice versa)
  5. Feature Bloat: Future behaviors (serializable, lot-trackable, etc.) would explode the inheritance tree
  6. Aggregate Boundary Confusion: Different subtypes have different invariants, complicating domain logic

3.2. Composition Approach (Accepted)

public class Item : AggregateRoot
{
public string ItemNumber { get; private set; }
public string Name { get; private set; }
public Guid CategoryId { get; private set; }

// Optional composition of stockable behavior
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 = StockableBehavior.Create(Id, allowNegativeStock, trackByLocation);
AddDomainEvent(new ItemMadeStockableEvent(Id));
}

public void RemoveStockableBehavior()
{
if (StockableBehavior == null)
throw new DomainException("Item is not stockable");

// Business rule: Cannot remove if inventory exists
if (/* has inventory */)
throw new DomainException("Cannot make item non-stockable while inventory exists");

StockableBehavior = null;
AddDomainEvent(new ItemMadeNonStockableEvent(Id));
}
}

public class StockableBehavior : ValueObject
{
public Guid ItemId { get; private set; }
public bool AllowNegativeStock { get; private set; }
public bool TrackByLocation { get; private set; }

private StockableBehavior() { }

public static StockableBehavior Create(Guid itemId, bool allowNegativeStock, bool trackByLocation)
{
return new StockableBehavior
{
ItemId = itemId,
AllowNegativeStock = allowNegativeStock,
TrackByLocation = trackByLocation
};
}

protected override IEnumerable<object> GetEqualityComponents()
{
yield return AllowNegativeStock;
yield return TrackByLocation;
}
}

Arguments For Composition (Why We Accepted It):

  1. Flexibility: Items can transition between stockable and non-stockable states through domain methods
  2. Single Repository: One repository handles all items, simplifying data access
  3. Query Simplicity: Easy to query all items, filter by stockability: items.Where(i => i.IsStockable)
  4. Business Reality Alignment: Mirrors real-world scenarios where items change nature over time
  5. Extensibility: New behaviors can be added compositionally without affecting existing structure
  6. Clear Boundaries: The StockableBehavior value object has well-defined responsibilities
  7. Persistence Efficiency: EF Core handles this naturally with owned entities (no discriminators or table-per-type)

4. The Accepted Solution: Composition Architecture

4.1. Core Design Principles

Principle 1: Behavior as Optional Composition

  • Core Item entity contains universally-applicable properties (number, name, category)
  • Stockable-specific concerns encapsulated in StockableBehavior value object
  • Nullable composition (StockableBehavior?) explicitly represents optional nature

Principle 2: Explicit State Transitions

  • Items created with or without stockable behavior via factory methods
  • Behavior added through AddStockableBehavior() method
  • Behavior removed through RemoveStockableBehavior() with business rule validation
  • Domain events raised for state transitions

Principle 3: Value Object for Behavior

  • StockableBehavior is an immutable value object
  • Equality based on properties, not identity
  • To change stockable settings, remove and re-add behavior with new values

Principle 4: Repository Transparency

  • Single IRepository<Item> handles all items
  • Queries can filter by presence of behavior: IsStockable computed property
  • EF Core owned entity configuration for StockableBehavior

4.2. Architectural Diagram

4.3. Business Rules Enforced by Composition

Rule 1: Only Stockable Items Have Inventory

// In InventoryRecord creation
if (!item.IsStockable)
throw new DomainException("Cannot create inventory record for non-stockable item");

Rule 2: Only Stockable Items in Inventory Transactions

// In Transaction validation
foreach (var line in transaction.Lines)
{
if (!line.Item.IsStockable)
throw new DomainException($"Item {line.ItemNumber} is not stockable");
}

Rule 3: Cannot Remove Stockable Behavior While Inventory Exists

public void RemoveStockableBehavior(IInventoryRecordRepository inventoryRepo)
{
if (StockableBehavior == null)
throw new DomainException("Item is not stockable");

var hasInventory = inventoryRepo.HasInventory(Id);
if (hasInventory)
throw new DomainException("Cannot make item non-stockable while inventory exists");

StockableBehavior = null;
AddDomainEvent(new ItemMadeNonStockableEvent(Id));
}

5. Implementation Guidance

5.1. Entity Framework Core Configuration

public class ItemConfiguration : IEntityTypeConfiguration<Item>
{
public void Configure(EntityTypeBuilder<Item> builder)
{
builder.ToTable("items");

builder.HasKey(i => i.Id);

builder.Property(i => i.ItemNumber)
.HasMaxLength(50)
.IsRequired();

// Owned entity for StockableBehavior
builder.OwnsOne(i => i.StockableBehavior, sb =>
{
sb.Property(b => b.AllowNegativeStock)
.HasColumnName("allow_negative_stock");

sb.Property(b => b.TrackByLocation)
.HasColumnName("track_by_location");
});

// Computed property helper
builder.Ignore(i => i.IsStockable);
}
}

Database Schema:

CREATE TABLE items (
id UUID PRIMARY KEY,
item_number VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
category_id UUID NOT NULL,
unit_class_id UUID NOT NULL,
allow_negative_stock BOOLEAN NULL, -- NULL indicates non-stockable
track_by_location BOOLEAN NULL, -- NULL indicates non-stockable
is_active BOOLEAN NOT NULL DEFAULT true,
created_date TIMESTAMP NOT NULL,
modified_date TIMESTAMP NOT NULL
);

5.2. Query Patterns

All Stockable Items:

var stockableItems = await _repository
.GetAllAsync()
.Where(i => i.StockableBehavior != null)
.ToListAsync();

Items Allowing Negative Stock:

var items = await _repository
.GetAllAsync()
.Where(i => i.StockableBehavior != null && i.StockableBehavior.AllowNegativeStock)
.ToListAsync();

Check If Item Is Stockable Before Transaction:

var item = await _repository.GetByIdAsync(itemId);
if (!item.IsStockable)
return Result.Failure("Item is not stockable and cannot be used in inventory transactions");

5.3. API Layer DTOs

public class ItemDto
{
public Guid Id { get; set; }
public string ItemNumber { get; set; }
public string Name { get; set; }
public Guid CategoryId { get; set; }
public bool IsStockable { get; set; }
public StockableBehaviorDto? StockableBehavior { get; set; }
}

public class StockableBehaviorDto
{
public bool AllowNegativeStock { get; set; }
public bool TrackByLocation { get; set; }
}

public class AddStockableBehaviorCommand
{
public Guid ItemId { get; set; }
public bool AllowNegativeStock { get; set; }
public bool TrackByLocation { get; set; }
}

6. Benefits and Trade-offs

6.1. Benefits

Flexibility:

  • Items can change stockability status as business needs evolve
  • No data migration required to change item types
  • Supports gradual rollout (items can start non-stockable, become stockable later)

Simplicity:

  • Single repository and query interface
  • No discriminator columns or complex inheritance mapping
  • Straightforward null checks: if (item.StockableBehavior != null)

Extensibility:

  • Pattern established for future behaviors (SerializableBehavior, LotTrackableBehavior, etc.)
  • Behaviors can be added compositionally without touching core Item entity
  • Multiple behaviors can coexist on same item

Domain Clarity:

  • Explicit methods for state changes with business rule enforcement
  • Domain events communicate important lifecycle changes
  • Value objects isolate concerns with clear boundaries

6.2. Trade-offs

Null Checks Required:

  • Code must check StockableBehavior != null before accessing properties
  • Mitigation: Encapsulate checks in IsStockable property and validation methods

No Compile-Time Guarantees:

  • Can't rely on type system to ensure stockable-specific properties exist
  • Mitigation: Business rules enforced at domain level, comprehensive tests verify behavior

Cognitive Load:

  • Developers must understand composition pattern vs. traditional inheritance
  • Mitigation: Clear documentation, code examples, team training

7. Future Enhancements

This composition pattern establishes a foundation for additional behaviors:

Serializable Behavior (Future):

public SerializableBehavior? SerializableBehavior { get; private set; }
public bool IsSerializable => SerializableBehavior != null;

Lot-Trackable Behavior (Future):

public LotTrackableBehavior? LotTrackableBehavior { get; private set; }
public bool IsLotTracked => LotTrackableBehavior != null;

Expirable Behavior (Future):

public ExpirableBehavior? ExpirableBehavior { get; private set; }
public bool RequiresExpirationTracking => ExpirableBehavior != null;

Each behavior added compositionally without modifying core Item entity, maintaining backwards compatibility.

8. Final Decision

The Composition Over Inheritance pattern for item stockability provides the optimal balance of flexibility, simplicity, and domain clarity for the Inventory Management Module. It aligns with modern DDD practices, supports evolving business requirements, and establishes a scalable pattern for future behavioral extensions.

The team is directed to implement item stockability using composition as specified in this document. All new behavioral concerns should follow this established pattern unless explicitly justified otherwise.

Status: Accepted Implementation Start: October 2024 Review Date: April 2025 (6-month retrospective)