Menu Management Context¶
The Menu Management context manages the corporate menu lifecycle
including recipe development, pricing, and atomic menu
distribution to all 500+ locations via the MenuRelease entity.
It addresses the Head Chef's monthly coordination bottleneck by
enabling versioned, atomic menu updates.
Purpose¶
Every month, the Head Chef develops new recipes, adjusts pricing, and needs to distribute the updated menu simultaneously to all locations, the website, the printing service, and the photography service. Previously, this required manual coordination across multiple teams and systems.
Interview Connection¶
From the Head Chef's interview:
"The menu is decided on monthly and distributed via email."
"The biggest headache is coordinating the monthly menu update across all locations, the website, the printers..."
The MenuRelease entity models this as an atomic operation:
changes are bundled, reviewed, and published simultaneously.
Types¶
Note the RecipeInfo record uses Duration for prep and cook
times — a RIDDL predefined type — and many String for the
ingredients list.
Entity: MenuItem¶
The MenuItem entity manages individual menu items:
event-sourced entity MenuItem 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 CreateMenuItem yields event MenuItemCreated is { menuItemId: MenuItemId }
command SetPrice yields event PriceSet is { menuItemId: MenuItemId }
event MenuItemCreated is { menuItemId: MenuItemId }
event PriceSet is { menuItemId: MenuItemId }
event SetPriceRejected is { menuItemId: MenuItemId, rejectionReason: String(1,500) }
record MenuItemData is { menuItemId: MenuItemId }
// Lifecycle phases are named STATES, not a status field: each state
// declares the commands it accepts, so the compiler knows the machine.
initial state ActiveMenuItem of record MenuItemData is {
handler ActiveMenuItemHandler is {
on cmd: command SetPrice is {
yield event PriceSet(menuItemId = cmd.menuItemId)
}
// `set` and `morph` may appear ONLY in an `on event` clause here:
// replay has to re-apply exactly the same change.
on evt: event PriceSet is {
morph entity MenuItem to state Priced
with record MenuItemData(menuItemId = evt.menuItemId)
}
}
}
state Priced of record MenuItemData is {
handler PricedHandler 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 SetPrice is {
send event SetPriceRejected(menuItemId = cmd.menuItemId,
rejectionReason = "MenuItem does not accept SetPrice in this state")
to outlet MenuItemEvents
error "MenuItem does not accept SetPrice 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 MenuItemCommand is CreateMenuItem | SetPrice
type MenuItemEvent is MenuItemCreated | PriceSet | SetPriceRejected
inlet MenuItemCommands is type MenuItemCommand
outlet MenuItemEvents is type MenuItemEvent
}
Note that UpdateMenuItem uses optional fields — you can
update the name, description, or recipe independently without
providing all fields every time.
Entity: MenuRelease¶
The MenuRelease entity is the key innovation — it bundles
menu changes into an atomic release:
// A release is its own entity because the ATOMIC unit is the whole menu, not
// one item: 500+ locations must flip together or the chain is inconsistent.
event-sourced entity MenuRelease as flow is {
command PublishRelease yields event ReleasePublished is { releaseId: UUID }
event ReleasePublished is { releaseId: UUID }
record MenuReleaseData is { releaseId: UUID }
initial state ActiveRelease of record MenuReleaseData is {
handler ActiveReleaseHandler is {
on cmd: command PublishRelease is {
yield event ReleasePublished(releaseId = cmd.releaseId)
}
on evt: event ReleasePublished is {
set field MenuReleaseData.releaseId to "the published release id"
}
}
}
type MenuReleaseEvent is ReleasePublished
inlet MenuReleaseCommands is command PublishRelease
outlet MenuReleaseEvents is type MenuReleaseEvent
}
The lifecycle: Create → Add Items → Finalize → Publish →
(optional) Rollback. The releaseAction field on
AddItemToRelease supports different actions per item: add,
update, remove, or price-change.
The ReleasePublished event is the key moment:
event ReleasePublished is {
releaseId: UUID
releaseEffectiveAt: TimeStamp
releaseItemCount: Natural
}
Repositories¶
repository MenuItemRepository as flow is {
inlet MenuItemRepositoryFromMenuItem is command PersistPriceSet
outlet MenuItemRepositoryResponses is result MenuItemResult
// A repository answers with a RESULT, never an event.
result MenuItemResult is { found: Boolean }
record StoredMenuItem is { menuItemId: MenuItemId }
// A repository that answers queries and declares NO index at all is a
// sequential scan by construction, and draws a warning saying so.
schema MenuItemSchema is relational
of rows as record StoredMenuItem
index on field StoredMenuItem.menuItemId
command PersistPriceSet is { menuItemId: MenuItemId }
handler MenuItemPersistence is {
on command PersistPriceSet is {
do "update the stored menuItem row for this menuItemId"
}
// 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"
}
}
}
Adaptor¶
Menu Management has an outbound adaptor for distributing menus:
context MenuManagement 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 ToRestaurants to context FrontOfHouse is {
handler ToRestaurantsIntake is {
on command FrontOfHouse.RecordMenuItem is {
do "push the published menu to every location at once"
}
// 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 FrontOfHouse"
}
}
}
}
Design Decisions¶
Why atomic releases instead of individual item updates?
Updating menu items one at a time risks inconsistency — some
locations might have the new price while others still show the
old one. The MenuRelease entity bundles all changes and
publishes them atomically, ensuring all 500+ locations update
simultaneously.
Why separate MenuItem and MenuRelease entities? MenuItems represent the master catalog and evolve over time. MenuReleases are point-in-time snapshots of changes to be distributed. An item might be updated many times before being included in a release.
Solving the coordination bottleneck: The Head Chef
described a manual process involving email coordination with
printers, web teams, and locations. The MenuRelease entity
replaces this with a structured workflow: draft → add items →
finalize (review gate) → publish (atomic distribution) → optional
rollback.