Ordering — version 1
Ordering bounded context — taking and pricing a customer’s pizza order. This is the heart of the template: the Sales aggregate (root Order) is the headline aggregate, its lifecycle is an explicit state machine, it is priced from its line items, and it publishes OrderPlaced so Kitchen, Delivery and Payment can react.
Domain Model
Section titled “Domain Model”classDiagram
class Fulfillment {
<<enumeration>>
DineIn
Pickup
Delivery
}
class OrderStatus {
<<enumeration>>
Draft
Placed
InKitchen
OutForDelivery
Completed
Cancelled
}
class Money {
<<value object>>
+Decimal amount
+Currency currency
}
class OrderOpened {
<<event>>
+OrderId orderId
+CustomerId customer
+Int lineCount
}
class OrderPlacedInternally {
<<event>>
+OrderId orderId
+Int lineCount
}
class OrderCancelled {
<<event>>
+OrderId orderId
}
class OrderLine {
<<value object>>
+PizzaCode pizza
+Int quantity
+Money unitPrice
+Money /lineTotal
+Money /payable
}
class Order {
<<aggregate root>>
+Int version
+OrderId id
+CustomerId customer
+Fulfillment fulfillment
+List~OrderLine~ lines
+OrderStatus status
+Instant placedAt
+Money /total
+Int /lineCount
+Bool /isPlaced
+Bool /isCancelled
+Bool /isDelivery
+open(CustomerId customer, Fulfillment fulfillment, List~OrderLine~ lines) Order
+place()
+cancel()
}
OrderOpened --> Order
OrderPlacedInternally --> Order
OrderCancelled --> Order
OrderLine *-- Money
Order *-- Fulfillment
Order *-- OrderLine
Order *-- OrderStatus
Order *-- Money
Aggregates
Section titled “Aggregates”Sales (versioned)
Section titled “Sales (versioned)”The order aggregate. versioned adds an optimistic-concurrency token; the repository block tunes the mutating set and adds intention-revealing finders; being an aggregate it also joins the context IUnitOfWork.
Root entity: Order (identified by OrderId)
Repository finders:
byCustomer(customer: CustomerId): List<Order>mostRecent(customer: CustomerId): Order
Repository operations: getById, add, update
Value Objects
Section titled “Value Objects”OrderLine
One line of an order: a pizza, how many, and the unit price. The pricing value object of the template — it derives its own totals.
| Field | Type | Description |
|---|---|---|
| pizza | PizzaCode | |
| quantity | Int | How many of this pizza. At least one. |
| unitPrice | Money | |
| lineTotal | Money | derived |
| payable | Money | derived — What the customer actually pays for this line (10% off at 5+ pizzas). |
Business rules
- an order line needs at least one pizza
Domain Events
Section titled “Domain Events”OrderOpened
Raised when an order is opened by the factory.
| Field | Type | Description |
|---|---|---|
| orderId | OrderId | |
| customer | CustomerId | |
| lineCount | Int |
OrderPlacedInternally
Raised when an order is placed for processing.
| Field | Type | Description |
|---|---|---|
| orderId | OrderId | |
| lineCount | Int |
OrderCancelled
Raised when an order is cancelled.
| Field | Type | Description |
|---|---|---|
| orderId | OrderId |
Lifecycle
Section titled “Lifecycle”stateDiagram-v2
[*] --> Draft
Draft --> Placed
Draft --> Cancelled
Placed --> InKitchen
Placed --> Cancelled
InKitchen --> OutForDelivery
InKitchen --> Completed
InKitchen --> Cancelled
OutForDelivery --> Completed
Completed --> [*]
Cancelled --> [*]
| From | To | Guard |
|---|---|---|
Draft | Placed | |
Draft | Cancelled | |
Placed | InKitchen | |
Placed | Cancelled | |
InKitchen | OutForDelivery | |
InKitchen | Completed | |
InKitchen | Cancelled | |
OutForDelivery | Completed | |
Completed | (terminal) | |
Cancelled | (terminal) |
Commands
Section titled “Commands”place()
Section titled “place()”Place a drafted order for processing, stamping the time and recording a domain event. The cross-context announcement is OrderPlaced.
Preconditions:
- only a draft order can be placed
- cannot place an empty order
Effects:
status -> PlacedplacedAt -> now
Events:
OrderPlacedInternally(orderId: id, lineCount: lines.count)
cancel()
Section titled “cancel()”Cancel an order that has not yet gone out for delivery.
Preconditions:
- an order already out for delivery cannot be cancelled
- a completed order cannot be cancelled
Effects:
status -> Cancelled
Events:
OrderCancelled(orderId: id)
Factory Operations
Section titled “Factory Operations”open(customer: CustomerId, fulfillment: Fulfillment, lines: List<OrderLine>)
Section titled “open(customer: CustomerId, fulfillment: Fulfillment, lines: List<OrderLine>)”Factory-only creation: open a draft order for a customer. The presence of any create makes the all-args constructor private, so callers must go through Order.Open(...).
Preconditions:
- cannot open an empty order
Events:
OrderOpened(orderId: id, customer: customer, lineCount: lines.count)
Invariants
Section titled “Invariants”Business rules
- every line needs a positive quantity
- no duplicate pizzas in an order
- status == Draft when lines.isEmpty
Domain Types
Section titled “Domain Types”Money — value object
Section titled “Money — value object”A monetary amount in a specific currency. Never negative. Currency is a shared-kernel type owned jointly with Menu (see context-map.koi) — the same enum flows through both contexts without translation.
| Field | Type | Description |
|---|---|---|
| amount | Decimal | |
| currency | Currency |
Business rules
- an amount cannot be negative
Fulfillment — enum
Section titled “Fulfillment — enum”How the customer wants the order fulfilled. Drives routing into Delivery.
Values: DineIn, Pickup, Delivery
OrderStatus — enum
Section titled “OrderStatus — enum”The lifecycle state of an order. Drives the states status { … } machine on the entity, so only the transitions declared there are legal at runtime.
Values: Draft, Placed, InKitchen, OutForDelivery, Completed, Cancelled
Events
Section titled “Events”Integration Events
Section titled “Integration Events”OrderPlaced
Section titled “OrderPlaced”Announced to the rest of the system when an order is placed. An integration event is a published language — its fields stay primitive (ids/scalars/enums), never leaking internal value objects. Kitchen, Delivery and Payment all subscribe to it (authorized by the open-host relations).
| Field | Type | Description |
|---|---|---|
| orderId | OrderId | |
| customer | CustomerId | |
| fulfillment | Fulfillment | |
| total | Decimal | |
| placedAt | Instant |
Published by this context.
Services
Section titled “Services”OrderingService
Section titled “OrderingService”The application/use-case service interface (IOrderingService). Each use case maps to one async method; a context with aggregates also gets a UoW.
Use Cases
Section titled “Use Cases”-
PlaceOrder(customer: CustomerId, fulfillment: Fulfillment, lines: List<OrderLine>): OrderId -
CancelOrder(order: OrderId)
Coverage
Section titled “Coverage”| Kind | Covered | Total |
|---|---|---|
| aggregate | 1 | 1 |
| entity | 1 | 1 |
| enum | 2 | 2 |
| event | 3 | 3 |
| integrationEvent | 1 | 1 |
| query | 0 | 1 |
| readModel | 0 | 1 |
| value | 2 | 2 |
⚠️ 2 not documented