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.
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.
| Field | Meaning |
|---|---|
summary | What the area covers, in one paragraph. |
dependsOn | The 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.
| Field | Meaning |
|---|---|
term, meaning | The one word and what it means. |
layer | intent, behavior, or implementation. |
aliases | Established synonyms readers know. |
rejected | Synonyms barred in their scope. |
broader, narrower, values | The wider and narrower terms, and the closed values. |
labels | The 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.
| Kind | Meaning |
|---|---|
entity | One thing kept consistent through its root, with states, transitions, and data. |
saga | A coordinator whose steps run use cases on other aggregates, each with its compensation. |
store | State 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:
| Field | Meaning |
|---|---|
name, classification, fields | The class, its public, internal, personal, or sensitive handling, and its fields. |
purpose | Why the data is kept. |
onErasure | erase, pseudonymize, anonymize, retain, or exempt. |
basis | statute, business, or engineering. |
period | The retention period as an ISO duration, or { "mode": "none", "reason": "..." }. |
enforcedBy | reaction/..., job/..., record-expiry, store-ttl, or query-filter. |
reach | own-tenant, subject-wide, or platform-wide. |
exceptions | Per-field erasure positions differing from the class. |
reason, basedOn | The 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.
| Field | Meaning |
|---|---|
steps | Ordered { 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>. |
finishedWhen | The observable end, in one sentence. |
drivenBy | { "job": "job/<...>" }, { "reaction": "reaction/<...>" }, or { "person": "<actor or role id>", "runbook": "runbook/<...>", "dueWithin": "<ISO duration>" } entries. |
cursor | Where 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.
| Kind | Meaning |
|---|---|
invariant | Holds after every change. |
precondition | Guards the behavior before it runs. |
calculation | Derives a value, proven by an examples table. |
cross-aggregate | Spans aggregates and lives on a rule page. |
atomicity | Names one atomic write group in binds. |
reservation | Holds a claim until a use case releases it, named in releasedBy as a use-case id, or { "mode": "never", "reason": "..." } with its cost. |
commercial-data | Freezes a commercial value, with frozenWhen and retainedIn. |
default | States 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.
| Field | Meaning |
|---|---|
meaning | What happened, in one sentence. |
carries | The facts arriving with the event. |
Standards#
Define Every Behavior Term in the Glossary#
ruleruleguidancelitespec/core/domain-model#define-every-behavior-term-in-the-glossaryEach 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#
rulerulecheckablelitespec/core/domain-model#mark-each-term-with-its-layerEach 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#
ruleruleguidancelitespec/core/domain-model#state-every-module-dependencyEach 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#
rulerulecheckablelitespec/core/domain-model#declare-the-kind-of-every-aggregateEach 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#
rulerulecheckablelitespec/core/domain-model#name-the-owner-of-every-storeEach 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#
rulerulecheckablelitespec/core/domain-model#model-each-aggregate-lifecycle-with-statesEach 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#
rulerulecheckablelitespec/core/domain-model#leave-states-off-every-storeA 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#
rulerulecheckablelitespec/core/domain-model#connect-every-state-through-transitionsEach 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#
rulerulecheckablelitespec/core/domain-model#state-each-rule-with-its-kindEach 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#
rulerulecheckablelitespec/core/domain-model#bind-every-atomic-write-group-in-its-ruleEach 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#
rulerulecheckablelitespec/core/domain-model#keep-the-negation-word-in-the-rule-nameA 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#
ruleruleguidancelitespec/core/domain-model#keep-cross-aggregate-rules-on-a-rule-pageA 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#
ruleruleguidancelitespec/core/domain-model#name-the-refusal-a-broken-rule-producesEach 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#
rulerulecheckablelitespec/core/domain-model#release-every-reservation-by-a-named-use-caseEach 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#
rulerulecheckablelitespec/core/domain-model#freeze-every-commercial-data-valueEach 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#
ruleruleguidancelitespec/core/domain-model#compute-shared-values-from-one-calculation-ruleA 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#
rulerulecheckablelitespec/core/domain-model#tabulate-every-calculation-with-examplesEach 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#
rulerulecheckablelitespec/core/domain-model#state-each-event-with-meaning-and-carriesEach 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#
ruleruleguidancelitespec/core/domain-model#name-every-event-in-the-past-tenseEach 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#
ruleruleguidancelitespec/core/domain-model#raise-every-event-through-its-aggregate-rootEach 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#
ruleruleguidancelitespec/core/domain-model#keep-model-invariants-in-rulesA 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#
conventiondefaultguidancelitespec/core/domain-model#name-each-module-after-its-capabilityName 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.