Scheduling Context¶
The Scheduling context manages staff shift planning, assignment, time tracking, shift swaps, and coverage. It provides the foundation for labor reporting in the Reporting context.
Purpose¶
Every restaurant location needs to schedule staff across roles (host, server, bartender, chef, cook, dishwasher, manager). The Scheduling context handles the lifecycle of each shift from creation through assignment, clock-in/out, and potential swaps or cancellations.
Types¶
The ShiftRole enumeration maps directly to the personas
interviewed — Host, Server, Bartender, Chef, Cook — plus
Dishwasher and Manager.
Entity: Shift¶
The Shift entity has a 6-command lifecycle:
event-sourced entity Shift 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 CreateShift yields event ShiftCreated is { shiftId: ShiftId }
command AssignEmployee yields event EmployeeAssigned is { shiftId: ShiftId }
event ShiftCreated is { shiftId: ShiftId }
event EmployeeAssigned is { shiftId: ShiftId }
event AssignEmployeeRejected is { shiftId: ShiftId, rejectionReason: String(1,500) }
record ShiftData is { shiftId: ShiftId }
// Lifecycle phases are named STATES, not a status field: each state
// declares the commands it accepts, so the compiler knows the machine.
initial state ActiveShift of record ShiftData is {
handler ActiveShiftHandler is {
on cmd: command AssignEmployee is {
yield event EmployeeAssigned(shiftId = cmd.shiftId)
}
// `set` and `morph` may appear ONLY in an `on event` clause here:
// replay has to re-apply exactly the same change.
on evt: event EmployeeAssigned is {
morph entity Shift to state Filled
with record ShiftData(shiftId = evt.shiftId)
}
}
}
state Filled of record ShiftData is {
handler FilledHandler 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 AssignEmployee is {
send event AssignEmployeeRejected(shiftId = cmd.shiftId,
rejectionReason = "Shift does not accept AssignEmployee in this state")
to outlet ShiftEvents
error "Shift does not accept AssignEmployee 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 ShiftCommand is CreateShift | AssignEmployee
type ShiftEvent is ShiftCreated | EmployeeAssigned | AssignEmployeeRejected
inlet ShiftCommands is type ShiftCommand
outlet ShiftEvents is type ShiftEvent
}
The lifecycle: Create → Assign Employee → Clock In → Clock Out (with optional Swap or Cancel at any point).
The SwapShift command tracks both the original and replacement
employee, maintaining an audit trail of who was originally
assigned. This matters for labor compliance and reporting.
Repository¶
repository ShiftRepository as flow is {
inlet ShiftRepositoryFromShift is command PersistEmployeeAssigned
outlet ShiftRepositoryResponses is result ShiftResult
// A repository answers with a RESULT, never an event.
result ShiftResult is { found: Boolean }
record StoredShift is { shiftId: ShiftId }
// A repository that answers queries and declares NO index at all is a
// sequential scan by construction, and draws a warning saying so.
schema ShiftSchema is relational
of rows as record StoredShift
index on field StoredShift.shiftId
command PersistEmployeeAssigned is { shiftId: ShiftId }
handler ShiftPersistence is {
on command PersistEmployeeAssigned is {
do "update the stored shift row for this shiftId"
}
// 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 shiftDate enables schedule-by-day views. The
index on employeeId supports employee-centric schedule views
("What are my shifts this week?").
Design Decisions¶
Why no projectors? The Scheduling context is focused on
write operations — creating and managing shifts. The
schedule view could be a projector, but the
Reporting context already handles the read-model
side with the LaborReport projector that listens to
scheduling events.
Foundation for labor reporting: The ClockedIn and
ClockedOut events flow to the Reporting context's
LaborReport projector, which calculates hours worked,
completed shifts, and average shift duration. Scheduling
doesn't need to know about reports — it just emits events.