External Contexts¶
The Reactive BBQ model includes six external contexts — bounded
contexts that represent third-party systems the business depends
on but doesn't own. These are marked with option is external
to signal that the implementation is outside the model boundary.
The option is external Pattern¶
In RIDDL, external contexts define the interface to a third-party system without modeling its internals. The context specifies the commands you can send and the events you expect to receive, but the implementation is someone else's responsibility.
// `external` marks a context the chain does NOT build. It still declares a
// real interface, because everything inside must be typed against it.
external context PaymentGateway is {
command AuthorizePayment yields event PaymentAuthorized is {
paymentGatewayTransactionId: String(1,100)
authorizeAmount: Decimal(10,2)
}
event PaymentAuthorized is {
paymentGatewayTransactionId: String(1,100)
authorizeAmount: Decimal(10,2)
}
}
The option is external metadata tells the compiler and any code
generators that this context won't be implemented — it's a
boundary definition only.
Restaurant Domain — External Contexts¶
PaymentGateway¶
external context PaymentGateway as flow is {
// An external context that publishes declares its OWN portlets: a
// cross-context connector must land on the context's own outlet, never
// reach past the boundary to something the context contains.
inlet PaymentGatewayIn is type PaymentGatewayEvent
outlet PaymentGatewayOut is type PaymentGatewayEvent
command AuthorizePayment yields event PaymentAuthorized is {
paymentGatewayTransactionId: String(1,100)
authorizeAmount: Decimal(10,2)
}
event PaymentAuthorized is {
paymentGatewayTransactionId: String(1,100)
authorizeAmount: Decimal(10,2)
}
type PaymentGatewayEvent is PaymentAuthorized
handler PaymentGatewayHandler is {
on cmd: command AuthorizePayment is {
yield event PaymentAuthorized(
paymentGatewayTransactionId = cmd.paymentGatewayTransactionId,
authorizeAmount = cmd.authorizeAmount)
do "submit the authorization request to the payment processor"
}
// An adaptor or boundary handler must say what it does with what it does
// not recognise.
on other is {
error "Unexpected message at the PaymentGateway boundary"
}
}
}
The PaymentGateway models the authorize-then-capture payment flow used by credit card processors. The two-step process (authorize, then capture) is standard in payment processing — authorization holds the funds, capture completes the transfer.
NotificationService¶
external context NotificationService as flow is {
// An external context that publishes declares its OWN portlets: a
// cross-context connector must land on the context's own outlet, never
// reach past the boundary to something the context contains.
inlet NotificationServiceIn is type NotificationServiceEvent
outlet NotificationServiceOut is type NotificationServiceEvent
command SendPushNotification yields event NotificationSent is {
notificationRecipient: String(1,120)
notificationBody: String(1,500)
}
event NotificationSent is {
notificationRecipient: String(1,120)
notificationBody: String(1,500)
}
type NotificationServiceEvent is NotificationSent
handler NotificationServiceHandler is {
on cmd: command SendPushNotification is {
yield event NotificationSent(
notificationRecipient = cmd.notificationRecipient,
notificationBody = cmd.notificationBody)
do "hand the message to the push provider"
}
// An adaptor or boundary handler must say what it does with what it does
// not recognise.
on other is {
error "Unexpected message at the NotificationService boundary"
}
}
}
The NotificationService is used by the Bar context to send push notifications when drinks are ready. It's also available for the Delivery context to notify drivers of new orders.
BackOffice Domain — External Contexts¶
HRSystem¶
external context HRSystem as flow is {
// An external context that publishes declares its OWN portlets: a
// cross-context connector must land on the context's own outlet, never
// reach past the boundary to something the context contains.
inlet HRSystemIn is type HRSystemEvent
outlet HRSystemOut is type HRSystemEvent
command SyncEmployeeData yields event EmployeeDataSynced is {
employeeRecordId: String(1,50)
syncedAt: TimeStamp
}
event EmployeeDataSynced is {
employeeRecordId: String(1,50)
syncedAt: TimeStamp
}
type HRSystemEvent is EmployeeDataSynced
handler HRSystemHandler is {
on cmd: command SyncEmployeeData is {
yield event EmployeeDataSynced(
employeeRecordId = cmd.employeeRecordId,
syncedAt = cmd.syncedAt)
do "reconcile the employee record with the HR system of record"
}
// An adaptor or boundary handler must say what it does with what it does
// not recognise.
on other is {
error "Unexpected message at the HRSystem boundary"
}
}
}
The HRSystem provides employee master data to the
Scheduling context. The
SyncEmployeeData command enables the scheduling system to
pull current employee records — roles, availability, and
contact information.
AccountingSystem¶
external context AccountingSystem as flow is {
// An external context that publishes declares its OWN portlets: a
// cross-context connector must land on the context's own outlet, never
// reach past the boundary to something the context contains.
inlet AccountingSystemIn is type AccountingSystemEvent
outlet AccountingSystemOut is type AccountingSystemEvent
command PostTransaction yields event TransactionPosted is {
transactionId: String(1,50)
postedAmount: Decimal(12,2)
}
event TransactionPosted is {
transactionId: String(1,50)
postedAmount: Decimal(12,2)
}
type AccountingSystemEvent is TransactionPosted
handler AccountingSystemHandler is {
on cmd: command PostTransaction is {
yield event TransactionPosted(
transactionId = cmd.transactionId,
postedAmount = cmd.postedAmount)
do "post the transaction to the general ledger"
}
// An adaptor or boundary handler must say what it does with what it does
// not recognise.
on other is {
error "Unexpected message at the AccountingSystem boundary"
}
}
}
The AccountingSystem receives financial transactions from the restaurant operations. Sales revenue, labor costs, and inventory purchases are posted to the general ledger for financial reporting.
Corporate Domain — External Contexts¶
PrintingService¶
external context PrintingService as flow is {
// An external context that publishes declares its OWN portlets: a
// cross-context connector must land on the context's own outlet, never
// reach past the boundary to something the context contains.
inlet PrintingServiceIn is type PrintingServiceEvent
outlet PrintingServiceOut is type PrintingServiceEvent
command PrintMenus yields event MenusPrinted is {
printJobId: String(1,50)
printQuantity: Natural
}
event MenusPrinted is {
printJobId: String(1,50)
printQuantity: Natural
}
type PrintingServiceEvent is MenusPrinted
handler PrintingServiceHandler is {
on cmd: command PrintMenus is {
yield event MenusPrinted(
printJobId = cmd.printJobId,
printQuantity = cmd.printQuantity)
do "send the menu artwork to the print vendor"
}
// An adaptor or boundary handler must say what it does with what it does
// not recognise.
on other is {
error "Unexpected message at the PrintingService boundary"
}
}
}
The PrintingService is triggered when a MenuRelease is published, sending the updated menu to the printer for physical copies distributed to all locations.
PhotographyService¶
external context PhotographyService as flow is {
// An external context that publishes declares its OWN portlets: a
// cross-context connector must land on the context's own outlet, never
// reach past the boundary to something the context contains.
inlet PhotographyServiceIn is type PhotographyServiceEvent
outlet PhotographyServiceOut is type PhotographyServiceEvent
command SchedulePhotoShoot yields event PhotoShootScheduled is {
photoShootId: String(1,50)
scheduledFor: TimeStamp
}
event PhotoShootScheduled is {
photoShootId: String(1,50)
scheduledFor: TimeStamp
}
type PhotographyServiceEvent is PhotoShootScheduled
handler PhotographyServiceHandler is {
on cmd: command SchedulePhotoShoot is {
yield event PhotoShootScheduled(
photoShootId = cmd.photoShootId,
scheduledFor = cmd.scheduledFor)
do "book the photographer for the new menu items"
}
// An adaptor or boundary handler must say what it does with what it does
// not recognise.
on other is {
error "Unexpected message at the PhotographyService boundary"
}
}
}
From the Head Chef's interview: working with a photographer is part of the monthly menu update process. The PhotographyService captures this relationship in the model.
Design Decisions¶
Why model external systems at all? Even though we don't implement these systems, modeling them provides several benefits:
- Interface documentation — The commands and events define the exact integration contract
- Validation — The compiler can check that references to external context commands and events are valid
- Completeness — The model shows the full system boundary, not just the parts we build
- Code generation — Integration adapters and API clients can be generated from the interface definitions
Why not use adaptors instead? Adaptors bridge between two bounded contexts you own. External contexts model systems you don't own. The interface is defined on the external context itself, and adaptors in your contexts reference it.
Minimal interfaces: External contexts define only the commands and events relevant to this system. The actual payment gateway has hundreds of operations — we only model the ones Reactive BBQ uses.