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
| Thing | Casing | Example |
|---|---|---|
| modules | lower_snake_case | sales |
| entities | UpperCamelCase | Customer |
| attributes | lowerCamelCase | companyName |
| associations | UpperCamelCase | Customer |
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_case — companyName is the company_name column,
and that is the name you see in API responses too.
Types
| Type | Parameters | Notes |
|---|---|---|
string | maxLength | Bounded text |
text | — | Unbounded |
integer | min, max | 32-bit |
long | min, max | 64-bit |
decimal | precision, scale | The only correct type for money |
boolean | — | |
datetime | — | RFC 3339, stored as timestamptz |
date | — | YYYY-MM-DD |
enum | values | The 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.
referenceSetcannot 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 areferenceto each side, which also gives you somewhere to put the attributes such a relationship usually turns out to need.
onDelete is the interesting field:
| Value | What happens when the target is deleted |
|---|---|
restrict | The delete is refused while references remain. Default. |
cascade | This row goes too. |
setNull | The 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.