Type to search pages and rules.

Browse pages

Domain Model

Domain Model states the words and things inside the bounded context: the modules, the glossary, the aggregates with their lifecycles, the sagas coordinating them, the rules that hold, and the domain events. Agents write these records from Intent, and code carries them. The Behavior overview maps the five Behavior pages, and the record language page states how each field, value and record name is written.

Markdown22 provisions

Stack: any Layer: domain

Concepts#

Bounded Context and Module#

The product speaks one business language inside one deployment boundary, as the scope page states. Modules organize that language into areas; they stay areas of one language and one deployment, never languages or deployments of their own. A module is one business area: a page at intent/modules/<m>/module.yaml carrying a summary and the dependsOn links to the modules it relies on, in the home the model page names. behavior/modules/<m>/ holds the same module name, so Behavior folders follow Intent. Inside behavior/modules/<m>/, a subfolder is either a fixed type folder from the closed set or an aggregate holding aggregate.yaml. A module with one aggregate of the module's own name may stay flat. Example, Known Limits, and Notes live in the explanation file beside the record file.

FieldMeaning
summaryWhat the area covers, in one paragraph.
dependsOnThe modules this module relies on.

Glossary#

The glossary is the project ubiquitous language: the one word or phrase the project uses for each concept, with its meaning. Each term carries layer: intent for business words, behavior for design words such as aggregate, refusal, reaction, and job, and implementation for design words that live in code, such as a read model. Each term lists in aliases the established synonyms its readers know, so industry readers find LiteSpec own words: a refusal is a rejection or error, a reaction is a policy, a saga is a process manager, a compensation is an undo. The rejected list still bars the synonyms the project refuses, as the authoring page states.

FieldMeaning
term, meaningThe one word and what it means.
layerintent, behavior, or implementation.
aliasesEstablished synonyms readers know.
rejectedSynonyms barred in their scope.
broader, narrower, valuesThe wider and narrower terms, and the closed values.
labelsThe term in each interface language beside English, keyed by its two-letter language code.

Aggregates#

An aggregate is one thing the product keeps consistent and changes in one step, through its root. Each aggregate declares one kind. The aggregate identifier carries its module, so containment follows the identifier and needs no link field.

KindMeaning
entityOne thing kept consistent through its root, with states, transitions, and data.
sagaA coordinator whose steps run use cases on other aggregates, each with its compensation.
storeState no aggregate invariant guards, such as a library-owned table, with a named owner.

An aggregate lists in parts the kebab-case names of the child entities and value objects inside it that use cases act on, such as line, hold, and note. The use-case noun check reads each part as a known word. Example, Known Limits, and Notes live in the explanation file beside the record file.

transitions lists every allowed change with the use case moving it: each entry names the state left in from, the state entered in to, and the moving use case in by. A creation, the change that brings an instance into being, writes - as the state left. data lists the fields the aggregate keeps, each with name, meaning, classification, and data class. Each data class declares how its fields erase:

FieldMeaning
name, classification, fieldsThe class, its public, internal, personal, or sensitive handling, and its fields.
purposeWhy the data is kept.
onErasureerase, pseudonymize, anonymize, retain, or exempt.
basisstatute, business, or engineering.
periodThe retention period as an ISO duration, or { "mode": "none", "reason": "..." }.
enforcedByreaction/..., job/..., record-expiry, store-ttl, or query-filter.
reachown-tenant, subject-wide, or platform-wide.
exceptionsPer-field erasure positions differing from the class.
reason, basedOnThe reason of an exemption, and the Intent records accepting retained or exempt personal data.

Sagas#

A saga is an aggregate that coordinates work across aggregates: a refund, a resale, a hiring pipeline. Its steps run in order, each running one use case on another aggregate; each step carries an id, the use case in runs, its compensation, and an optional guard clause completing "when ...". Its exits name the ways out, each a past participle or adjective; its finishedWhen names the observable end; its drivenBy names what moves it; and its cursor names where resume state lives.

FieldMeaning
stepsOrdered { id, runs, compensation, guard } entries; runs names use-case/...; compensation is { "mode": "use-case", "useCase": "use-case/..." } or { "mode": "none", "reason": "..." }; guard is optional.
exits{ name, trigger, then } entries; the name is a past participle or adjective; a trigger is a condition, a decision by <actor>, or a due <duration>.
finishedWhenThe observable end, in one sentence.
drivenBy{ "job": "job/<...>" }, { "reaction": "reaction/<...>" }, or { "person": "<actor or role id>", "runbook": "runbook/<...>", "dueWithin": "<ISO duration>" } entries.
cursorWhere resume state lives.

