Skip to content

Patterns

The Reactive BBQ model demonstrates seven cross-cutting RIDDL patterns. This page summarizes each pattern with real code and links to where it appears in the model.

Entity Lifecycle

Every entity follows the same structure: commands trigger state transitions, events record what happened, state captures the current data, and a handler wires it together.

// The lifecycle is NAMED STATES, not a status field: each state declares the
// commands it accepts, so an out-of-order command is refused by construction.
event-sourced entity KitchenTicket as flow is {
  command AssignStation yields event StationAssigned is { kitchenTicketId: KitchenTicketId }

  event StationAssigned is { kitchenTicketId: KitchenTicketId }
  event TicketReceived is { kitchenTicketId: KitchenTicketId }
  event AssignStationRejected is {
    kitchenTicketId: KitchenTicketId
    rejectionReason: String(1,500)
  }

  record KitchenTicketData is { kitchenTicketId: KitchenTicketId }

  initial state Received of record KitchenTicketData is {
    handler ReceivedHandler is {
      on cmd: command AssignStation is {
        yield event StationAssigned(kitchenTicketId = cmd.kitchenTicketId)
      }
      on evt: event StationAssigned is {
        morph entity KitchenTicket to state Assigned
          with record KitchenTicketData(kitchenTicketId = evt.kitchenTicketId)
      }
    }
  }

  state Assigned of record KitchenTicketData is {
    handler AssignedHandler is {
      on cmd: command AssignStation is {
        send event AssignStationRejected(kitchenTicketId = cmd.kitchenTicketId,
          rejectionReason = "already assigned") to outlet KitchenTicketEvents
        error "KitchenTicket does not accept AssignStation in this state"
      }
    }
  }

  type KitchenTicketEvent is TicketReceived | StationAssigned | AssignStationRejected
  inlet KitchenTicketCommands is command AssignStation
  outlet KitchenTicketEvents is type KitchenTicketEvent
}

The first command uses morph to create the entity instance. Subsequent commands use tell to emit events and update state.

Appears in: Every context — all 13 entities follow this pattern. See Kitchen for the canonical example.

Type System

RIDDL provides a rich type system for modeling domain data:

Id Types

type ReservationId is Id(Reservation)

Typed identifiers link to specific entities, enabling compile-time validation of cross-context references.

Enumerations

// An enumeration's enumerators join the ENCLOSING namespace, so a name here
// can collide with a state or constant elsewhere in the same context.
type DeliveryStatus is any of {
  Pending,
  InTransit,
  Delivered,
  Failed
}

Enumerations use any of to define a fixed set of values. Each value is a constant.

Records

record GeoLocation is {
  latitude: Decimal(9,6)
  longitude: Decimal(9,6)
}

Records group related fields. They can use predefined types like Decimal(9, 6), TimeStamp, Date, Duration, Boolean, Natural, Integer, UUID, and constrained strings like String(1, 200).

Collections

ticketItems: TicketItem+

The many keyword denotes a collection. The optional keyword makes a field nullable.

Appears in: Every context defines types. See Front of House for the most comprehensive type catalog and Delivery for GeoLocation.

Repository

Repositories define persistence schemas with indexes:

