Events and Behavior
Thinai models application-service operations, aggregate behavior, domain and
integration events, and user-started journeys. Behavior specifications remain
descriptive; structured clauses such as invokes, emits, and transition
provide navigable semantic relationships.
Quoted specifications can link to methods, parameters, repositories, domain
types, and fields by surrounding a qualified name with [[ and ]]. A link
identifies a model element; it does not evaluate an expression or invoke the method. Plain
quoted specifications remain unlinked text. Use triple double quotes for multiline
specification blocks, or keep single or double quotes; the formatter normalizes
the common outer indentation while preserving relative indentation, line breaks,
linked references, and the delimiters you chose.
Use invariant for a domain rule that should hold for the relevant model
element, precondition for what must hold before an operation, and
postcondition for what the operation guarantees when it succeeds. Use
behavior for a general human-readable description that does not frame the
statement as a condition or guarantee. These clauses are optional and
descriptive: Thinai does not execute them or verify that their text is true.
Prefer not to repeat relationships already expressed by structured clauses such
as invokes, emits, or transition.
Aggregate Effects and Application Services
Section titled “Aggregate Effects and Application Services”An ordinary application-service method is a command operation. It coordinates aggregate-root behavior; root and child entity methods own their respective state changes, and the aggregate root protects invariants across the aggregate. Application services may also invoke domain-service methods for domain policies or calculations, but should invoke the aggregate root directly for state changes. In Thinai, domain services cannot invoke aggregate-owned methods, and aggregate-owned methods cannot invoke domain services; this keeps both directions of the invocation boundary explicit. Domain events are emitted by aggregate-root behavior:
bounded context Ordering
in bounded context Ordering { aggregate Order { root entity Order { method place() { emits OrderPlaced transition status from Draft to Placed }
identifier id: OrderId status: OrderStatus initial Draft }
value object OrderId { raw: string }
value object OrderLine { sku: string }
enum OrderStatus { Draft, Placed } }
repository OrderRepository { manages Order.Order }
factory OrderFactory { method createOrder() returns Order.Order }
event OrderPlaced { orderId: OrderId }
application service OrderApplication { method placeOrder(orderId: OrderId, items: List<OrderLine>) { precondition "The order contains at least one item." invokes Order.Order.place when "The order is ready to be placed." behavior "Place the order while its status is [[Order.Order.status]]." }
method createOrder() { invokes OrderFactory.createOrder } }}Factories and repositories are declared beside aggregates in the bounded context. A factory creates exactly one aggregate root: all of its valid methods return that same root, and different roots need separate factories. Behavior specifications link to the factory method rather than to a constructor or the factory declaration itself.
Aggregate-root methods may emit multiple domain events. Emissions are listed in order. Application-service methods coordinate aggregate-root behavior, and may invoke a factory method when creation is part of the use case. Thinai describes these domain relationships; it does not require a load/save sequence or declare transaction mechanics.
An emits clause may list multiple events and may have a descriptive when
guard. The guard applies to every event in that clause; use separate emits
clauses when the events have different conditions. A transition can similarly
group changes to multiple enum fields declared on the same entity under one
descriptive when guard. Each field change is validated against its own enum.
An invokes clause may also have a descriptive when guard. It describes when
the invocation is intended to occur, but does not change which calls are allowed
by Thinai’s invocation rules or turn the relationship into executable behavior.
Grouping records coordinated intent, not an implementation-level atomicity
guarantee. Guards are prose, not evaluated conditions; state-graph reachability
therefore treats guarded transitions as possible edges. The when guard on a
journey step is separate and describes a condition for invoking the command.
Integration Events and Event-Triggered Methods
Section titled “Integration Events and Event-Triggered Methods”Domain events are private to their bounded context. To publish a fact across a context boundary, an application-service method can receive a domain event and emit one or more integration events. The integration-event declaration defines the public schema; the method’s behavior text describes any payload mapping. Event payloads carry stable value data rather than entity instances or other aggregate-owned types. Validation includes collection element and value types and nested value-object shapes, not only direct field types. An IntegrationEvent nested in a module must be explicitly exposed before another bounded context can consume it. When the event is in a module and a direct payload type lives in a sibling module, the event module must depend on that payload module and the payload must still be exposed; nested payload types follow their outer type’s module dependencies.
For CQRS reads, a read model is an optional query-result shape, not necessarily
a materialized projection or separate database. A query repository is an
optional logical read boundary and may query the same physical tables as the
write side. reads from may name one or more aggregate tables, or a published
query-service operation, and may carry a descriptive when guard. A projector
is needed only when the model uses an event-driven projection. Query services may
read through query repositories, which may source aggregate tables, and must
return data, not entity instances. Projectors do not call application services or
change aggregates:
bounded context Ordering
in bounded context Ordering { event OrderPlaced { orderId: string }
read model OrderSummary { orderId: string status: string }
query repository OrderSummaryRepository { serves OrderSummary }
projector OrderSummaryProjector { method onOrderPlaced(source: OrderPlaced) { projects OrderSummary behavior "Copy [[OrderPlaced.orderId]] into [[OrderSummary.orderId]] via [[OrderSummaryRepository]]." } }}bounded context Ordering
in bounded context Ordering { event OrderPlaced { orderId: string }
module Contracts { exposes OrderPlacedV1
integration event OrderPlacedV1 { orderId: string } }
application service OrderApplication { method publishOrderPlaced(source: OrderPlaced) { emits Contracts.OrderPlacedV1 behavior "Map the internal event to the public contract." } }}A method whose parameter type is a DomainEvent or IntegrationEvent is event-triggered. An event handler has exactly one event-typed parameter and no additional parameters. A consumer’s method can receive an imported integration event and invoke a local command method. Only an event handler may dispatch to a command in another application service; an ordinary command method does not call another application service’s command.
bounded context Shipping
in bounded context Shipping { application service ShipmentApplication { method createShipment(shipmentId: ShipmentId) { invokes Shipment.Shipment.start }
method onOrderPlaced(source: Ordering.Contracts.OrderPlacedV1) { invokes createShipment behavior "Map the incoming event to the local command input." } }}Keep the imported contract at this boundary; use local types in ordinary command methods and aggregates. Do not model arbitrary payload transformations as expressions. Integration-event publication is an application-layer reaction to a local DomainEvent. Delivery ordering, broker behavior, and outbox mechanics are outside the DSL.
Cross-context integration-event use requires an explicit bounded-context
import and an imported context map with an authorized, correctly directed
relationship. The customer-to-supplier arrow points from the downstream
consumer to the upstream producer. This relationship authorizes the customer to
consume the supplier’s IntegrationEvents; a partnership authorizes consumption
in either direction. integrationPatterns are descriptive labels and do not
affect authorization or assert implementation conformance. Separate-ways and
shared-kernel relationships do not authorize event handling. Context maps do
not make domain events public.
Journeys
Section titled “Journeys”A journey is a top-level, descriptive EventStorming timeline of non-event
application-service commands. Any application-service method without an event
parameter is treated as a command; a method with an event parameter is an event
handler. A journey can span bounded contexts. Each command remains
owned by its bounded context; the journey does not imply a synchronous remote
call or shared transaction. Background event handoffs are modeled by the
contexts’ events and handlers, not as journey steps. Queries and event-triggered
handlers are not journey commands. Optional with input descriptions and when
guards are linked prose, not executable data flow or conditions; each command
must still enforce its own invariant:
actor Customer
journey PlaceOrder { by Customer { invoke Ordering.OrderApplication.placeOrder with "orderId from [[Ordering.OrderSummary.orderId]] and items from the selected order lines" when "The eligibility result [[Ordering.OrderQueries.canPlaceOrder.result.eligible]] allows this order." }}Actors are declared once at the top level and referenced in by Actor { ... }
blocks, which group commands initiated by that actor. Journey order is
significant. A query result linked from when is not bound to a command
argument, invoked by the journey, or evaluated by the DSL. Emitted events are
derived from the invoked methods and need not be repeated in the journey.
Journey timeline rules:
- Current inputs and results of earlier steps are visible to later steps.
- The current command’s result is not visible to its own step.
- A command invoked later in the journey is not visible from an earlier step.
- Reference a prior command result through its qualified path, for example
[[Ordering.OrderApplication.placeOrder.result.orderId]]. resultis reserved on methods with a result and on application-service commands.- Reusing the same command more than once makes result references ambiguous and produces a warning.
See references and paths for path and quoting rules.
See the Order Fulfillment example for a multi-file model with an integration-event consumer.