Rules#

A rule states what holds, with its kind and its statement. A rule names what it constrains in governs, the refusal its breach produces in refusal, the requirements it carries in fulfills, and the constraints and research behind it in basedOn. A rule an organization chooses, such as rounding, consent, or seller responsibility, is a requirement or a constraint in Intent, not a Behavior rule. An invariant an aggregate enforces lives under its aggregate or its module. A cross-aggregate rule spans aggregates and lives as an entry on a rule page; a module rule lives in behavior/modules/<m>/rules/; module.yaml holds no rules. A rule page carries a summary stating why its rules sit together, and its explanation file holds Notes. No rule page restates another record type. A rule identifier keeps its segment when the rule moves; only identifiers whose first segment names a module change with a module move.

KindMeaning
invariantHolds after every change.
preconditionGuards the behavior before it runs.
calculationDerives a value, proven by an examples table.
cross-aggregateSpans aggregates and lives on a rule page.
atomicityNames one atomic write group in binds.
reservationHolds a claim until a use case releases it, named in releasedBy as a use-case id, or { "mode": "never", "reason": "..." } with its cost.
commercial-dataFreezes a commercial value, with frozenWhen and retainedIn.
defaultStates a model default.

A query and a command showing and recording one value both cite the same calculation rule in computedBy. Code carries a mark naming the rule, and never the reverse.

Domain Events#

A domain event is an immutable statement that a business fact completed, named in the past tense, with its meaning and the facts it carries. Each event is raised through the root of its aggregate, and each event stays inside the product. Facts crossing the boundary are integration events.

FieldMeaning
meaningWhat happened, in one sentence.
carriesThe facts arriving with the event.

Standards#

Define Every Behavior Term in the Glossary#

ruleruleguidance
litespec/core/domain-model#define-every-behavior-term-in-the-glossary
Statement

Each term a Behavior record names MUST have a glossary entry stating its meaning.

Applies toartifact: record:term

Why

Records and code share one language only when each word is defined once: a term used without an entry means whatever each reader guesses. The entry fixes the meaning, the rejected synonyms, and the wider and narrower terms, so reviewers and checks read the same word the same way.

Example

The term receipt carries the meaning "proof of payment accepted for an expense" in the glossary, so every record naming a receipt means that proof.

Checka reviewer answers

Reviewed forWhich terms do the Behavior records name without a glossary entry stating their meaning? Look at the records beside the project glossary.

Marked guidance: a reviewer answers this question.

Mark Each Term with Its Layer#

rulerulecheckable
litespec/core/domain-model#mark-each-term-with-its-layer
Statement

Each glossary term MUST carry its record layer, intent, behavior, or implementation, with the established synonyms its readers know in aliases.

Applies toartifact: record:term

Why

Business words, design words, and implementation words age on different clocks: a business term moves when the market moves, a design term moves when the model moves, and an implementation term moves when the code moves. The record layer tells each reader which clock the word follows, and the aliases let industry readers find LiteSpec own words without learning a second vocabulary first.

Example

The term saga carries layer: behavior with aliases: ["process manager"].

Checkchecked by a tool

Checked bylitespec model check fails a glossary term without layer, and each behavior term lists the synonyms its readers know in aliases.

Marked checkable: the tool named here checks it.

State Every Module Dependency#

ruleruleguidance
litespec/core/domain-model#state-every-module-dependency
Statement

Each module MUST name every module it depends on in dependsOn.

Applies toartifact: record:module

Why

A module relies on the language and behavior of others, and each reliance shapes change: editing a module ripples into everything depending on it. Naming every reliance in dependsOn shows that ripple as data instead of leaving it for readers to reconstruct from prose.

Checka reviewer answers

Reviewed forWhich modules rely on a module that dependsOn does not name? Look at the module pages beside the modules they rely on.

Marked guidance: a reviewer answers this question.

Declare the Kind of Every Aggregate#

rulerulecheckable
litespec/core/domain-model#declare-the-kind-of-every-aggregate
Statement

Each aggregate MUST declare its kind: entity, saga, or store.

Applies toartifact: aggregate

Why

The kind decides what the page carries: an entity carries a lifecycle, a saga carries steps with compensations, exits, and drivenBy, and a store carries an owner with no guarded invariant. Without the kind, a thing kept consistent reads the same as a table the product only holds.

Example