repository KitchenTicketRepository as flow is {
  inlet KitchenTicketRepositoryFromKitchenTicket is command PersistStationAssigned
  outlet KitchenTicketRepositoryResponses is result KitchenTicketResult

  // A repository answers with a RESULT, never an event.
  result KitchenTicketResult is { found: Boolean }

  record StoredKitchenTicket is {
    kitchenTicketId: KitchenTicketId
    currentStation: String(1,50)?
  }

  // A repository that answers queries and declares NO index at all is a
  // sequential scan by construction, and is warned about as one.
  schema KitchenTicketSchema is relational
    of tickets as record StoredKitchenTicket
      index on field StoredKitchenTicket.kitchenTicketId

  command PersistStationAssigned is { kitchenTicketId: KitchenTicketId }

  handler KitchenTicketPersistence is {
    on command PersistStationAssigned is {
      do "update the stored ticket's station"
    }
    // 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 schema declares relational storage with named indexes. The handler maps entity commands to persistence operations.

Appears in: Every context except Reporting (which has only projectors). See Inventory for an example with stock-level indexing.

Projector / CQRS

Projectors build read-optimized views from events:

// A projector is the READ side: it owns its own shape and persists through a
// repository, so report queries never compete with ticket processing.
projector ReservationBoard as flow is {
  updates repository KitchenTicketRepository
  inlet ReservationBoardIn is type KitchenTicketEvent
  outlet ReservationBoardOut is type KitchenTicketEvent

  record ReservationBoardEntry is {
    kitchenTicketId: KitchenTicketId
    boardStation: String(1,50)?
  }

  handler ReservationBoardHandler is {
    on evt: event StationAssigned is {
      tell command PersistStationAssigned(kitchenTicketId = evt.kitchenTicketId)
        to repository KitchenTicketRepository
    }
    // The inlet admits an alternation, so the projector must say what
    // ARRIVING means -- handling each member individually is not enough.
    on other is {
      do "ignore any other event on this stream"
    }
  }
}

Projectors listen to events (not commands) and maintain a denormalized view. They can be rebuilt from the event stream at any time.

Appears in:

Adaptor Communication

Adaptors bridge bounded contexts. There are two directions:

Outbound (to)

context FrontOfHouseSeam is {
  // OUTBOUND: an adaptor `to` a context handles that context's INPUT -- a
  // command. Handling an event here is an Error.
  adaptor ToKitchen to context Kitchen is {
    handler ToKitchenOuttake is {
      on command Kitchen.ReceiveTicket is {
        do "turn the food lines of a submitted table order into a kitchen ticket"
      }
      on other is { error "Unexpected message bound for Kitchen" }
    }
  }
}

Outbound adaptors send messages from this context to another.

Inbound (from)

context KitchenSeam is {
  // INBOUND: an adaptor `from` a context handles that context's OUTPUT -- an
  // event. It is the only place that knows the other context's shapes.
  adaptor FromFrontOfHouse from context FrontOfHouse is {
    handler FrontOfHouseIntake is {
      on event FrontOfHouse.OrderSubmitted is {
        do "convert a submitted dine-in order into a kitchen ticket"
      }
      on other is { error "Unexpected message from Front of House" }
    }
  }
}

Inbound adaptors receive and transform messages from another context.

Appears in: Most contexts. See Kitchen for inbound adaptors, Front of House for outbound adaptors, and Loyalty for the most elegant use — consuming events without the source knowing about the consumer.

External Contexts

External contexts model third-party system boundaries:

// `external` marks a context the chain does not build. It still declares its
// OWN portlets, because a cross-context connector may not reach past it.
external context PaymentGateway as flow is {
  inlet PaymentGatewayIn is type PaymentGatewayEvent
  outlet PaymentGatewayOut is type PaymentGatewayEvent

  command AuthorizePayment yields event PaymentAuthorized is {
    paymentGatewayTransactionId: String(1,100)
  }
  event PaymentAuthorized is { paymentGatewayTransactionId: String(1,100) }
  type PaymentGatewayEvent is PaymentAuthorized

  handler PaymentGatewayHandler is {
    on cmd: command AuthorizePayment is {
      yield event PaymentAuthorized(
        paymentGatewayTransactionId = cmd.paymentGatewayTransactionId)
    }
    on other is { error "Unexpected message at the PaymentGateway boundary" }
  }
}

The option is external metadata marks the context as externally implemented. Only the interface is modeled.

Appears in: External Contexts — PaymentGateway, NotificationService, HRSystem, AccountingSystem, PrintingService, PhotographyService.

Epics / Use Cases

Epics capture user journeys across contexts:

epic KitchenWorkflow is {
  user Chef wants to "keep the ticket queue moving"
    so that "food leaves the pass while it is hot"

  case AssignAndPrepare is {
    user Chef wants to "assign a ticket to a station"
      so that "a cook can start on it"
    // A user interacts ONLY at the application boundary.
    step focus user Chef on group RestaurantApp.KitchenScreen
    step take input RestaurantApp.KitchenScreen.AssignStationInput from user Chef
    step show output RestaurantApp.KitchenScreen.TicketQueueDisplay to user Chef
  } with {
    briefly "Assign and prepare"
    described as {
      |The Chef assigns a ticket to a station and watches the queue.
    }
  }
} with {
  briefly "Kitchen workflow"
  described as {
    |Ticket intake through station assignment, preparation and approval.
  }
}

Epics reference user definitions and trace step sequences through contexts. The wants ... so that syntax captures the user's goal and motivation.

Appears in: Domain Model — four epics: DineInExperience, OnlineOrderJourney, KitchenWorkflow, LoyaltyEnrollment.

Summary Table

Pattern RIDDL Construct Count in Model
Entity Lifecycle entity, command, event, state, handler 13 entities
Type System type, any of, records, many, optional ~40 types
Repository repository, schema, index 11 repositories
Projector / CQRS projector, updates, event handlers 5 projectors
Adaptor adaptor to, adaptor from 14 adaptors
External Context option is external 6 contexts
Epic epic, case, step, user 4 epics