Skip to main content

Composition Over Inheritance Pattern

Overview

The Inventory Management System uses a composition-based architecture for items instead of the traditional class inheritance approach. This fundamental design decision provides maximum flexibility for managing diverse item types without rigid type hierarchies.

Business Context

Traditional inventory systems often use inheritance hierarchies like:

Item (base class)
├── StockableItem (has inventory tracking)
│ ├── RawMaterialItem
│ ├── FinishedGoodItem
│ └── ComponentItem
└── NonStockableItem (no inventory tracking)
├── ServiceItem
└── LaborItem

Problems with Inheritance:

  • Items cannot transition between types (service → stockable)
  • Rigid classification doesn't match business reality
  • Adding new item types requires code changes
  • Complex hierarchies become difficult to maintain

The Composition Solution

Instead of inheritance, we use composition with optional behaviors:

Item (single class)
└── Optional: StockableBehavior (composition)

Key Principle: Items are defined by their behaviors, not their types.

How It Works

Creating Non-Stockable Item (Service)

// Create service item - NO stockable behavior
var serviceItem = Item.Create(
itemNumber: "SRV-INSTALL",
name: "Installation Service");

// Result:
// - Item exists in catalog
// - Can be used for billing
// - NO inventory tracking
// - Cannot be used in stock transactions

Creating Stockable Item

// Create item
var item = Item.Create(
itemNumber: "WIDGET-001",
name: "Premium Widget");

// Add stockable behavior
item.AddStockableBehavior(
allowNegativeStock: false,
trackByLocation: true);

// Result:
// - Item exists in catalog
// - HAS inventory tracking
// - Can be used in stock transactions
// - Requires location specification
// - Prevents negative stock

Converting Between Types

// Start as non-stockable (service)
var item = Item.Create("ITEM-001", "Sample Item");

// Business decision: Now we need to track inventory
item.AddStockableBehavior(
allowNegativeStock: false,
trackByLocation: true);

// Item is now stockable!

// Later, convert back to non-stockable
item.RemoveStockableBehavior();

// Item is now non-stockable again!

Technical Implementation

Item Entity Structure

public class Item : AggregateRoot
{
// Core properties (always present)
public string ItemNumber { get; private set; }
public string Name { get; private set; }
public string Description { get; private set; }

// Optional behavior (composition)
public StockableBehavior? StockableBehavior { get; private set; }

// Computed property
public bool IsStockable => StockableBehavior != null;

// Behavior management
public void AddStockableBehavior(bool allowNegativeStock, bool trackByLocation)
{
if (StockableBehavior != null)
throw new InvalidOperationException("Already stockable");

StockableBehavior = StockableBehavior.Create(
Id, allowNegativeStock, trackByLocation);
}

public void RemoveStockableBehavior()
{
if (StockableBehavior == null)
throw new InvalidOperationException("Not stockable");

// Validate no inventory exists
StockableBehavior = null;
}
}

StockableBehavior Entity

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

public void UpdateInventoryRules(
bool allowNegativeStock,
bool trackByLocation)
{
AllowNegativeStock = allowNegativeStock;
TrackByLocation = trackByLocation;
}
}

Process Flows

Item Lifecycle with Composition

Behavior Addition Flow

Business Benefits

Flexibility

Traditional Inheritance:

Problem: Need to track inventory for a service item
Solution: Create new item, migrate data, update references
Risk: Data integrity issues, reference breakage

Composition:

Problem: Need to track inventory for a service item
Solution: Add stockable behavior to existing item
Risk: Minimal - controlled by domain logic

No Type Explosion

Traditional Inheritance:

  • StockableItem
  • NonStockableItem
  • SerializedStockableItem
  • LotControlledStockableItem
  • BackorderableStockableItem
  • ... (15+ types)

Composition:

  • Item (one class)
  • StockableBehavior (one behavior)
  • Future: SerialBehavior, LotBehavior (additional optional behaviors)

Business Rule Flexibility

Different items can have different inventory rules:

// High-value items: Strict control
item.AddStockableBehavior(
allowNegativeStock: false, // No backorders
trackByLocation: true); // Track where it is

// Fast-moving items: Flexible control
item.AddStockableBehavior(
allowNegativeStock: true, // Allow backorders
trackByLocation: false); // Simplified tracking

// Services: No inventory
// No behavior added

Comparison with Inheritance

Inheritance Approach

// Cannot change types after creation
public abstract class Item { }

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

public class NonStockableItem : Item
{
// No inventory properties
}

// Creating items
Item rawMaterial = new StockableItem { /* ... */ };
Item service = new NonStockableItem { /* ... */ };

// Problem: Cannot convert service to stockable!
// Must create new item and migrate data

Composition Approach

