Order Hierarchy
Parent-child order relationships with full lineage tracking, composition metadata, and optimized descendant queries.
Order Hierarchy Features
Complete hierarchical order management:
- Unlimited Depth - Support complex multi-level workflows (configurable limits)
- Relationship Validation - Enforce valid parent-child type combinations
- Lineage Tracking - Maintain root order reference throughout hierarchy
- Composition Metadata - Track why, when, and how children were created
- Denormalized Descendants - Fast hierarchy queries without recursion
- Blocking Control - Children can optionally block or allow parent completion
Use Cases: Multi-stage fulfillment, complex workflows, task decomposition, cross-dock operations
What is Order Hierarchy?
In logistics operations, orders rarely exist in isolation. A single customer order might trigger dozens of internal operations - picking items from shelves, packing boxes, printing labels, arranging transport. The order hierarchy system manages these complex parent-child relationships while maintaining consistency and enforcing business rules.
Why Hierarchies Matter
Traditional Problem: Many systems treat orders as flat, independent entities. This creates challenges:
- ❌ No visibility into task dependencies
- ❌ Manual coordination of related orders
- ❌ Difficult to track "what caused what"
- ❌ Slow queries to find all related orders
- ❌ No automatic cleanup on failures
De. Solution: Hierarchical order management with:
- ✅ Explicit parent-child relationships
- ✅ Automatic coordination via policies
- ✅ Full composition metadata
- ✅ Fast descendant queries
- ✅ Automatic compensation on failures
Hierarchy Structure
Core Concepts
Root Order The top-level order that initiated the workflow. Could be a customer order, inbound shipment, or cross-dock operation.
Parent Order Any order that spawns child orders. Can be the root or intermediate in a multi-level hierarchy.
Child Order An order created from a parent. Represents a step or task within the parent's workflow.
Descendants All orders below a given order in the hierarchy (children, grandchildren, etc.).
Example Hierarchy
// E-commerce fulfillment workflow
OutboundOrder (OUT-001) [ROOT]
├── InternalOrder (PICK-001) [CHILD of OUT-001]
│ └── InternalOrder (AUDIT-001) [GRANDCHILD - optional QA]
├── InternalOrder (PACK-001) [CHILD of OUT-001]
│ └── InternalOrder (LABEL-001) [GRANDCHILD - auto-created]
└── ShippingOrder (SHIP-001) [CHILD of OUT-001]
└── ShippingOrder (RETURN-001) [GRANDCHILD - if delivery failed]
// Hierarchy metadata for OUT-001:
{
isRoot: true,
depth: 0,
childOrders: [PICK-001, PACK-001, SHIP-001],
allDescendantReferences: [PICK-001, AUDIT-001, PACK-001, LABEL-001, SHIP-001, RETURN-001],
descendantCount: 6,
descendantsByType: {
InternalOrder: [PICK-001, AUDIT-001, PACK-001, LABEL-001],
ShippingOrder: [SHIP-001, RETURN-001]
}
}Hierarchy Schema
Base Hierarchy Fields
Every order contains a hierarchy object:
{
// Root tracking
isRoot: boolean, // Is this the top-level order?
rootOrderId: string, // ID of root order
rootOrderReference: string, // Reference of root order
rootOrderType: string, // Type of root order
// Parent tracking
parentOrderId?: string, // ID of direct parent
parentOrderReference?: string, // Reference of direct parent
parentOrderType?: string, // Type of direct parent
// Depth
depth: number, // Level in hierarchy (root = 0)
// Children
childOrders?: [
{
orderId: string,
orderReference: string,
orderType: string,
lspOrderType: string,
processor: Context,
status: string,
// WHO/WHAT/WHEN created this child
created: {
by: { type, id, name },
at: { timestamp, timezone }
},
// WHY was this child created?
creationReason: 'LIFECYCLE_REQUIREMENT' | 'PIPELINE_STAGE' |
'AUTOMATIC_RULE' | 'CONDITIONAL_TRIGGER' |
'EXCEPTION_HANDLING' | 'MANUAL_REQUEST' |
'COMPENSATION' | 'SPLIT_ORDER',
// Parent state when child was created
parentStateAtCreation: {
process: string,
stage: string,
status: string
},
// Relationship metadata
relationship: {
type: 'MANDATORY' | 'OPTIONAL' | 'CONDITIONAL',
canBlockParent: boolean, // Can this child block parent completion?
sequence?: number, // Order of execution
dependencies?: string[] // Other children this depends on
},
// Additional metadata
metadata?: {
trigger?: string, // What triggered creation
notes?: string,
tags?: string[]
}
}
],
// Denormalized descendants (Flaw #9 fix)
allDescendantReferences?: string[], // All descendants (fast queries)
descendantCount?: number, // Total count
descendantsByType?: Record<string, string[]> // Grouped by type
}Relationship Validation
Valid Parent-Child Combinations
Not all order types can spawn all others. The system enforces valid relationships:
| Parent Type | Allowed Children |
|---|---|
OutboundOrder | InternalOrder, ShippingOrder |
InboundOrder | InternalOrder, PassthroughOrder |
InternalOrder | InternalOrder (sub-tasks) |
ShippingOrder | InternalOrder (exceptions), ShippingOrder (returns) |
PassthroughOrder | SortingOrder, ShippingOrder |
SortingOrder | ShippingOrder, CrossDockOrder |
Example:
// ✅ Valid
OutboundOrder → InternalOrder (picking task)
// ❌ Invalid
InternalOrder → OutboundOrder (tasks can't create customer orders)Depth Limits
Hierarchies have configurable maximum depth to prevent infinite recursion:
// Default: 5 levels
ROOT (0) → CHILD (1) → GRANDCHILD (2) → ... → MAX_DEPTH (5)
// Exceeding depth throws error
if (childDepth > MAX_DEPTH) {
return { error: 'Maximum hierarchy depth exceeded' }
}Composition Metadata
Why Track Composition?
Understanding why and how child orders were created is critical for:
- Debugging - "Why was this picking task created?"
- Analytics - "What percentage of tasks are pipeline vs manual?"
- Auditing - "Who created this exception order?"
- Optimization - "Which triggers create the most children?"
Creation Reasons
creationReason:
| 'LIFECYCLE_REQUIREMENT' // Business rule: outbound must have picking
| 'PIPELINE_STAGE' // Pipeline auto-created at stage X
| 'AUTOMATIC_RULE' // System rule triggered creation
| 'CONDITIONAL_TRIGGER' // Condition met (e.g., item requires QA)
| 'EXCEPTION_HANDLING' // Created to handle exception
| 'MANUAL_REQUEST' // Operator manually created
| 'COMPENSATION' // Created as part of rollback
| 'SPLIT_ORDER' // Parent split into multipleParent State Context
Capture what the parent was doing when child was created:
parentStateAtCreation: {
process: 'FULFILLMENT',
stage: 'PICKING',
status: 'IN_PROGRESS'
}
// Useful for analysis:
// "Exception orders are mostly created during PACKING stage"
// "Returns happen most often after DELIVERY stage"Created By
Track who/what created the child:
created: {
by: {
type: 'PIPELINE', // Created by automation
id: 'pipe-outbound-v2',
name: 'Standard Outbound Pipeline'
},
at: { timestamp: 1699123456789, timezone: 'UTC' }
}
// Or manual creation:
created: {
by: {
type: 'USER',
id: 'OPR-123',
name: 'John Doe'
},
at: { timestamp: 1699123456789, timezone: 'America/New_York' }
}Relationship Types
Mandatory Children
Children that must complete before parent can complete:
relationship: {
type: 'MANDATORY',
canBlockParent: true
}
// Parent completion blocked if child not complete
OutboundOrder cannot complete until:
✅ PICK-001 is COMPLETED
✅ PACK-001 is COMPLETED
✅ SHIP-001 is COMPLETEDOptional Children
Children that don't block parent completion:
relationship: {
type: 'OPTIONAL',
canBlockParent: false
}
// Parent can complete even if optional child pending
OutboundOrder can complete even if:
⏸️ AUDIT-001 is still PENDING (quality check)
⏸️ PHOTO-001 is still PENDING (documentation)Use Cases:
- Quality audits (nice-to-have, not required)
- Documentation tasks
- Analytics/reporting tasks
- Optional gift wrapping
Conditional Children
Children whose blocking status depends on runtime conditions:
relationship: {
type: 'CONDITIONAL',
canBlockParent: item.requiresQA ? true : false
}
// Blocking status determined at creation time
// If item requires QA → child blocks parent
// If item doesn't require QA → child is optionalSequence & Dependencies
Control execution order and dependencies:
childOrders: [
{
ref: 'PICK-001',
relationship: {
type: 'MANDATORY',
canBlockParent: true,
sequence: 1, // Execute first
dependencies: [] // No dependencies
}
},
{
ref: 'PACK-001',
relationship: {
type: 'MANDATORY',
canBlockParent: true,
sequence: 2, // Execute second
dependencies: ['PICK-001'] // Wait for picking
}
},
{
ref: 'SHIP-001',
relationship: {
type: 'MANDATORY',
canBlockParent: true,
sequence: 3, // Execute third
dependencies: ['PACK-001'] // Wait for packing
}
}
]Denormalized Descendants
The Problem
Traditional hierarchy queries require recursion:
// ❌ Slow: Find all descendants recursively
async function getAllDescendants(orderId) {
const children = await db.find({ parentOrderId: orderId })
const descendants = [...children]
for (const child of children) {
const grandchildren = await getAllDescendants(child.id) // Recursive!
descendants.push(...grandchildren)
}
return descendants
}
// For 3-level hierarchy with 10 orders = 10+ database queriesThe Solution
Maintain denormalized lists updated on child creation/removal:
// ✅ Fast: Single query
const descendants = order.hierarchy.allDescendantReferences
// Returns: ['PICK-001', 'PACK-001', 'LABEL-001', 'SHIP-001', 'RETURN-001']
// Bulk fetch all descendants
const orders = await db.find({ reference: { $in: descendants } })
// 2 queries total instead of 10+Performance Comparison
| Operation | Before (Recursive) | After (Denormalized) | Speedup |
|---|---|---|---|
| Get all descendants | 7 queries (3 levels) | 1 query | 7x |
| Bulk fetch descendants | 7 queries + N fetches | 2 queries | 5x |
| Count descendants | Full traversal | Read single field | 50x+ |
| Filter by type | Query + filter | Read pre-grouped | 10x |
How It Works
On child creation:
// Update all ancestors with new descendant
await updateAncestorsDescendants(
db, context, parentOrder,
'ADD',
childReference,
childType
)
// Updates:
// - parent.allDescendantReferences += childReference
// - grandparent.allDescendantReferences += childReference
// - root.allDescendantReferences += childReference
// - All get descendantCount incremented
// - All get descendantsByType[childType] += childReferenceOn child removal (compensation):
await updateAncestorsDescendants(
db, context, parentOrder,
'REMOVE',
childReference,
childType
)Creating Child Orders
Basic Creation
import { createChildOrder } from '#lib/order/helpers'
const result = await createChildOrder({
// Parent info
parentOrder: outboundOrder,
parentOrderType: 'OutboundOrder',
// Child info
childOrderType: 'InternalOrder',
childOrderId: 'task-123',
childOrderReference: 'PICK-001',
lspOrderType: 'InternalOrder',
processor: context,
// Composition metadata
creationReason: 'PIPELINE_STAGE',
created: {
by: { type: 'PIPELINE', id: 'pipe-001', name: 'Outbound Pipeline' },
at: { timestamp: Date.now(), timezone: 'UTC' }
},
relationship: {
type: 'MANDATORY',
canBlockParent: true,
sequence: 1
},
metadata: {
trigger: 'allocation_complete',
notes: 'Auto-created picking task'
}
}, db, context)
// Returns:
{
hierarchy: { /* child hierarchy object */ },
tracking: { /* hierarchical tracking numbers */ },
error: undefined
}What Happens Automatically
- Validates relationship - Checks if parent type can have this child type
- Validates depth - Ensures hierarchy doesn't exceed max depth
- Generates tracking - Creates hierarchical tracking number
- Builds hierarchy - Sets up child hierarchy object with lineage
- Updates parent - Adds child to parent's childOrders array
- Updates ancestors - Adds child to all ancestors' descendant lists
- Returns result - Hierarchy and tracking for the new child
Error Handling
const result = await createChildOrder(params, db, context)
if (result.error) {
// Invalid relationship, depth exceeded, etc.
console.error(result.error)
// "Invalid order relationship: InternalOrder cannot spawn OutboundOrder"
// "Maximum order hierarchy depth exceeded. Current depth: 6"
}Querying Hierarchies
Get All Descendants
import { getAllDescendants } from '#lib/order/descendants'
// Fast single query
const descendantRefs = await getAllDescendants(db, context, 'OUT-001')
// ['PICK-001', 'PACK-001', 'LABEL-001', 'SHIP-001']Get Descendant Orders
import { getAllDescendantOrders } from '#lib/order/descendants'
// Bulk fetch all descendant orders
const descendants = await getAllDescendantOrders(db, context, 'OUT-001')
// Returns full order objects for all descendantsFilter by Type
import { getDescendantsByType } from '#lib/order/descendants'
// Get only InternalOrders
const taskRefs = await getDescendantsByType(db, context, 'OUT-001', 'InternalOrder')
// ['PICK-001', 'PACK-001', 'LABEL-001']Direct Access
// From order object (fastest - no query)
const order = await db.findOne({ reference: 'OUT-001' })
// All descendants
const all = order.hierarchy.allDescendantReferences
// Count
const count = order.hierarchy.descendantCount
// By type
const tasks = order.hierarchy.descendantsByType.InternalOrder
const shipping = order.hierarchy.descendantsByType.ShippingOrderBest Practices
Design Hierarchies Carefully
✅ Good Hierarchy:
OutboundOrder
├── InternalOrder (Pick) // Single picking task
├── InternalOrder (Pack) // Single packing task
└── ShippingOrder // Single shipment
// Total: 4 orders, 2 levels❌ Poor Hierarchy:
OutboundOrder
├── InternalOrder (Pick item 1)
├── InternalOrder (Pick item 2)
├── InternalOrder (Pick item 3)
// ... 100 items = 100 picking tasks
└── ShippingOrder
// Total: 102 orders, massive overheadBetter Approach:
OutboundOrder
├── InternalOrder (Pick all items) // Single task, multiple items
│ items: [item1, item2, item3, ...]
└── ShippingOrder
// Total: 3 orders, efficientUse Relationship Types Wisely
Mandatory for critical paths:
// Parent cannot complete without these
relationship: { type: 'MANDATORY', canBlockParent: true }Optional for nice-to-haves:
// Parent can complete even if pending
relationship: { type: 'OPTIONAL', canBlockParent: false }Set Meaningful Metadata
❌ Generic:
metadata: { notes: 'Task created' }✅ Specific:
metadata: {
trigger: 'inventory_allocated',
notes: 'Auto-created after allocation for SKU-12345',
tags: ['auto-created', 'high-priority']
}Leverage Denormalized Queries
❌ Don't query recursively:
// Slow - multiple queries
async function getAll(id) {
const children = await db.find({ parentOrderId: id })
for (const child of children) {
await getAll(child.id) // Recursion!
}
}✅ Use denormalized lists:
// Fast - single query
const descendants = await getAllDescendants(db, context, orderId)Use Cases
Multi-Stage Fulfillment
OutboundOrder (Customer order)
├── InternalOrder (Allocate inventory)
├── InternalOrder (Pick items)
│ └── InternalOrder (Quality check) [OPTIONAL]
├── InternalOrder (Pack box)
│ └── InternalOrder (Gift wrap) [OPTIONAL]
├── InternalOrder (Print label)
└── ShippingOrder (Last-mile delivery)
└── ShippingOrder (Return) [Created if delivery fails]Cross-Dock Operations
InboundOrder (Receiving at hub)
└── PassthroughOrder (No storage)
├── SortingOrder (Sort by destination)
└── ShippingOrder (Route to next hub)
└── ShippingOrder (Final delivery)Exception Handling
OutboundOrder (Normal flow)
├── InternalOrder (Pick)
│ └── InternalOrder (Exception - damaged item found)
│ └── InternalOrder (Re-pick replacement item)
├── InternalOrder (Pack)
└── ShippingOrderNext Steps
Related Documentation:

