Reporting Context¶
The Reporting context is a pure CQRS read-model — it contains only projectors, no entities. It builds reports asynchronously from events emitted by production contexts, ensuring that report generation never degrades peak-hour restaurant performance.
Purpose¶
Management needs sales reports, labor reports, and inventory reports. In a monolithic system, generating these reports queries the same database that handles live orders, causing slowdowns during peak hours. The Reporting context solves this by maintaining its own read-optimized views built from events.
Design: No Entities¶
This is the only context in the entire Reactive BBQ model that has no entities. It only has projectors. This is a deliberate CQRS pattern — the Reporting context is the "query" side. The "command" sides are the production contexts (FrontOfHouse, OnlineOrdering, Scheduling, Inventory) that emit events.
SalesReport Projector¶
// The repository is a SINK: reports are written, never read back into the
// write side. The projector is its SOURCE.
repository SalesReportRepository as sink is {
inlet SalesReportRepositoryFromSalesReport is type SalesReportCommand
type SalesReportCommand is RecordDineInPayment
record SalesReportRecord is {
reportDate: Date
totalRevenue: Decimal(12,2)
orderCount: Natural
}
schema SalesReportData is relational
of rows as record SalesReportRecord
index on field SalesReportRecord.reportDate
command RecordDineInPayment is { tableOrderId: String(1,50) }
handler SalesReportPersistence is {
on command RecordDineInPayment is {
do "upsert the sales row for this date: increment orderCount and add the payment to totalRevenue"
}
// 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"
}
}
}
// `tell` needs a MODELLED channel: a connector from the sender's own
// outlet to the target's inlet. Declaring both ports is not enough -- the
// delivery has to be something the model states, not something it implies.
connector SalesReportToRepository is
from outlet SalesReport.SalesReportOut
to inlet SalesReportRepository.SalesReportRepositoryFromSalesReport
projector SalesReport as source is {
updates repository SalesReportRepository
outlet SalesReportOut is type SalesReportCommand
record SalesReportEntry is {
reportDate: Date
totalRevenue: Decimal(12,2)
orderCount: Natural
}
handler SalesReportHandler is {
on evt: event PaymentProcessed is {
tell command RecordDineInPayment(tableOrderId = evt.tableOrderId) to repository SalesReportRepository
}
}
}
The SalesReport listens to payment events from two different contexts — FrontOfHouse (dine-in) and OnlineOrdering (online). It aggregates them into a unified view with breakdowns by channel.
LaborReport Projector¶
// The repository is a SINK: reports are written, never read back into the
// write side. The projector is its SOURCE.
repository LaborReportRepository as sink is {
inlet LaborReportRepositoryFromLaborReport is type LaborReportCommand
type LaborReportCommand is RecordShiftHours
record LaborReportRecord is {
laborReportDate: Date
hoursWorked: Decimal(10,2)
laborCost: Decimal(12,2)
}
schema LaborReportData is relational
of rows as record LaborReportRecord
index on field LaborReportRecord.laborReportDate
command RecordShiftHours is { shiftId: String(1,50) }
handler LaborReportPersistence is {
on command RecordShiftHours is {
do "upsert the labor row for this date: add the shift's hours and cost"
}
// 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"
}
}
}
// `tell` needs a MODELLED channel: a connector from the sender's own
// outlet to the target's inlet. Declaring both ports is not enough -- the
// delivery has to be something the model states, not something it implies.
connector LaborReportToRepository is
from outlet LaborReport.LaborReportOut
to inlet LaborReportRepository.LaborReportRepositoryFromLaborReport
projector LaborReport as source is {
updates repository LaborReportRepository
outlet LaborReportOut is type LaborReportCommand
record LaborReportEntry is {
laborReportDate: Date
hoursWorked: Decimal(10,2)
laborCost: Decimal(12,2)
}
handler LaborReportHandler is {
on evt: event ClockedIn is {
tell command RecordShiftHours(shiftId = evt.shiftId) to repository LaborReportRepository
}
}
}
The LaborReport listens to scheduling events from the same
BackOffice domain. It calculates hours worked by comparing
ClockedIn and ClockedOut event timestamps and tracks
shift cancellation rates.
InventoryReport Projector¶
// The repository is a SINK: reports are written, never read back into the
// write side. The projector is its SOURCE.
repository InventoryReportRepository as sink is {
inlet InventoryReportRepositoryFromInventoryReport is type InventoryReportCommand
type InventoryReportCommand is RecordStockMovement
record InventoryReportRecord is {
inventoryReportDate: Date
itemsConsumed: Natural
stockValue: Decimal(12,2)
}
schema InventoryReportData is relational
of rows as record InventoryReportRecord
index on field InventoryReportRecord.inventoryReportDate
command RecordStockMovement is { inventoryItemId: String(1,50) }
handler InventoryReportPersistence is {
on command RecordStockMovement is {
do "upsert the inventory row for this date: add the consumed quantity and revalue stock"
}
// 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"
}
}
}
// `tell` needs a MODELLED channel: a connector from the sender's own
// outlet to the target's inlet. Declaring both ports is not enough -- the
// delivery has to be something the model states, not something it implies.
connector InventoryReportToRepository is
from outlet InventoryReport.InventoryReportOut
to inlet InventoryReportRepository.InventoryReportRepositoryFromInventoryReport
projector InventoryReport as source is {
updates repository InventoryReportRepository
outlet InventoryReportOut is type InventoryReportCommand
record InventoryReportEntry is {
inventoryReportDate: Date
itemsConsumed: Natural
stockValue: Decimal(12,2)
}
handler InventoryReportHandler is {
on evt: event StockConsumed is {
tell command RecordStockMovement(inventoryItemId = evt.inventoryItemId) to repository InventoryReportRepository
}
}
}
The InventoryReport provides a dashboard view of stock health across the location — how many items are tracked, how many are running low, and the total estimated stock value.
Design Decisions¶
Why no entities? Reports don't have their own commands or lifecycle. They are purely derived from events happening elsewhere. Making them projectors without entities makes this read-only nature explicit in the model.
Why a separate context? If reports were projectors inside the FrontOfHouse or Scheduling contexts, they would share resources with production workloads. Isolating them in their own context means they can be deployed on separate infrastructure, scaled independently, and even be temporarily unavailable without affecting production.
Cross-domain event consumption: Notice that the Reporting context listens to events from both the Restaurant domain (payment events) and the BackOffice domain (scheduling and inventory events). This cross-domain listening is exactly what CQRS projectors are designed for — they aggregate data from wherever it originates.
Eventual consistency: Reports are eventually consistent with production data. There may be a brief delay between a payment being processed and the sales report reflecting it. This is acceptable for management reporting and is the key trade-off that enables production isolation.