Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Modelling a domain

domain.yaml is the shape of the thing. Every entity becomes a table, every attribute a column, every association a foreign key or a junction table.

schemaVersion: 1
module: sales

entities:
  - name: Customer
    description: A company that places orders.
    attributes:
      - { name: companyName, type: string, maxLength: 120, required: true }
      - {
          name: tier,
          type: enum,
          values: [Bronze, Silver, Gold],
          default: Bronze,
        }
      - { name: creditLimit, type: decimal, precision: 12, scale: 2 }
      - { name: active, type: boolean, default: true }
    indexes:
      - { attributes: [companyName], unique: true }

Naming is enforced, not suggested

ThingCasingExample
moduleslower_snake_casesales
entitiesUpperCamelCaseCustomer
attributeslowerCamelCasecompanyName
associationsUpperCamelCaseCustomer

Validation rejects anything else. A format that permits two spellings gets both, and then every reader has to know which one this codebase chose.

Names reach SQL as snake_casecompanyName is the company_name column, and that is the name you see in API responses too.

Types

TypeParametersNotes
stringmaxLengthBounded text
textUnbounded
integermin, max32-bit
longmin, max64-bit
decimalprecision, scaleThe only correct type for money
boolean
datetimeRFC 3339, stored as timestamptz
dateYYYY-MM-DD
enumvaluesThe only labellable type besides boolean

There is deliberately no float. A binary float cannot represent 0.10, and the one place people reach for a float is the one place it is unacceptable. A decimal default is written quoted — default: "0.00" — because YAML would otherwise parse it as a float and lose both the precision and the trailing zeros that carry the scale.

Associations

associations:
  - name: Customer
    kind: reference # a foreign key on this table
    target: sales.Customer
    required: true
    onDelete: restrict

kind is either reference — a foreign key on this table — or referenceSet, a many-to-many junction table.

referenceSet cannot be written yet. The junction table is created and nothing fills it: there is no API for adding or removing a link. Model a many-to-many as an entity of its own for now, with a reference to each side, which also gives you somewhere to put the attributes such a relationship usually turns out to need.

onDelete is the interesting field:

ValueWhat happens when the target is deleted
restrictThe delete is refused while references remain. Default.
cascadeThis row goes too.
setNullThe reference is cleared. Only if not required.

restrict is the default deliberately: deleting a customer who has orders is nearly always a mistake, and the cases where it is not should have to say so.

A cascade is walked by the runtime, not left to the database — each row it reaches is authorized in its own right, so a cascade that would remove something the caller may not delete refuses the whole request.

Renaming things

Say so. There are no hidden identifiers:

- { name: emailAddress, previousName: email, type: string }

Without that, renaming is indistinguishable from dropping one column and adding another — and dropped columns are renamed out of the way rather than deleted, so the data would survive invisibly. Worse than losing it, because nobody notices for a month.

If the runtime cannot tell a rename from a delete-plus-add, it refuses the migration and says so. No flag overrides that: --allow-drop means “I meant to lose this”, and a refusal means the planner cannot tell what you meant.

System attributes

Every entity gets these. You do not declare them and cannot write them:

id (UUIDv7), createdAt, changedAt, createdBy, changedBy, version.

createdBy and changedBy are the token’s subject, not anything in the request body — an audit trail a caller can write is not an audit trail. version is what ETag carries.

Checking it

runtime model validate myapp   # three passes: parse, resolve, check
runtime db plan myapp          # what provisioning would do

db plan is safe to run against production, and worth running there: it names every change that would take a lock or lose data.