Online Ordering Context¶
The Online Ordering context manages the online ordering experience including menu browsing, cart management, fulfillment selection (pickup or delivery), and checkout. It is deliberately decoupled from the Delivery context so electronic menus can be developed independently.
Purpose¶
Online customers interact with the website or mobile app to browse the menu, add items to a cart, choose pickup or delivery, and pay. This context handles everything up to and including payment. If the customer chose delivery, the order is handed off to the Delivery context via an adaptor.
Interview Connection¶
From the Online Customer's interview:
"Their website and app aren't always working. Or if it is working it can be really slow."
"When it doesn't work at all, I usually just don't bother. When that happens I will usually just order from the other place down the street."
Isolating online ordering in its own context means it can be deployed, scaled, and maintained independently from the restaurant's dine-in systems. A kitchen outage doesn't crash the online menu.
Types¶
Note the 9-value OnlineOrderStatus enumeration — online orders
have more states than dine-in orders because they include
fulfillment tracking.
Entity: OnlineOrder¶
The OnlineOrder entity has a 6-command lifecycle:
event-sourced entity OnlineOrder as flow is {
// An event-sourced entity OWNS the commands and events that change it, so
// they are declared INSIDE it, and every command names the event it yields.
command BrowseMenu yields event MenuBrowsed is { onlineOrderId: OnlineOrderId }
command AddToCart yields event ItemAddedToCart is { onlineOrderId: OnlineOrderId }
event MenuBrowsed is { onlineOrderId: OnlineOrderId }
event ItemAddedToCart is { onlineOrderId: OnlineOrderId }
event AddToCartRejected is { onlineOrderId: OnlineOrderId, rejectionReason: String(1,500) }
record OnlineOrderData is { onlineOrderId: OnlineOrderId }
// Lifecycle phases are named STATES, not a status field: each state
// declares the commands it accepts, so the compiler knows the machine.
initial state Browsing of record OnlineOrderData is {
handler BrowsingHandler is {
on cmd: command AddToCart is {
yield event ItemAddedToCart(onlineOrderId = cmd.onlineOrderId)
}
// `set` and `morph` may appear ONLY in an `on event` clause here:
// replay has to re-apply exactly the same change.
on evt: event ItemAddedToCart is {
morph entity OnlineOrder to state FulfillmentChosen
with record OnlineOrderData(onlineOrderId = evt.onlineOrderId)
}
}
}
state FulfillmentChosen of record OnlineOrderData is {
handler FulfillmentChosenHandler is {
// A command this state does not accept is refused -- and the refusal
// is PUBLISHED before it is raised, so the attempt is recorded.
on cmd: command AddToCart is {
send event AddToCartRejected(onlineOrderId = cmd.onlineOrderId,
rejectionReason = "OnlineOrder does not accept AddToCart in this state")
to outlet OnlineOrderEvents
error "OnlineOrder does not accept AddToCart in this state"
}
}
}
// A processor receives on its OWN inlet and publishes on its OWN outlet,
// and a portlet's type must ADMIT everything that travels on it.
type OnlineOrderCommand is BrowseMenu | AddToCart
type OnlineOrderEvent is MenuBrowsed | ItemAddedToCart | AddToCartRejected
inlet OnlineOrderCommands is type OnlineOrderCommand
outlet OnlineOrderEvents is type OnlineOrderEvent
}
Note that SelectFulfillment uses optional DeliveryAddress —
the address is only required for delivery fulfillment, not
pickup. The optional keyword in RIDDL makes this explicit in
the model.
Repository¶
repository OnlineOrderRepository as flow is {
inlet OnlineOrderRepositoryFromOnlineOrder is command PersistItemAddedToCart
outlet OnlineOrderRepositoryResponses is result OnlineOrderResult
// A repository answers with a RESULT, never an event.
result OnlineOrderResult is { found: Boolean }
record StoredOnlineOrder is { onlineOrderId: OnlineOrderId }
// A repository that answers queries and declares NO index at all is a
// sequential scan by construction, and draws a warning saying so.
schema OnlineOrderSchema is relational
of rows as record StoredOnlineOrder
index on field StoredOnlineOrder.onlineOrderId
command PersistItemAddedToCart is { onlineOrderId: OnlineOrderId }
handler OnlineOrderPersistence is {
on command PersistItemAddedToCart is {
do "update the stored onlineOrder row for this onlineOrderId"
}
// An inlet admitting an alternation needs a clause that receives it.
// Handling each member is not enough -- say what ARRIVING means.
on other is {
do "persist whatever else arrives on this inlet"
}
}
}
The index on customerId supports order history lookups — the
online customer mentioned "they have all of that stuff on file
already."
Adaptors¶
Online Ordering has three outbound adaptors:
context OnlineOrdering is {
// An adaptor is the translation seam at a context boundary: it is the only
// place that knows the OTHER context's message shapes.
adaptor ToKitchen to context Kitchen is {
handler ToKitchenIntake is {
on command Kitchen.ReceiveTicket is {
do "turn a submitted online order into a kitchen ticket"
}
// Every adaptor handler must say what it does with what it does not
// recognise. Silence is not an option in 2.0.
on other is {
error "Unexpected message from Kitchen"
}
}
}
}
Design Decisions¶
Why decouple from Delivery? The CEO wanted electronic menus. The Head Chef wanted streamlined menu distribution. If online ordering and delivery were a single context, you couldn't ship the electronic menu without also completing the delivery rewrite. Decoupling means the menu experience can launch first, with delivery improvements following independently.
Why BrowseMenu creates the entity? The morph on
BrowseMenu creates the online order session. This captures
the browsing-to-purchase funnel in the event stream, enabling
analytics on cart abandonment and conversion rates.
Why separate from FrontOfHouse? Online and dine-in orders
have different types (CartItem vs OrderLine), different
flows (cart + fulfillment selection vs table + server), and
different scalability requirements. A busy Friday night at the
restaurant shouldn't slow down the website.