Domain events

An event is a fact an aggregate emits when something happens to it — an order was placed, an order was paid. Events are how one part of the system announces a change without knowing who reacts to it: the emitter states a fact; subscribers elsewhere decide what to do with it. Only aggregate roots emit events, and each event names the aggregate it belongs to.

Declaring an event

An event names its aggregate, how it is triggered, how it is delivered, and the payload it carries:

sub-domains/ordering/events/order.vishwakarma
event OrderPlaced {
  aggregate Order
  trigger   custom
  delivery  signal
  topic     "ordering.order.placed"
  payload   "this.id"
  doc       "A customer placed an order."
  systems   [OrderingSystem]

  order_id    uuid
  customer_id uuid
}
  • aggregate Order binds the event to its emitting aggregate root.
  • topic is the channel subscribers listen on.
  • payload is an expression selecting what the event carries.
  • The fields (order_id, customer_id) form the event's own message shape.

Two ways an event fires

The trigger clause decides when the event is emitted, and Chowk shows both styles on the same aggregate.

Explicit (custom) — the event is emitted deliberately by a command. OrderPlaced above is custom, and the PlaceOrder command announces it with emits OrderPlaced. Use custom when the fact is not a plain lifecycle transition.

Lifecycle (on_update) — the framework emits the event automatically on a lifecycle change, optionally filtered by a when guard. OrderPaid fires whenever an order update lands the row in PAID:

sub-domains/ordering/events/order.vishwakarma
event OrderPaid {
  aggregate Order
  trigger   on_update
  when      "this.status == 'PAID'"
  delivery  signal
  topic     "ordering.order.paid"
  payload   "this.id"
  doc       "An order was paid."
  systems   [OrderingSystem]

  order_id    uuid
  customer_id uuid
}

No command has to remember to emit it — marking an order paid is what triggers it, and the when guard ensures it only fires on the transition into PAID. The available triggers are on_create, on_update, on_delete, on_restore, and custom.

Events decouple the sub-domains

An event is only half a story — its value is that other sub-domains react to it without the emitter knowing. Chowk's fulfillment opens a shipment in response to OrderPlaced, through a subscription:

sub-domains/fulfillment/subscriptions/fulfillment.vishwakarma
subscription StartFulfillmentOnOrderPlaced {
  on      ordering.v1.OrderPlaced
  command StartFulfillment
  systems [FulfillmentSystem]
  doc     "On OrderPlaced, open a shipment for the order."

  input {
    order_id: "event.order_id"
  }
}

The flow is fully decoupled: PlaceOrder (in ordering) emits OrderPlaced; the event travels through the platform's delivery pipeline; this subscription (in fulfillment) maps the payload into StartFulfillment and opens a shipment. Ordering never calls fulfillment — it announces a fact, and fulfillment listens. New reactions (a payments sub-domain, an analytics feed) subscribe to the same event without touching the ordering model at all.

flowchart LR
  A[PlaceOrder<br/>ordering] -->|emits| E((OrderPlaced))
  E -->|subscription| B[StartFulfillment<br/>fulfillment]
  E -.->|future subscriber| C[payments · analytics]

This is the seam between the write side and everything that reacts to it: an aggregate records a fact as an event, and the reactions are declared separately as subscriptions — so the thing that changes and the things that respond to it stay independent.