Aggregates and entities

An entity is a persisted thing your business keeps — a product, an order, a shipment. Every entity in Vishwakarma is an aggregate root: an identity-bearing object that owns a consistency boundary. You declare the noun, its fields, its indexes, and its relationships, and the platform generates the table, the repository, the API, and the typed client. The running example throughout is Chowk, a small marketplace.

An aggregate root

The aggregate_root modifier marks an entity as the entry point to its own consistency boundary — the thing commands write to and events are emitted from. Here is Chowk's order:

sub-domains/ordering/entities/order.vishwakarma
entity Order aggregate_root global {
  doc     "A customer's order — the root of the checkout lifecycle."
  systems [OrderingSystem]
  audit   timestamps

  indexes [
    { name "uk_order_id"       kind unique   fields [id] }
    { name "ix_order_customer" kind standard fields [customer_id] }
  ]

  id               uuid         pk filterable[eq]
  customer_id      uuid         required immutable filterable[eq]
  status           OrderStatus  required
  total            Money        required
  shipping_address Address      required
  gift_message     GiftMessage?
  created_at       timestamp    immutable sortable
  updated_at       timestamp
}

Two modifiers sit before the brace. aggregate_root is the boundary marker. global means the entity is not tenant-isolated — one shared dataset rather than per-tenant rows; a tenant-scoped entity drops global and declares isolation_axes [tenant_id] instead (see Sub-domains, isolation, and the caller). audit timestamps asks the framework to manage created_at/updated_at, and indexes declares the access paths — a unique index on id, a lookup index on customer_id.

The boundary is composed by reference, not by nesting

There is no nested child-entity construct. An order line is conceptually part of an order, but it is its own aggregate root, tied to its order by a foreign-key relationship:

sub-domains/ordering/entities/order_line.vishwakarma
entity OrderLine aggregate_root global {
  doc        "A single line on an order — a product and a quantity. Owned by its Order."
  systems    [OrderingSystem]
  audit      timestamps
  invariants [OrderOpenToAddLine]

  indexes [
    { name "uk_order_line_id"    kind unique   fields [id] }
    { name "ix_order_line_order" kind standard fields [order_id] }
  ]

  id         uuid  pk filterable[eq]
  order_id   uuid  required immutable filterable[eq] relates Order many_to_one on_delete cascade
  product_id uuid  required immutable filterable[eq]
  quantity   int32 required
  unit_price Money required
}

The line composes into the order through relates Order many_to_one on_delete cascade. The cascade is what makes the ownership real: delete the order and its lines go with it. The immutable on order_id says a line never re-parents; the ix_order_line_order index is the "list the lines of this order" path. This is how an aggregate is assembled — separate roots joined by an owning foreign key, not lexical containment.

Owning versus referencing: on_delete

Not every relationship is ownership. A shipment references the order it fulfils, but it does not own it — you should not be able to delete an order out from under a shipment that ships it. That intent is expressed with on_delete restrict:

sub-domains/fulfillment/entities/shipment.vishwakarma
entity Shipment aggregate_root global {
  doc     "A shipment opened to fulfil an order."
  systems [FulfillmentSystem]
  audit   timestamps

  indexes [
    { name "uk_shipment_id"    kind unique   fields [id] }
    { name "ix_shipment_order" kind standard fields [order_id] }
  ]

  id         uuid           pk filterable[eq]
  order_id   uuid           required immutable filterable[eq] relates Order many_to_one on_delete restrict
  status     ShipmentStatus required
  created_at timestamp      immutable sortable
  updated_at timestamp
}

OrderLine and Shipment both relate to Order, but they mean different things — and the single word after on_delete carries the whole difference:

Relationshipon_deleteMeaning
OrderLine → OrdercascadeOwned. The line is part of the order; deleting the order deletes its lines.
Shipment → OrderrestrictReferenced. The shipment points at the order; the order cannot be deleted while a shipment references it.

The other behaviours are set_null (clear a nullable foreign key when the target goes away) and no_action (defer to the database). Choosing the right one is a modeling decision — you are stating whether one thing is part of another or merely points at it.

What you get for declaring it

From these declarations the platform generates the Postgres table with its columns, indexes, and foreign keys; the repository to load and save it; the HTTP surface; and the typed client. You never write the schema, the data-access layer, or the wire types — they are derived from the entity, and they stay correct as the entity changes. The next page covers identity and the field vocabulary an entity is built from.