The refund saga declares kind: saga, and the suppression list declares kind: store.

Checkchecked by a tool

Checked bylitespec model check fails an aggregate page without kind or with an unknown kind.

Marked checkable: the tool named here checks it.

Name the Owner of Every Store#

rulerulecheckable
litespec/core/domain-model#name-the-owner-of-every-store
Statement

Each store MUST name its owning system or module in owner.

Applies toartifact: aggregate

Why

A store holds state no aggregate invariant guards, so nothing in the model explains who answers for its shape: a library-owned table, a suppression list, or a delivery log. Naming the owner puts one party behind it, and keeps staff actions on it as ordinary use cases instead of unwritten operations.

Checkchecked by a tool

Checked bylitespec model check fails a store aggregate page without owner.

Marked checkable: the tool named here checks it.

Model Each Aggregate Lifecycle with States#

rulerulecheckable
litespec/core/domain-model#model-each-aggregate-lifecycle-with-states
Statement

Each entity and each saga MUST declare its lifecycle as states entries with a name and a meaning.

Applies toartifact: aggregate

Why

A bare state name tells a reviewer nothing: rejected without its meaning hides what rejection costs and allows. A name with a meaning makes each lifecycle step reviewable on the page, and gives transitions states worth connecting. An entity tracks what the thing is, and a saga tracks where its coordination stands. A store carries no lifecycle, so it carries no states.

Example

The hold queue entity declares Requested, Ready, Collected, Cancelled, and Lapsed, each with one sentence saying what the hold awaits or has received.

Checkchecked by a tool

Checked bylitespec model check fails an entity or saga page without states entries carrying a name and a meaning.

Marked checkable: the tool named here checks it.

Leave States off Every Store#

rulerulecheckable
litespec/core/domain-model#leave-states-off-every-store
Statement

A store MUST carry no states.

Applies toartifact: aggregate

Why

A store holds state no aggregate invariant guards, so a lifecycle on it promises what nothing guards: states claim a reviewed path while any writer moves the rows. The store names its owner instead, as litespec/core/domain-model#name-the-owner-of-every-store states, and staff actions on it stay ordinary use cases.

Example

The suppression list declares kind: store with its owner and no states.

Checkchecked by a tool

Checked bylitespec model check fails a store aggregate carrying states.

Marked checkable: the tool named here checks it.

Connect Every State Through Transitions#

rulerulecheckable
litespec/core/domain-model#connect-every-state-through-transitions
Statement

Each entity and each saga MUST list every allowed state change with its use case in transitions, including the creation that enters its first state.

Applies toartifact: aggregate

Why

States without transitions are a list of names with no path between them: a reviewer sees where an aggregate rests but never how it moves. Listing every allowed change with the use case that moves it shows the whole lifecycle beside the states, and leaves no state reachable only by a path no record names. The state-unreached and transition-unknown checks report a state no transition reaches and a transition naming a missing state or use case.

Checkchecked by a tool

Checked bylitespec model check reports state-unreached (level G) for a state reached by no transition, a creation included, and transition-unknown (level G) for a transition naming a missing state or use case.

Marked checkable: the tool named here checks it.

State Each Rule with Its Kind#

rulerulecheckable
litespec/core/domain-model#state-each-rule-with-its-kind
Statement

Each rule MUST state its kind and its statement.

Applies toartifact: record:rule

Why

The kind decides how the rule is proved: an invariant on every change, a precondition before the behavior, a calculation through its examples, a cross-aggregate rule where aggregates meet, an atomicity rule naming its bound group, a reservation through its release, a commercial-data rule through its frozen value, a default through the value the model holds. A rule without a kind leaves each reader to prove it a different way, and the checks no place to confirm it.

Example

rule/expense.reimbursed-once is an invariant stating "an expense is reimbursed at most once", checked on every reimbursement.

Checkchecked by a tool

Checked bylitespec model check fails a rule entry without kind or statement.

Marked checkable: the tool named here checks it.

Bind Every Atomic Write Group in Its Rule#

rulerulecheckable
litespec/core/domain-model#bind-every-atomic-write-group-in-its-rule
Statement

Each atomicity rule MUST name the aggregates it binds in binds.

Applies toartifact: record:rule

Why

An atomic write across aggregates stays allowed only through its rule: the rule states the group and why, such as an order without its hold overselling, and the use case commits exactly that group, as the use-cases page states. A group above 4 aggregates is a saga candidate, reported at level A.

Example

The hold rule binds the hold and the copy aggregates, so a hold without its copy never stands.

