The Order Archetype
Based on "Enterprise Patterns and MDA" by Jim Arlow & Ila Neustadt
Problem
Orders are central to commerce but have complex requirements:
- Header + lines structure: Order metadata vs individual items
- Lifecycle management: Draft → Confirmed → Paid → Shipped → Completed
- Price calculations: Line totals, discounts, tax, shipping, grand total
- Modifications: Adding/removing items, quantity changes, cancellations
- History: Who changed what and when
Concept
The Order pattern models a commercial transaction between a buyer and seller. An Order has:
- Header — Overall order information (customer, dates, totals, status)
- Lines — Individual items being ordered (product, quantity, price)
- Lifecycle — States the order passes through
Orders are typically immutable once confirmed — changes create new versions or adjustment records.
Structure
Attributes
Order
| Attribute | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Unique identifier |
| orderNumber | String | Yes | Human-readable order number |
| customer | Customer | Yes | Who placed the order |
| status | OrderStatus | Yes | Current order state |
| orderDate | Timestamp | Yes | When order was placed |
| confirmedAt | Timestamp | No | When order was confirmed |
| currency | String | Yes | ISO currency code (PLN, EUR) |
| billingAddress | Address | Yes | Invoice address |
| shippingAddress | Address | No | Delivery address (if different) |
| subtotal | Money | Yes | Sum of line totals |
| discountTotal | Money | No | Order-level discounts |
| taxTotal | Money | Yes | Total tax amount |
| shippingCost | Money | No | Delivery cost |
| grandTotal | Money | Yes | Final amount to pay |
| notes | String | No | Customer notes |
| internalNotes | String | No | Staff notes |
OrderLine
| Attribute | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Unique identifier |
| order | Order | Yes | Parent order |
| lineNumber | Integer | Yes | Line sequence (1, 2, 3...) |
| product | Product | Yes | What is being ordered |
| productName | String | Yes | Product name (snapshot) |
| productSku | String | Yes | SKU at time of order |
| quantity | Integer | Yes | How many |
| unitPrice | Money | Yes | Price per unit |
| discount | Money | No | Line-level discount |
| taxRate | Decimal | Yes | Tax percentage (e.g., 23.00) |
| taxAmount | Money | Yes | Calculated tax |
| lineTotal | Money | Yes | (unitPrice × quantity) - discount |
Behaviors
Entity Order:
function addLine(product: Product, quantity: Integer, unitPrice: Money): OrderLine
require status == OrderStatus.Draft
require quantity > 0
line = new OrderLine(
order: this,
lineNumber: lines.length + 1,
product: product,
productName: product.name, // Snapshot
productSku: product.sku,
quantity: quantity,
unitPrice: unitPrice,
taxRate: product.taxRate
)
lines.add(line)
recalculateTotals()
return line
function removeLine(line: OrderLine):
require status == OrderStatus.Draft
require line in lines
lines.remove(line)
renumberLines()
recalculateTotals()
function updateQuantity(line: OrderLine, newQuantity: Integer):
require status == OrderStatus.Draft
require newQuantity > 0
line.quantity = newQuantity
line.recalculate()
recalculateTotals()
function recalculateTotals():
subtotal = lines.sum(line => line.lineTotal)
taxTotal = lines.sum(line => line.taxAmount)
grandTotal = subtotal + taxTotal + shippingCost - discountTotal
function confirm():
require status == OrderStatus.Draft
require lines.isNotEmpty()
require validate().isValid
status = OrderStatus.Confirmed
confirmedAt = now()
emit OrderConfirmed(this)
function markPaid(payment: Payment):
require status == OrderStatus.Confirmed
require payment.amount >= grandTotal
status = OrderStatus.Paid
paidAt = now()
emit OrderPaid(this, payment)
function cancel(reason: String):
require status in [OrderStatus.Draft, OrderStatus.Confirmed]
previousStatus = status
status = OrderStatus.Cancelled
cancelledAt = now()
cancellationReason = reason
emit OrderCancelled(this, reason)
function validate(): ValidationResult
errors = []
if lines.isEmpty():
errors.add("Order must have at least one line")
if billingAddress is None:
errors.add("Billing address is required")
for line in lines:
if line.quantity <= 0:
errors.add("Line " + line.lineNumber + ": quantity must be positive")
if line.unitPrice < 0:
errors.add("Line " + line.lineNumber + ": price cannot be negative")
return ValidationResult(isValid: errors.isEmpty(), errors: errors)
function canBeModified(): Boolean
return status == OrderStatus.Draft
function canBeCancelled(): Boolean
return status in [OrderStatus.Draft, OrderStatus.Confirmed]
Entity OrderLine:
function getLineTotal(): Money
return (unitPrice * quantity) - discount
function getTaxAmount(): Money
return getLineTotal() * (taxRate / 100)
function getGrossTotal(): Money
return getLineTotal() + getTaxAmount()
function recalculate():
lineTotal = getLineTotal()
taxAmount = getTaxAmount()State Machine
Examples
Creating and confirming an order
jan = findCustomer("CUST-00001234")
thinkpad = findProduct("LEN-X1C-G11-16-512")
mouse = findProduct("LOG-MX-MASTER-3S")
order = new Order(
orderNumber: "ORD-2026-000042",
customer: jan,
currency: "PLN",
billingAddress: jan.party.getPrimaryAddress()
)
// Add lines (order is in Draft status)
order.addLine(thinkpad, quantity: 1, unitPrice: Money(649900, "PLN"))
order.addLine(mouse, quantity: 2, unitPrice: Money(44900, "PLN"))
order.subtotal // 739700 (6499.00 + 2 x 449.00)
order.grandTotal // 909633 (with 23% VAT)
// Confirm locks the lines
order.confirm()
// emits OrderConfirmed(order)
// order.status == Confirmed
// order.addLine(...) would now fail — require status == DraftCancelling an order
order.cancel("Customer changed their mind")
// emits OrderCancelled(order, "Customer changed their mind")
// order.status == CancelledOrder Number Generation
// Strategies for generating order numbers
// 1. Sequential with prefix
function generateOrderNumber(): String
year = currentYear()
sequence = getNextSequence("order", year)
return "ORD-" + year + "-" + padLeft(sequence, 6, '0')
// Example: ORD-2024-000123
// 2. Date-based
function generateOrderNumber(): String
date = today().format("YYYYMMDD")
sequence = getNextSequence("order", date)
return date + "-" + padLeft(sequence, 4, '0')
// Example: 20241215-0042
// 3. Random (for hiding volume)
function generateOrderNumber(): String
return "ORD-" + randomAlphanumeric(10).toUpperCase()
// Example: ORD-X7K9M2P4Q1Invariants
- Order number unique — No two orders share the same order number
- At least one line — Confirmed orders must have at least one line
- Positive quantities — Line quantities must be > 0
- Non-negative prices — Prices and totals cannot be negative
- Consistent currency — All monetary values in same currency
- Lines locked after confirm — Cannot modify lines after confirmation
- Status transitions valid — Only allowed state transitions
Error Handling
| Operation | Precondition Violated | Error |
|---|---|---|
addLine(product, qty, price) |
Order not in Draft | PreconditionError: cannot modify confirmed order |
addLine(product, qty, price) |
quantity <= 0 |
PreconditionError: quantity must be positive |
removeLine(line) |
Order not in Draft | PreconditionError: cannot modify confirmed order |
updateQuantity(line, qty) |
Order not in Draft | PreconditionError: cannot modify confirmed order |
confirm() |
No lines | PreconditionError: order must have at least one line |
confirm() |
Validation fails | PreconditionError: billing address is required |
markPaid(payment) |
Not Confirmed | PreconditionError: order must be confirmed before payment |
markPaid(payment) |
Payment < total | PreconditionError: payment amount insufficient |
cancel(reason) |
Already shipped/completed | PreconditionError: cannot cancel order in status Shipped |
Note: After confirmation, all modification attempts return a clear error directing the caller to cancel-and-reorder instead.
SQL Schema
sqlCREATE TABLE orders ( -- "order" is reserved word
id UUID PRIMARY KEY,
order_number VARCHAR(30) NOT NULL UNIQUE,
customer_id UUID NOT NULL REFERENCES party_role(id),
status VARCHAR(20) NOT NULL DEFAULT 'Draft',
order_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
confirmed_at TIMESTAMP,
currency CHAR(3) NOT NULL DEFAULT 'PLN',
-- Addresses (stored as JSON or separate table)
billing_address JSONB NOT NULL,
shipping_address JSONB,
-- Totals (stored in cents)
subtotal_cents INTEGER NOT NULL DEFAULT 0,
discount_total_cents INTEGER NOT NULL DEFAULT 0,
tax_total_cents INTEGER NOT NULL DEFAULT 0,
shipping_cost_cents INTEGER NOT NULL DEFAULT 0,
grand_total_cents INTEGER NOT NULL DEFAULT 0,
-- Notes
notes TEXT,
internal_notes TEXT,
-- Cancellation
cancelled_at TIMESTAMP,
cancellation_reason TEXT,
-- Audit
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_order_status CHECK (
status IN ('Draft', 'Confirmed', 'Paid', 'Processing', 'Shipped', 'Delivered', 'Completed', 'Cancelled')
)
);
CREATE TABLE order_line (
id UUID PRIMARY KEY,
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
line_number INTEGER NOT NULL,
product_id UUID REFERENCES product(id), -- Nullable if product deleted
product_name VARCHAR(255) NOT NULL, -- Snapshot
product_sku VARCHAR(50) NOT NULL, -- Snapshot
quantity INTEGER NOT NULL,
unit_price_cents INTEGER NOT NULL,
discount_cents INTEGER NOT NULL DEFAULT 0,
tax_rate DECIMAL(5,2) NOT NULL,
tax_amount_cents INTEGER NOT NULL,
line_total_cents INTEGER NOT NULL,
CONSTRAINT chk_positive_quantity CHECK (quantity > 0),
CONSTRAINT chk_non_negative_price CHECK (unit_price_cents >= 0),
UNIQUE(order_id, line_number)
);
-- Order status history for audit
CREATE TABLE order_status_history (
id UUID PRIMARY KEY,
order_id UUID NOT NULL REFERENCES orders(id),
from_status VARCHAR(20),
to_status VARCHAR(20) NOT NULL,
changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
changed_by UUID, -- User who made change
reason TEXT
);
-- Indexes
CREATE INDEX idx_order_customer ON orders(customer_id);
CREATE INDEX idx_order_status ON orders(status);
CREATE INDEX idx_order_date ON orders(order_date);
CREATE INDEX idx_order_line_order ON order_line(order_id);
CREATE INDEX idx_order_line_product ON order_line(product_id);Common Queries
sql-- Get order with lines
SELECT
o.*,
json_agg(
json_build_object(
'lineNumber', ol.line_number,
'productName', ol.product_name,
'quantity', ol.quantity,
'unitPrice', ol.unit_price_cents / 100.0,
'lineTotal', ol.line_total_cents / 100.0
) ORDER BY ol.line_number
) as lines
FROM orders o
LEFT JOIN order_line ol ON ol.order_id = o.id
WHERE o.id = :orderId
GROUP BY o.id;
-- Customer order history
SELECT o.order_number, o.order_date, o.status, o.grand_total_cents
FROM orders o
WHERE o.customer_id = :customerId
ORDER BY o.order_date DESC;
-- Orders pending payment
SELECT * FROM orders
WHERE status = 'Confirmed'
AND confirmed_at < CURRENT_TIMESTAMP - INTERVAL '24 hours';
-- Sales by product
SELECT
ol.product_sku,
ol.product_name,
SUM(ol.quantity) as total_quantity,
SUM(ol.line_total_cents) as total_revenue
FROM order_line ol
JOIN orders o ON o.id = ol.order_id
WHERE o.status NOT IN ('Draft', 'Cancelled')
AND o.order_date BETWEEN :startDate AND :endDate
GROUP BY ol.product_sku, ol.product_name
ORDER BY total_revenue DESC;Design Considerations
Snapshot vs Reference
| Approach | Pros | Cons |
|---|---|---|
| Reference | Always current data | Historical orders show wrong data |
| Snapshot | Historical accuracy | Data duplication |
Recommendation: Snapshot critical fields (name, SKU, price) at order time. Keep reference for navigation.
Monetary Values
// NEVER use floating point for money!
// Bad: price = 19.99 (float)
// Good: priceInCents = 1999 (integer)
// Or use a Money type:
Money {
amount: Integer // In smallest unit (cents, grosze)
currency: String // ISO code
}Immutability After Confirmation
Once an order is confirmed:
- Lines cannot be added/removed
- Quantities cannot change
- Prices are locked
For changes after confirmation:
- Cancel and create new order
- Or create adjustment/amendment record
Order vs Quote vs Invoice
| Document | Purpose | Status Flow |
|---|---|---|
| Quote | Price estimate | Draft → Sent → Accepted/Expired |
| Order | Customer purchase intent | Draft → Confirmed → Paid... |
| Invoice | Payment request | Issued → Paid → Overdue |
These can share structure but have different lifecycles.
Related Patterns
- Party Role (Customer) — Who places orders
- Product — What is ordered
- Payment — How orders are paid
- Shipment — How orders are delivered
- Inventory — Stock reserved/issued for orders