// Single class, behavior added dynamically
public class Item
{
public StockableBehavior? StockableBehavior { get; set; }
}

// Creating items
var rawMaterial = Item.Create("RM-001", "Raw Material");
rawMaterial.AddStockableBehavior(false, true);

var service = Item.Create("SRV-001", "Service");
// No behavior added

// Solution: Easy conversion!
service.AddStockableBehavior(false, true);
// Service is now stockable!

Database Design

Table Structure

-- Items table (all items)
CREATE TABLE items (
id UUID PRIMARY KEY,
item_number VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
description TEXT,
category_id UUID,
unit_class_id UUID,
is_active BOOLEAN DEFAULT TRUE,
created_date TIMESTAMP NOT NULL,
modified_date TIMESTAMP NOT NULL
);

-- Stockable behavior table (1:1 optional)
CREATE TABLE stockable_behaviors (
id UUID PRIMARY KEY,
item_id UUID NOT NULL UNIQUE REFERENCES items(id),
allow_negative_stock BOOLEAN NOT NULL,
track_by_location BOOLEAN NOT NULL,
created_date TIMESTAMP NOT NULL,
modified_date TIMESTAMP NOT NULL
);

-- Queries
-- All items: SELECT * FROM items
-- Stockable only: SELECT * FROM items WHERE EXISTS (SELECT 1 FROM stockable_behaviors WHERE item_id = items.id)
-- Non-stockable only: SELECT * FROM items WHERE NOT EXISTS (SELECT 1 FROM stockable_behaviors WHERE item_id = items.id)

API Design

RESTful Endpoint

# Create stockable item
POST /api/items
{
"itemNumber": "WIDGET-001",
"name": "Premium Widget",
"allowNegativeStock": false, # Creates stockable behavior
"trackByLocation": true
}

# Create non-stockable item
POST /api/items
{
"itemNumber": "SRV-001",
"name": "Service Item"
# No stockable properties = non-stockable
}

# Convert to stockable
PATCH /api/items/{id}
{
"allowNegativeStock": true,
"trackByLocation": false
}

# Convert to non-stockable
PATCH /api/items/{id}
{
"allowNegativeStock": null,
"trackByLocation": null
}

Real-World Scenarios

Scenario 1: Service to Product

Business Situation:

"We used to sell installation as a service. Now we're packaging it as a product kit that needs inventory tracking."

Inheritance Approach:

1. Create new StockableItem
2. Migrate all data from ServiceItem
3. Update all references (BOMs, transactions, etc.)
4. Archive old ServiceItem
5. Risk: Broken references, data loss

Composition Approach:

// 1. Get existing service item
var item = await itemRepo.GetByNumberAsync("SRV-INSTALL");

// 2. Add stockable behavior
item.AddStockableBehavior(
allowNegativeStock: false,
trackByLocation: true);

// 3. Save
await unitOfWork.CommitAsync();

// Done! All references remain intact

Scenario 2: Different Inventory Rules

Business Situation:

"We have 1000 items. Some allow backorders, some don't. Some need location tracking, some don't. Rules vary by item."

Inheritance Approach:

Problem: Need 4 different classes
- StockableWithLocationAndBackorder
- StockableWithLocationNoBackorder
- StockableNoLocationWithBackorder
- StockableNoLocationNoBackorder

Explosion of types!

Composition Approach:

// Each item has independent settings
item1.AddStockableBehavior(allowNegativeStock: true, trackByLocation: true);
item2.AddStockableBehavior(allowNegativeStock: true, trackByLocation: false);
item3.AddStockableBehavior(allowNegativeStock: false, trackByLocation: true);
item4.AddStockableBehavior(allowNegativeStock: false, trackByLocation: false);

// One class, four configurations!

Future Extensibility

Adding New Behaviors

// Future behaviors via composition
public class Item
{
public StockableBehavior? StockableBehavior { get; set; }
public SerialBehavior? SerialBehavior { get; set; } // Future
public LotBehavior? LotBehavior { get; set; } // Future
public ExpirationBehavior? ExpirationBehavior { get; set; }// Future
}

// Flexible combinations
item.AddStockableBehavior(...);
item.AddSerialBehavior(...); // Stockable + Serialized
item.AddExpirationBehavior(...); // Stockable + Expiration tracking

Key Takeaways

  1. Composition over Inheritance provides flexibility without complexity
  2. Items can transition between stockable and non-stockable
  3. No type explosion - single Item class with optional behaviors
  4. Independent configuration - each item has its own rules
  5. Easy to extend - new behaviors added without changing Item class
  6. Database efficient - optional 1:1 relationship
  7. API friendly - simple null handling for optional behaviors

Last Updated: 2025-10-24 | Status: Active | Version: 1.0