Package-First Architecture: Why Order Status Should Be Derived, Not Assigned
Package-First Architecture: Why Order Status Should Be Derived, Not Assigned
Most order management systems track order status directly. Someone clicks “shipped” and the order is marked shipped. Simple, right?
Until you have an order with three packages. Two shipped yesterday, one ships today. Is the order “shipped”? “Partially shipped”? When did it ship?
This is why Vectis uses package-first architecture. Order status is derived from package status, not assigned directly.
The Traditional Approach (And Its Problems)
In a typical OMS:
- Order comes in → status:
pending - Warehouse picks items → someone clicks a button → status:
picking - Items packed → someone clicks a button → status:
packed - Label printed → someone clicks a button → status:
shipped
The order status is a single field that gets updated manually or via simple triggers. This works fine for simple orders but breaks down with:
Multi-package orders: Customer orders 10 items. You pack them in 3 boxes. When is the order “packed”? When the first box is packed? The last? What if box 2 is packed but box 1 isn’t?
Split shipments: Items come from different warehouses. Phoenix ships Monday, Atlanta ships Tuesday. The order isn’t really “shipped” until both are out the door.
Partial fulfillment: 8 items in stock, 2 backordered. You ship what you have. Is the order “shipped” or “partially shipped”? What happens when the backordered items arrive?
Repackaging: Packer realizes the box is wrong, needs to repack. In a traditional system, you’re fighting the status model.
The Package-First Approach
In Vectis, packages are first-class entities with their own lifecycle:
pending → allocated → sent_to_wms → picking → picked →
packing → packed → shipped → in_transit → out_for_delivery → delivered
Plus exception states: failed_delivery, returned, cancelled
Each package tracks its own status independently. The order status is then computed from its packages:
- All packages
pending? Order ispending - Any package
picking? Order ispicking - All packages
shipped? Order isshipped - Some shipped, some not? Order is
partially_shipped - All packages
delivered? Order isdelivered
Why This Matters
Accuracy: The order status always reflects reality. You can’t have an order marked “shipped” when a package is still sitting in the warehouse.
Auditability: Every status change is tied to a specific package. “Order moved to shipped because package PKG-789 was scanned at FedEx.”
Flexibility: Need to repack? Change the package status. Need to split a package? Create two packages, each with their own lifecycle. The order status updates automatically.
Multi-warehouse: Each warehouse ships its packages independently. The order status reflects the aggregate state without manual coordination.
Implementation Details
In Vectis, the order status computation looks roughly like this:
function deriveOrderStatus(packages: Package[]): OrderStatus {
if (packages.every(p => p.status === 'delivered')) return 'delivered';
if (packages.every(p => p.status === 'shipped' || p.status === 'delivered')) return 'shipped';
if (packages.some(p => ['shipped', 'in_transit', 'delivered'].includes(p.status))) return 'partially_shipped';
if (packages.every(p => p.status === 'packed')) return 'packed';
// ... and so on
}
This runs on every package status change, keeping the order status in sync automatically.
The Tradeoff
Package-first architecture adds complexity. You’re managing more entities, more status transitions, more edge cases. For simple operations (single warehouse, single package per order), it’s overkill.
But if you’re doing any of the following, the complexity pays for itself:
- Multi-package orders
- Split shipments across warehouses
- Partial fulfillment with backorders
- High-volume operations where manual status updates don’t scale
Conclusion
Order status should describe reality, not intent. When you derive status from packages, you get accurate tracking that handles real-world complexity without manual intervention.
Vectis uses package-first architecture with a 14-state package lifecycle. Learn more about how it works.