Checkchecked by a tool

Checked bylitespec model check reports atomic-group-drift (level G) for an atomic use case whose changes differs from its rule's binds, and atomic-group-invalid (level A) for a group binding more than 4 aggregates.

Marked checkable: the tool named here checks it.

Keep the Negation Word in the Rule Name#

rulerulecheckable
litespec/core/domain-model#keep-the-negation-word-in-the-rule-name
Statement

A rule whose statement says not, no, never, or cannot MUST keep a restricting word in its name: no, never, not, cannot, none, nothing, without, unless, non, refused, refuses, refusal, forbidden, blocked, or rejected, a limit only, or a uniqueness one-...-per-....

Applies toartifact: record:rule

Why

A name that drops the negation reads as its own opposite: a rule named cancel-after-shipment whose statement says "never" allows what it forbids. A restricting word keeps the name a true sentence a reviewer trusts without opening the statement, and one-grant-per-account already says what "an account never holds two grants" says. The rule-name-invalid check reports a rule whose statement negates while its name keeps no restricting word or uniqueness.

Example

The rule reads rule/order.cancel-never-after-shipment.

Checkchecked by a tool

Checked bylitespec model check reports rule-name-invalid for a rule whose statement says not, no, never, or cannot while its name keeps no restricting word or uniqueness.

Marked checkable: the tool named here checks it.

Keep Cross-Aggregate Rules on a Rule Page#

ruleruleguidance
litespec/core/domain-model#keep-cross-aggregate-rules-on-a-rule-page
Statement

A rule spanning aggregates MUST live as an entry on a rule page.

Applies toartifact: record:rule

Why

An aggregate page holds what one aggregate guarantees; a rule spanning aggregates guarantees what no single aggregate owns. Giving such rules their own page keeps each aggregate page a true account of its aggregate, and gathers the shared rules where a reviewer judges them together.

Checka reviewer answers

Reviewed forWhich cross-aggregate rules sit outside a rule page? Look at the rule entries beside their aggregate pages.

Marked guidance: a reviewer answers this question.

Name the Refusal a Broken Rule Produces#

ruleruleguidance
litespec/core/domain-model#name-the-refusal-a-broken-rule-produces
Statement

Each rule whose breach refuses a use case MUST name the refusal in refusal.

Applies toartifact: record:rule

Why

A guarded rule without a named refusal leaves the ending unnamed: callers branch on a refusal the records never predicted. Naming the refusal ties the rule to the result the use case declares, so the breach and the ending stay one fact.

Checka reviewer answers

Reviewed forWhich guarded rules name no refusal in refusal? Look at the rule entries beside the results they refuse.

Marked guidance: a reviewer answers this question.

Release Every Reservation by a Named Use Case#

rulerulecheckable
litespec/core/domain-model#release-every-reservation-by-a-named-use-case
Statement

Each reservation rule MUST name the use case releasing it in releasedBy, or { "mode": "never", "reason": "..." } with its cost.

Applies toartifact: record:rule

Why

