Type to search pages and rules.

Browse pages

Model a Domain Module

This tutorial models one domain module that the model builder reads.

Markdown

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, and use-case.md templates from the template index.

Lesson#

  1. 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.

    TermMeaningRejected words
    term/loanOne member holds one copy until its due dateCheckout, issue
    term/copyOne lendable item on the shelvesVolume, unit
    term/memberOne person allowed to borrowPatron, user

    Rejected words stop a later contributor from creating Checkout beside Loan. They also tell an agent which plausible names are wrong for this domain.

  2. 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-record and litespec/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.

  3. 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 satisfies litespec/core/system#group-language-and-use-cases-by-module.

  4. 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-boundaries and litespec/backend/domain#keep-child-entities-inside-the-aggregate-boundary.

    Business recordOwnsReferences by typed ID
    business-record/circulation.loanLoan lifecycle, due date, return factsCopyId, MemberId
    business-record/circulation.holdHold queue position and expiryCopyId, MemberId

    A 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 Member inside Loan for convenient navigation. This satisfies litespec/backend/domain#reference-other-aggregates-by-id.

  5. 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.

    StateMeaningRequired facts
    OpenThe copy is out and the member holds itDue date
    ReturnedThe copy is back on the shelvesReturn 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.

  6. Write the rules with kinds. Give each rule a person-chosen identifier of one to six lowercase words. A statement with not, no, never, or cannot keeps that word in its name. This satisfies litespec/core/system#state-each-rule-with-its-kind.

    RuleKindEnforced byStatementRefusal
    rule/loan.only-an-open-loan-closesalways-trueaggregateOnly an open loan closes.result/return-a-copy.loan-not-open
    rule/loan.a-due-date-is-required-to-borrowchecked-beforevalidatorA borrow carries a due date.result/borrow-a-copy.due-date-missing
    rule/loan.no-copy-carries-two-open-loansacross-recordsstoreNo copy carries two open loans.result/borrow-a-copy.copy-already-on-loan

    An always-true rule holds after every change to its record. A checked-before rule rejects caller-correctable input before domain work starts. An across-records rule spans records that one transaction cannot hold. Input shape stays in the validator; current record state stays in the record. This satisfies litespec/backend/application#separate-input-validation-from-invariants.

  7. 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.

    ActorRolePermission
    actor/memberrole/member at self scopepermission/borrow-copy
    actor/librarianrole/librarian at desk scopepermission/close-any-loan
  8. 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-layout and litespec/backend/application#keep-command-handlers-narrow.

    Use caseOperationNeedsRaises
    use-case/circulation.borrow-a-copycommandpermission/borrow-copyevent/loan.copy-borrowed
    use-case/circulation.return-a-copycommandpermission/borrow-copyevent/loan.copy-returned
    use-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.

  9. Write one result per ending. A result is one way a use case ends: success, or a named refusal with its failure code. This satisfies litespec/backend/application#model-expected-use-case-failures-explicitly and litespec/backend/api#use-consistent-status-codes.

    ResultOutcomeCodeStatus
    result/borrow-a-copy.copy-borrowedsuccessNone201
    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.

  10. 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.

    CaseSituationActionResultExpect
    case/borrow-a-copy.first-borrow-succeedsThe copy sits on the shelfThe member borrows itresult/borrow-a-copy.copy-borrowedThe loan opens with a due date
    case/borrow-a-copy.second-borrow-is-refusedThe copy is already outAnother member borrows itresult/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.

  11. 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.

    EventMeaningCarries
    event/loan.copy-borrowedA member took a copy from the deskloanId, copyId, memberId, dueDate
    event/loan.copy-returnedA copy came back to the shelvesloanId, copyId, returnedAt

    Work 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.

  12. 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-pipeline and litespec/backend/persistence#commit-each-command-pipeline-once.

  13. 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.cs

    Create subfolders only when real types require them.

  14. Run the model check. Run litespec model check from the consumer root. It resolves every citation, computes the code, screen, and test links, and reports each unproved case. The command index states how litespec runs 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.