I Foundations

The Component

A component is an isolated, stateless unit of application logic that combines a declaration of entity state with an implementation of behavior. It exclusively manages the persisted state of one entity type. It can be deployed in its own composition or share a composition with other components.

§State

An entity schema describes the structure of the state managed by a component. It defines one kind of entity; each entity object is an individual instance with its own identity and state. For example, an orders component declares the state of an order:

name: orders

entity:
  properties:
    customer:
      type: string
    status:
      type: string
      enum: [pending, approved, cancelled]
    total:
      type: integer
      minimum: 0
  required: [customer, status, total]

This declaration describes valid order state: every order belongs to a customer, has one of the three statuses, and has a non-negative total. It does not describe a database table or a storage client. The runtime maps that state to the configured storage.

Only the component can access the entity state it manages. Other components interact with it through operations and events; they do not reach into its storage. This keeps the rules governing orders together with the state those rules govern.

§Behavior

§Operations

An operation is a unit of component behavior. Its execution is described by three phases:

  1. Retrieve — acquire the current entity state.
  2. Run — execute the operation’s implementation.
  3. Commit — persist the resulting changes to entity state.

The application implements Run; the runtime handles Retrieve and Commit. The operation’s type determines which phases it uses:

TypePhasesRelationship to state
TransitionRetrieve → Run → CommitReads and changes current state.
ObservationRetrieve → RunReads state without committing changes.
AssignmentRun → CommitBuilds a changeset without retrieving current state.

§Transition

Change the current state

A transition receives the current state of an entity and can modify it. An approval operation can inspect an order before deciding whether to change its status:

export async function transition (input: Input, entry: State) {
  if (entry.status !== 'pending')
    return new Error('NOT_PENDING')

  entry.status = 'approved'
}

The runtime retrieves the order, passes its state to the function, and persists the modified state when the operation succeeds. A returned error rejects the operation without committing its changes; it is an answer the operation declares, not a failure (see Replies). The function’s return value is its reply; state changes are made to the supplied order object.

The runtime also checks for concurrent changes. A transition can be configured to retry against fresh state if the order changed while it was being processed. The implementation expresses the business decision; the runtime coordinates the read and write around it.

§Observation

Read the current state

An observation receives state and derives a reply from it:

export async function observation (input: Input, entry: Entity) {
  return entry.status
}

The runtime retrieves the order before the function runs. It does not persist changes made by an observation. Reading an order’s status therefore has no state-changing effect.

§Assignment

Describe a change

An assignment receives a changeset instead of the current entity state. The changeset starts empty, and the operation adds the values it wants to assign:

export async function assignment (input: Input, changeset: Partial<Entity>) {
  changeset.total = input.total
}

The runtime applies those changes to the selected entity. Properties omitted from the changeset are left unchanged. The function does not receive the existing total, customer, or status.

An assignment fits a change whose values can be determined from the input. If the decision depends on current state — such as approving only a pending order — it belongs in a transition.

These three types define an operation’s relationship to state. Two more — computations and effects — cover logic that manages no state of its own; see Operations.

§Context

The gateway to the runtime

The operation context gives business logic access to the runtime: it can call other components and read configuration through the same argument that the runtime supplies to the operation.

export async function observation (input: Input, entry: Entity, context: Context) {
  const balance = await context.remote.billing.balance({
    query: { id: entry.customer }
  })

  return {
    total: entry.total,
    balance,
    limit: context.configuration.limit
  }
}

Here, the operation asks billing for a customer’s balance and reads a configured limit. It names the component and operation it needs, without knowing where they run or how to reach them. The context connects the operation to those capabilities while keeping their mechanics outside its implementation.

§Events

Publishing and receiving changes

Operations let one component ask another to do something. Events let a component announce something that has happened, so other components can react independently.

Publishing. A component defines an event as a consequence of a state change. For example, an orders.approved event can be conditioned on an order becoming approved:

export function condition (event: Event<Entity>) {
  return event.origin?.status !== 'approved' && event.state.status === 'approved'
}

event.origin is the state before the change; event.state is the state afterward. When a committed change meets the condition, the runtime publishes the event. By default, its payload is the resulting entity state. The operation changes the order; it does not send the event itself.

Receiving. A receiver connects an event to an operation of the receiving component. A billing component can react to order approval by invoking its charge operation:

receivers:
  orders.approved: charge

A receiver can translate the event payload into the request its operation expects:

export function request (order: { id: string, total: number }) {
  return { input: { order: order.id, total: order.total } }
}

The receiving operation runs under the same rules as one called directly. Each component keeps control of its own entity state: orders announces approval, and billing decides how to react. The publisher does not need to know which components receive its events.

Search
Search the documentation