A held claim with no release is a leak: places, funds, or names stay locked past their need, and nobody owns freeing them. Naming the releasing use case states the end of the hold beside its start, and { "mode": "never", "reason": "..." } with its cost admits a hold that truly lasts, with the price on record. Persistence claims the added keys and releases the removed ones in the commit that changes the aggregate (litespec/backend/persistence#claim-a-cross-record-key-at-commit). The reservation-release-missing check reports a reservation with no release.

Checkchecked by a tool

Checked bylitespec model check reports reservation-release-missing for a reservation rule whose releasedBy names neither a use case nor { "mode": "never", "reason": "..." } with its cost.

Marked checkable: the tool named here checks it.

Freeze Every Commercial Data Value#

rulerulecheckable
litespec/core/domain-model#freeze-every-commercial-data-value
Statement

Each commercial-data rule MUST name when the value freezes in frozenWhen and where its retained facts live in retainedIn.

Applies toartifact: record:rule

Why

A price, fee, or rate read live changes under settled business: a refund follows today price instead of the price at sale. Naming when the value freezes pins each reading to its moment, and the retained link keeps the earlier values reachable for audit and dispute.

Checkchecked by a tool

Checked bylitespec model check fails a commercial-data rule without frozenWhen or retainedIn.

Marked checkable: the tool named here checks it.

Compute Shared Values from One Calculation Rule#

ruleruleguidance
litespec/core/domain-model#compute-shared-values-from-one-calculation-rule
Statement

A value shown by a query and recorded by a command MUST cite the same calculation rule in computedBy on both use cases.

Applies toartifact: record:use-case

Why

Two computations of one value drift: the screen shows one figure while the ledger records another, and each side cites its own code. Citing the same calculation rule on both use cases keeps one computation behind both faces. Code carries a mark naming the rule, and never the reverse.

Checka reviewer answers

Reviewed forWhich queries and commands computing one value cite different rules in computedBy? Look at the use cases beside the calculation rule.

Marked guidance: a reviewer answers this question.

Tabulate Every Calculation with Examples#

rulerulecheckable
litespec/core/domain-model#tabulate-every-calculation-with-examples
Statement

Each calculation rule MUST be proven by a scenario carrying an examples table.

Applies toartifact: record:rule

Why

A calculation stated only as a sentence hides its edges: reviewers agree on the words while each pictures other numbers. An examples table shows those edges, and the scenario proving the rule pins them as checked facts. The calculation-table-missing check reports a calculation rule with no scenario examples table.

Checkchecked by a tool

Checked bylitespec model check reports calculation-table-missing for a calculation rule with no scenario examples table.

Marked checkable: the tool named here checks it.

State Each Event with Meaning and Carries#

rulerulecheckable
litespec/core/domain-model#state-each-event-with-meaning-and-carries
Statement

Each event MUST state its meaning and the facts it carries.

Applies toartifact: domain-event

Why

An event without a meaning is a name every reaction interprets alone; facts without a carries list are reconstructed from code at each reaction. Stating both on the entry lets each reaction trust what happened and what arrived with it.

Checkchecked by a tool

Checked bylitespec model check fails an event entry without meaning or carries.

Marked checkable: the tool named here checks it.

Name Every Event in the Past Tense#

ruleruleguidance
litespec/core/domain-model#name-every-event-in-the-past-tense
Statement

Each domain event MUST be named in the past tense.

Applies toartifact: domain-event

Why

An event names a fact that completed, so the past tense tells a reader the business already moved: submitted reports, while submit orders. The past tense also pairs each event with the act raising it.

Example

The expense aggregate raises event/expense.submitted for a completed submission.

Checka reviewer answers

Reviewed forWhich event names read as an order rather than a completed fact? Look at the event entries beside their raising acts.

Marked guidance: a reviewer answers this question.

Raise Every Event Through Its Aggregate Root#

ruleruleguidance
litespec/core/domain-model#raise-every-event-through-its-aggregate-root
Statement

Each event MUST be raised through its aggregate root.

Applies toartifact: domain-event

Why

The root is the one type through which outside behavior changes the aggregate, so it sees every change worth announcing. Raising each event there keeps the fact and its announcement in one step, and leaves no event announced for a change the aggregate never made.

Checka reviewer answers

Reviewed forWhich events leave from outside the root of their aggregate? Look at the aggregate code beside the event entries.

Marked guidance: a reviewer answers this question.

Keep Model Invariants in Rules#

ruleruleguidance
litespec/core/domain-model#keep-model-invariants-in-rules
Statement

A model invariant MUST live as a rule, never as a setting.

Applies toartifact: record:rule

Why

A setting tunes; a rule holds. An invariant filed as a setting reads as tunable, and a later operator tunes away what the model guaranteed. Keeping invariants in rules keeps what holds apart from what varies.

Checka reviewer answers

Reviewed forWhich setting pages hold a model invariant? Look at the settings beside the rules.

Marked guidance: a reviewer answers this question.

Conventions#

Name Each Module After Its Capability#

conventiondefaultguidance
litespec/core/domain-model#name-each-module-after-its-capability
Default

Name each module after one capability the software offers, not after a company function or a catch-all, and split a module whose aggregates serve more than one capability into sibling modules.

ReplacementA consumer can replace this default with an explicit local convention.

Applies toartifact: record:module

Why

Modules mark the areas of the bounded context, where a monolith would later split and how a reader finds a use case by module, then aggregate, then use case. A capability name keeps one module cohesive; a company function or a catch-all such as platform, sales, operations, or misc gathers unrelated aggregates that change for unrelated reasons. Splitting such a module into siblings keeps each module one capability with aggregates that serve it.

Example

The finance module holds payout and ledger aggregates serving finance, while a misc module holding lending and catalog aggregates splits into lending and catalog siblings.

Checka reviewer answers

Reviewed forDoes every aggregate in this module serve the capability the module name states? Look at the aggregates beside the module name.

Marked guidance: a reviewer answers this question.