Browse pages
Model a Domain Module
This tutorial models one domain module that the model builder reads.
Purpose#
This tutorial models one domain module that the model builder reads. At the end the module holds its language, its business records, its rules, its use cases, its events, and its permissions. The worked example is the circulation desk of a library.
This guide carries no provision. Each step cites the provisions it satisfies.
Prerequisites#
- Finish Get started through its configuration steps.
- Read the product brief and the requirements this module fulfills.
- Read the decisions that constrain identity, persistence, security, or external behavior.
- Copy the
module.md,business-record.md, anduse-case.mdtemplates from the template index.
Lesson#
-
Name the module language. Write one Term entry per word with its meaning, its labels, and the words it rejects. Use the same term in documentation, domain types, application operations, API descriptions, and frontend features. This satisfies
litespec/backend/domain#use-one-ubiquitous-language.Term Meaning Rejected words term/loanOne member holds one copy until its due date Checkout, issue term/copyOne lendable item on the shelves Volume, unit term/memberOne person allowed to borrow Patron, user Rejected words stop a later contributor from creating
CheckoutbesideLoan. They also tell an agent which plausible names are wrong for this domain. -
Write the occasion before the rules. Name one occasion the module exists for: who acts, what surrounds them, and what they use today instead. Record it in the module page, and reuse its people and figures on every page beneath it. This satisfies
litespec/core/system#state-one-occasion-for-every-behavior-recordandlitespec/core/system#keep-a-scenario-informative.Saturday morning at the central library brings two hundred members before noon. Each hands books across the desk and trusts the stamp card inside the cover. Without a loan record, the desk reconstructs who holds what from the card index and memory.
-
Write the module record. Give the module a person-chosen identifier such as
module/circulation. Name each module it depends on, or remove the field. This satisfieslitespec/core/system#group-language-and-use-cases-by-module. -
Draw business record boundaries from invariants. A business record is a consistency boundary: state that must stay valid together in one command. An invariant is a condition that holds before and after every successful change. This satisfies
litespec/backend/domain#treat-aggregates-as-consistency-boundariesandlitespec/backend/domain#keep-child-entities-inside-the-aggregate-boundary.Business record Owns References by typed ID business-record/circulation.loanLoan lifecycle, due date, return facts CopyId,MemberIdbusiness-record/circulation.holdHold queue position and expiry CopyId,MemberIdA record owns a child when the child has no independent boundary and changes only through the record. Reference another record by typed ID when it changes on its own. Do not place
MemberinsideLoanfor convenient navigation. This satisfieslitespec/backend/domain#reference-other-aggregates-by-id. -
Define every state. A state is a business-valid condition with its required data and permitted behavior. Describe states before writing transitions. This satisfies
litespec/backend/domain#model-every-aggregate-lifecycle-with-state-types.State Meaning Required facts OpenThe copy is out and the member holds it Due date ReturnedThe copy is back on the shelves Return date Create an abstract state record and one sealed record per state. Put state-specific data on the state record. This satisfies
litespec/backend/dotnet/domain#model-every-aggregate-lifecycle-with-state-records.C# public abstract record LoanState; public sealed record LoanOpenState( DateTimeOffset DueDate) : LoanState; public sealed record LoanReturnedState( DateTimeOffset ReturnedAt) : LoanState;For each state, answer what makes it true, which data it alone requires, and which actions it allows and rejects. An unanswered question stays on the page. It receives no guessed default.
-
Write the rules with kinds. Give each rule a person-chosen identifier of one to six lowercase words. A statement with
not,no,never, orcannotkeeps that word in its name. This satisfieslitespec/core/system#state-each-rule-with-its-kind.Rule Kind Enforced by Statement Refusal rule/loan.only-an-open-loan-closesalways-trueaggregateOnly an open loan closes. result/return-a-copy.loan-not-openrule/loan.a-due-date-is-required-to-borrowchecked-beforevalidatorA borrow carries a due date. result/borrow-a-copy.due-date-missingrule/loan.no-copy-carries-two-open-loansacross-recordsstoreNo copy carries two open loans. result/borrow-a-copy.copy-already-on-loanAn
always-truerule holds after every change to its record. Achecked-beforerule rejects caller-correctable input before domain work starts. Anacross-recordsrule spans records that one transaction cannot hold. Input shape stays in the validator; current record state stays in the record. This satisfieslitespec/backend/application#separate-input-validation-from-invariants. -
Name the people and their permissions. Write one actor page per party and one role page per grantable set. Name the permission each use case needs. This satisfies
litespec/backend/application#enforce-target-authorization-in-the-pipeline.Actor Role Permission actor/memberrole/memberat self scopepermission/borrow-copyactor/librarianrole/librarianat desk scopepermission/close-any-loan -
Write the use cases. Give each use case a name that is unique across the project, so its results and cases resolve. Name the operation, the needed permissions, the entry points, the raised events, and the fulfilled requirements. This satisfies
litespec/backend/application#use-this-operation-layoutandlitespec/backend/application#keep-command-handlers-narrow.Use case Operation Needs Raises use-case/circulation.borrow-a-copycommandpermission/borrow-copyevent/loan.copy-borroweduse-case/circulation.return-a-copycommandpermission/borrow-copyevent/loan.copy-returneduse-case/circulation.list-open-loansquerypermission/borrow-copyNone A query states no domain transition and names its read model. It loads no business record for presentation.
-
Write one result per ending. A result is one way a use case ends:
success, or a namedrefusalwith its failure code. This satisfieslitespec/backend/application#model-expected-use-case-failures-explicitlyandlitespec/backend/api#use-consistent-status-codes.Result Outcome Code Status result/borrow-a-copy.copy-borrowedsuccessNone 201 result/borrow-a-copy.copy-already-on-loanrefusalfailure/circulation.copy-already-on-loan409 result/borrow-a-copy.due-date-missingrefusalfailure/circulation.due-date-missing400 Each refusal explains what the caller sees. The failure code is stable across releases.
-
Write the cases. A case is one working example: a situation and the result it leads to. Each case names its result. A case with no proving test yet becomes a known gap record with an owner and an end date.
Case Situation Action Result Expect case/borrow-a-copy.first-borrow-succeedsThe copy sits on the shelf The member borrows it result/borrow-a-copy.copy-borrowedThe loan opens with a due date case/borrow-a-copy.second-borrow-is-refusedThe copy is already out Another member borrows it result/borrow-a-copy.copy-already-on-loanThe desk sees the refusal code Name each test by the observable behavior it asserts. This satisfies
litespec/backend/testing#name-tests-by-observable-behavior. An acceptance scenario carries one/// <proves>case/...</proves>tag. The model builder confirms the claim when the scenario calls the use case and asserts the result. -
Record the events. An event is an immutable record of a completed fact. Name what completed and which fields it carries. This satisfies
litespec/backend/dotnet/domain#raise-immutable-domain-facts.Event Meaning Carries event/loan.copy-borrowedA member took a copy from the desk loanId,copyId,memberId,dueDateevent/loan.copy-returnedA copy came back to the shelves loanId,copyId,returnedAtWork that crosses a transaction or time boundary is a use case that reacts to an event, or a use case on a schedule. It declares what it reacts to and its delivery. This satisfies
litespec/backend/application#keep-event-reaction-implementations-explicit. -
Keep one transaction per command. One handler stages the records its use case owns and lets the pipeline commit once. It dispatches no further command. This satisfies
litespec/backend/application#keep-one-state-changing-use-case-in-one-pipelineandlitespec/backend/persistence#commit-each-command-pipeline-once. -
Map the model to folders. Mirror the module, record, and use case path in every layer. This satisfies
litespec/backend/architecture#organize-every-layer-by-module-and-use-case.Text apps/api/src/Library.Domain/ Circulation/ Loans/ Loan.cs LoanId.cs ILoanRepository.cs States/ LoanState.cs LoanOpenState.cs LoanReturnedState.cs Events/ CopyBorrowedEvent.cs Exceptions/ CopyAlreadyOnLoanException.cs apps/api/src/Library.Application/ Circulation/ Loans/ BorrowACopy/ BorrowACopyCommand.cs BorrowACopyCommandResult.cs BorrowACopyCommandValidator.cs BorrowACopyCommandHandler.csCreate subfolders only when real types require them.
-
Run the model check. Run
litespec model checkfrom the consumer root. It resolves every citation, computes the code, screen, and test links, and reports each unproved case. The command index states howlitespecruns in a consumer.
What you built#
- One module with its language, records, rules, use cases, results, cases, events, and permissions.
- Every identifier is person-chosen, and every citation resolves.
- Every case is proved by a passing test or owned by a known gap.