Type to search pages and rules.

Browse pages

Application Layer

The Application layer coordinates use cases, checking the shape of each incoming message, authorizing it, running the handler, and returning a result that carries no transport details. A use case is one command, which changes business state, or one query, which reads without changing state; a reaction answers an event without a request, and a job runs work on a schedule or by hand. This page states how use cases, reactions, and jobs are organized, what each stage of the command pipeline owns, and how ports and declared effects are wired.

Markdown42 provisions

Stack: any Layer: application

Concepts#

The Use Case Folder#

Every Application folder in the module tree is a product, module, aggregate, or use case layer folder, a Contracts/ folder, or a Jobs/ or UseCases/ folder. One use case folder holds everything a reviewer needs to judge a use case, flat: its message, result, validator, handler, mapping, definition of pipeline-read values, and its endpoint with the request and response models. A use case file's layer comes from its role suffix, so Application and WebApi files sit side by side in the one folder. An On{Fact}/ folder inside the module UseCases/ folder holds its event handler classes with the marks naming their reactions, named under litespec/workspace/naming#group-event-handlers-by-the-fact-they-handle. Contracts another module names live in the owning module's Contracts/ folder sorted by kind, and the pipeline machinery lives in the product Application/ folder.

The Command Pipeline#

A command travels fixed stages in order: validation, authorization, idempotency, audit, timestamp, holds, the handler, commit, and fault classification, with exactly one commit post-handler. The handler loads or creates the one aggregate its use case changes, calls one domain method on it, stages it through its domain repository, and returns a result. Validation, authorization, idempotency, audit, the timestamp, holds, the commit, and fault classification each belong to their own pipeline stage, so none appears in the handler. A query travels validation and the guard, then projects its result directly without loading an aggregate. An event handler answers its trigger outside the command pipeline, running the declared runs, sends, and calls effects after the commit, while an emits effect leaves the commit as a domain event staged in the outbox. A scheduled or manual job runs its commands once per item in its selection.

Ports#

A port is a narrow interface Application declares for work it does not do itself, such as archiving posts or reading the clock. Each port names one business capability with project-owned types, and Infrastructure implements it. A port is declared in the layer folder its consumers share, so it is reviewed with the specification it serves; a port another module calls is published in the owning module's Contracts/Ports/ folder. An external-system port carries the operations one calls effect names, a messenger port carries the notifications sends effects name, and a store port carries state the owning system holds.

Declared Effects#

Every edge between use cases, reactions, jobs, notifications, integration events, and external systems is declared on a record and checked in code. A use case declares its edges in effects: runs names a command, sends names a notification, emits names an integration event, and calls names an external-system operation. Each entry carries its timing: inline shares the caller's call, after-commit runs after the commit succeeds, and async runs outside the caller's transaction. An edge the records do not declare is the defect the effect checks report.

Standards#

Organize Application by Use Case#

ruleruleguidance
litespec/backend/application#organize-application-by-use-case
Statement

Each command or query MUST own one use case folder inside the UseCases/ folder of its module and aggregate holding its message, result, validator, handler, mapping, and definition of pipeline-read values.

Applies toartifact: any

Why

One use case folder inside its UseCases/ folder holds everything a reviewer needs to judge a use case: its message, result, validator, handler, mapping, and pipeline-read values, with its endpoint and transport models beside them. Grouping by technical type scatters one use case across the module, so a change touches six folders and review misses one. Type names share the use-case prefix and follow the suffix table under litespec/workspace/naming#use-architectural-suffixes. Pipeline-read values are declared beside the message under litespec/backend/application#declare-every-pipeline-position-beside-the-message.

Checka reviewer answers

Reviewed forWhich commands or queries lack one use case folder inside the UseCases/ folder of their module and aggregate with message, result, validator, handler, mapping, and pipeline-read values? Look at the folders under each module.

Marked guidance: a reviewer answers this question.

Dispatch Commands and Queries Through Separate Entry Points#

ruleruleguidance
litespec/backend/application#dispatch-commands-and-queries-through-separate-entry-points
Statement

An application MUST dispatch a command and a query through two separate Application-facing entry points rather than one message bus abstraction.

Applies toartifact: command-handler, endpoint, job, reaction

Why

Two entry points keep the write and read paths distinguishable at every call site and let the pipeline compose different stages for each. A unified bus hides that difference behind one method, so a reader sees no difference between a dispatch that changes state and one that reads. This backend's whole command and query split depends on that difference.

The rule states the property rather than the package. Each stack binding pins one library that supplies it. A consumer replacing that library records an exception against the binding convention rather than touching this rule.

Checka reviewer answers

Reviewed forWhich dispatch sites call a unified bus instead of the command or query entry point? Look at each dispatch site.

Marked guidance: a reviewer answers this question.

Separate Input Validation from Invariants#

ruleruleguidance
litespec/backend/application#separate-input-validation-from-invariants
Statement

A validator MUST limit its checks to structural input, leaving aggregate state decisions to Domain.

Applies toartifact: validator

Why

Structural input and aggregate state fail for different reasons and at different times: malformed input is rejected before Domain runs, while a state refusal needs the loaded aggregate. A validator that checked state would need the aggregate, which pulls persistence into validation. A validator rejects an empty title, a malformed identifier, or an invalid page size, and Domain decides whether the expense above the threshold carries a receipt. Every validator for a message reports validity findings under litespec/backend/application#report-input-problems-as-validity-findings, so the caller learns the whole form in one answer.

Example

In the Claimwick product, use-case/spending.submit-expense takes a command carrying an expense identifier and an employee identifier with the amount, currency, and receipt. The validator rejects an empty expense identifier or an empty employee identifier, and accepts every well-formed pair.

Whether the expense above the threshold carries a receipt is an aggregate state decision, so the validator leaves it to Domain. Domain rejects an expense above the threshold that carries no receipt with result/submit-expense.receipt-missing, which carries the stable code spending.receipt-missing.

Checka reviewer answers

Reviewed forWhich validators check aggregate state instead of structural input only? Look at each validator body.

Marked guidance: a reviewer answers this question.

Report Input Problems as Validity Findings#

ruleruleguidance
litespec/backend/application#report-input-problems-as-validity-findings
Statement

A validator MUST report its findings as validity findings, with each finding carrying a member, a code, and a message, and no transport status.

Applies toartifact: validator

Why

Running every validator and merging findings tells the caller everything wrong with the form in one answer; stopping at the first finding turns one form into repeated round trips. Each finding carries a member naming the input member, a code naming the stable reason, and a message explaining the problem in caller-safe language, with no transport status because Application answers no transport.

Example

A submit request with an empty expense identifier and an empty employee identifier reports two findings, one per member, with no transport status:

JSON
[
  { "member": "ExpenseId", "code": "required", "message": "An expense is required." },
  { "member": "EmployeeId", "code": "required", "message": "An employee is required." }
]

The caller learns the whole form in one answer, and the pipeline refuses the request before Domain runs.

Checka reviewer answers

Reviewed forWhich validators report outside validity findings, or skip member, code, or message, or leak transport status? Look at the findings each validator reports.

Marked guidance: a reviewer answers this question.

Model Expected Use-Case Refusals Explicitly#

ruleruleguidance
litespec/backend/application#model-expected-use-case-refusals-explicitly
Statement

An expected use-case refusal MUST carry its result code and a caller-safe message, with no transport status.

Applies toartifact: use-case

Why

An expected refusal is part of the use case's contract: a missing resource, a forbidden act, or a conflicting write, carrying the refusal result's stable code and a caller-safe message. The refusal carries no transport result, provider detail, or stack detail, so the same refusal maps correctly at any transport, and handlers do not catch it.

The use case lists every result it returns, and each refusal result carries its code. Generic refusals are derived, never written: a use case whose grant is a permission derives its forbidden result, and a use case with rules on its input derives its invalid-input result. A conflict refuses the same input the same way until the caller rereads the resource and retries with current state. A refusal that a blind retry can pass is transient and belongs to the integrations extension.

Checka reviewer answers

Reviewed forWhich expected refusals miss their result code or caller-safe message, or leak transport status? Look at each refusal.

Marked guidance: a reviewer answers this question.

Enforce Target Authorization in the Pipeline#

rulerulecheckable
litespec/backend/application#enforce-target-authorization-in-the-pipeline
Statement

The authorization guard, not the handler, MUST authorize every message from the access its use-case record declares.

Applies toartifact: any

Why

Authorization decided in each handler is decided differently in each handler. The guard decides once from what the record declares: the auth kinds the caller presents, the grant naming a permission, a self scope over one, anonymous, or system, and the tenant scope naming the tenants the call reaches. Ownership, tenant, role, state, or delegated access resolves against the target data through a scope owner lookup for the record the message names. A handler never compares the caller with a target and never throws the forbidden refusal itself. Code names no permitted action outside that grant, so the record stays the one list of who may run a use case; where the stack generates code, it generates one constant per permission/ record from the records.

Declaring access beside the message also lets composition refuse to start when a message states no position at all. The guard reports a refusal as a denial rather than raising it, so a refused attempt reaches the audit trail beside the ones that succeeded. The audit extension records the denial under litespec/ext/audit#record-refused-authorization.

A collection query has no single target record, so a scope owner lookup has nothing to resolve. Its authorized scope is a database predicate under litespec/quality/security#restrict-a-protected-collection-in-the-database, applied before paging. Filtering a loaded collection in memory satisfies neither rule. It matches the shape OWASP names as broken object level authorization.

WebApi enforces coarse authenticated, role, or scope policies first under litespec/backend/api#apply-only-coarse-policies-at-the-endpoint, which replaces no decision that depends on business data.

Checkchecked by a tool

Checked bydotnet build reports LS2012 for a handler that throws a forbidden refusal or that takes the authorization guard; the guard authorizes every message from its record's access.

Marked checkable: the tool named here checks it.

Keep Command Handlers to Load, Call and Stage#

ruleruleguidance
litespec/backend/application#keep-command-handlers-to-load-call-and-stage
Statement

A command handler MUST load or create the aggregate its use case changes, call one domain method on it, stage it through its repository, and return the result, and do nothing else.

Applies toartifact: command-handler

Why

Everything else has an owner. Validation, authorization, idempotency, audit, the timestamp, holds, and the commit are pipeline stages; decisions and invariants are the aggregate's; uniqueness is persistence's under litespec/backend/persistence#enforce-uniqueness-in-the-store; messages, provider calls, and changes to other aggregates are consequences that event handlers run after the commit under litespec/backend/application#dispatch-only-declared-effect-edges. A handler that does more widens the unit of work, so one failing step rolls back work that had no reason to fail with it, and agents copy the widened shape. The commit post-handler owns the transaction under litespec/backend/persistence#commit-each-command-pipeline-once, WebApi owns error mapping, the audit completion handler owns the trail, and the guard owns authorization. A use case whose record declares consistency: atomic over several aggregates is the one exception: the handler loads, calls, and stages each aggregate its changes names, one method each, and the record states why the change cannot be eventual. The handler takes its repositories under litespec/backend/application#take-repositories-from-declared-changes-and-reads. It returns only the results and refusals the record declares, under litespec/backend/application#return-only-declared-results-and-refusals.

Example

In the Claimwick product, use-case/spending.approve-expense runs through the command pipeline: the endpoint maps the request to the approve command, the command entry point dispatches it, validation and the authorization guard run, the handler loads the expense, calls Approve on it, stages the expense, and returns the result. The handler body reads as three steps: it loads the expense through its domain repository, it calls one domain method on it, and it stages the expense. The handler commits nothing, and exactly one post-handler commits after it. The diagram shows each stage from the HTTP request to that single commit.

The command pipeline from HTTP request to the single commit An HTTP request reaches the endpoint, which maps it to a command and dispatches it through the command mediator. Validation checks structural input, then the authorization guard checks the permission and the target. The handler loads aggregates through repositories, calls domain behavior, and stages changes. The repository stages the aggregate, and exactly one commit post-handler commits the session once. HTTP request Endpoint Command mediator Validation Authorization guard Handler Repository Commit post-handler submit request maps to command dispatches message structural checks permission and target loads, calls, stages stages aggregate one commit An arrow points from one stage to the next stage in the command pipeline.
An HTTP request reaches the endpoint, which maps it to a command and dispatches it through the command mediator. Validation checks structural input, then the authorization guard checks the permission and the target. The handler loads or creates one aggregate through its repository, calls one domain method, and stages it. The repository stages the aggregate, and exactly one commit post-handler commits the session once.

The handler loads or creates, calls once, stages, and returns. Validation, authorization, idempotency, audit, the timestamp, holds, commit, and fault classification belong to their owners, so none appears in the handler.

Checka reviewer answers

Reviewed forWhich command handlers do more than load or create one aggregate, call one method, stage it, and return? Look at each command handler body.

Marked guidance: a reviewer answers this question.

Take Repositories from Declared Changes and Reads#

rulerulecheckable
litespec/backend/application#take-repositories-from-declared-changes-and-reads
Statement

A command handler MUST write only the aggregates its record's changes names, through one domain repository per aggregate.

Applies toartifact: command-handler

Why

A write outside changes is behavior the record hides, so the stack reader fails write-undeclared on it. Under litespec/backend/application#keep-command-handlers-to-load-call-and-stage the handler normally takes one domain repository for the one aggregate it changes and stages only that aggregate. A use case whose record declares consistency: atomic over several aggregates takes one repository per named aggregate and stages each one. A query handler likewise projects only over the aggregates its record's reads names, through the read session.

Example

The approve-expense handler takes the expense repository and no other; staging a second aggregate fails the check unless the record declares consistency: atomic over both.

Checkchecked by a tool

Checked bylitespec model check reports write-undeclared (level S) for a handler that takes a repository whose aggregate sits outside the aggregates its record names in changes and reads; one domain repository serves each named aggregate.

Marked checkable: the tool named here checks it.

Implement One Outcome Path per Command#

rulerulecheckable
litespec/backend/application#implement-one-outcome-path-per-command
Statement

Each command MUST implement one use case with one success path, so a flow that finds a record or else creates it is two use cases that the boundary owning the flow chooses between.

Applies toartifact: command-handler

Why

A get-or-create command hides two use cases with different access, audit, and refusals behind one name. The boundary that owns the flow, an endpoint, an event handler, or a job, runs a query and then sends the command that fits. When the create loses a race, it refuses with its conflict under litespec/backend/application#refuse-a-lost-race-as-a-conflict, and the owner sends the other command. Where the record is usually present, the owner may instead send the command first and, on the record's not-found refusal, send the create and then the command again; one shared helper holds that sequence, so no endpoint repeats it.

Example

Signing in to the library with an unknown reader card runs as a query and then one of two commands: the endpoint queries query/loans.find-reader-by-card, then sends either use-case/loans.register-reader or use-case/loans.record-reader-sign-in.

Checkchecked by a tool

Checked bydotnet build reports LS2014 for a command handler that calls a Find repository method and then creates the found type in the same method.

Marked checkable: the tool named here checks it.

Return Only Declared Results and Refusals#

rulerulecheckable
litespec/backend/application#return-only-declared-results-and-refusals
Statement

A handler MUST return only the results, refusals, and codes its use-case record declares.

Applies toartifact: command-handler, query-handler

Why

An observable outcome no result declares is behavior the specification hides, so the stack reader fails result-undeclared on it. Refusals return in the precedence order the record lists, first match wins, and a success carrying codes returns only those codes. Generic refusals derive from access and input and need no declaration.

Example

A handler that returns a code its record never lists fails the check, even when the code is spelled correctly.

Checkchecked by a tool

Checked bylitespec model check reports result-undeclared (level S) for a code a handler produces that no declared result names; a handler returns only the results and codes its record lists.

Marked checkable: the tool named here checks it.

Project Queries Directly#

rulerulecheckable
litespec/backend/application#project-queries-directly
Statement

A query handler MUST project directly to its result without loading an aggregate.

Applies toartifact: query-handler

Why

A read path acts on no invariant, so loading an aggregate for presentation pays for enforcement no read needs and couples the read shape to the write model. Projecting directly to the result keeps reads on the read boundary the persistence standards state. A query handler takes no domain repository. The result type carries the include groups the record's output declares, under litespec/backend/application#shape-query-results-from-declared-output. The command and query split lives in litespec/backend/architecture#separate-command-and-query-behavior.

Checkchecked by a tool

Checked bydotnet build reports LS2011 for a query handler taking a repository; each query handler projects directly without loading an aggregate.

Marked checkable: the tool named here checks it.

Keep Query Handlers Free of Writes#

ruleruleguidance
litespec/backend/application#keep-query-handlers-free-of-writes
Statement

A query handler MUST NOT stage, store, delete or dispatch anything.

Applies toartifact: query-handler

Why

A read that writes makes retries, caching, and authorization of reads unsafe. An audited read is the pipeline's, not the handler's, under litespec/ext/audit#declare-audit-selection-on-every-command.

Example

The list-expenses query projects its rows and stages nothing.

Checka reviewer answers

Reviewed forWhich query handlers stage, store, delete, or dispatch? Look at each query handler body.

Marked guidance: a reviewer answers this question.

Shape Query Results from Declared Output#

ruleruleguidance
litespec/backend/application#shape-query-results-from-declared-output
Statement

A query result type MUST carry the include groups its record's output declares, with their fields, freshness, and visibility.

Applies toartifact: query-handler

Why

The record's output names each group with its fields, their classification, the aggregates it reads, and the freshness the caller can rely on: immediate, snapshot, bounded, or advisory. The result type carries one member per group, so the caller reads what the record promises and nothing more. A group with visibleWhen appears only when its condition holds, and a group wider than its reads names the approved requirement widening it in cites. A query with builtAsynchronously carries a bounded or advisory freshness with its lag scenario.

Example

The expense-list result carries its rows group with the declared fields, freshness, and visibility.

Checka reviewer answers

Reviewed forWhich query result types miss a declared include group, field, freshness, or visibility? Look at each result type beside its record's output.

Marked guidance: a reviewer answers this question.

Mirror a Domain Closed Set in the Result#

ruleruleguidance
litespec/backend/application#mirror-a-domain-closed-set-in-the-result
Statement

An Application result carrying a Domain closed set MUST model that set with the same shape as its own union or code value.

Applies toartifact: use-case

Why

Each case of a closed set can carry different data, and that data needs to survive to the caller. Mirroring the Domain shape in the result keeps the layers aligned; flattening case data into an enumeration with nullable fields loses which fields belong to which case. Application exposes no Domain union type. Narrowing follows litespec/backend/application#record-a-decision-for-closed-set-narrowing, and the set itself follows litespec/backend/domain#model-every-closed-set-of-domain-values-as-a-closed-type-hierarchy. The wire carries the same shape under litespec/backend/api#mirror-a-domain-closed-set-as-a-transport-model-of-the-same-shape.

Example

A refund outcome result carries one case per Domain case with its data: pending, succeeded, failed, and reversed.

Checka reviewer answers

Reviewed forWhich results carrying a Domain closed set flatten its shape or drop a case? Look at each such result beside its Domain set.

Marked guidance: a reviewer answers this question.

Record a Decision for Closed-Set Narrowing#

ruleruleguidance
litespec/backend/application#record-a-decision-for-closed-set-narrowing
Statement

A layer MUST NOT narrow a Domain closed set without a recorded decision and an updated specification.

Applies toartifact: endpoint, use-case

Why

Narrowing silently drops cases the Domain can produce, so a caller meeting a dropped case gets a shape the contract left undescribed. A recorded decision with an updated specification makes the dropped cases visible and owned. The decision follows litespec/core/model#record-each-technical-choice-as-a-decision.

Checka reviewer answers

Reviewed forWhich narrowed closed sets lack a recorded decision and updated specification? Look at each narrowed set beside the decision records.

Marked guidance: a reviewer answers this question.

Define Narrow External Ports#

ruleruleguidance
litespec/backend/application#define-narrow-external-ports
Statement

Application MUST declare each external port as a public interface with a noun or verb name, one method or one purpose, and project-owned types.

Applies toartifact: port

Why

A port is the seam where a provider change stops: one method or one purpose, named as a noun for the capability (PostArchive) or a verb for the act (ArchivePosts), typed with project-owned types. If provider names or transport models entered the interface, replacing the provider would rewrite Application. Keeping them in Infrastructure confines the change to the adapter.

Checka reviewer answers

Reviewed forWhich external ports escape the public interface with a noun or verb name, one method or one purpose, and project-owned types? Look at each port declaration.

Marked guidance: a reviewer answers this question.

Declare a Port Where Its Consumers Meet#

ruleruleguidance
litespec/backend/application#declare-a-port-where-its-consumers-meet
Statement

A port MUST be declared at the lowest folder holding every consumer that names it, following litespec/backend/architecture#place-a-type-at-the-lowest-folder-that-holds-its-consumers.

Applies toartifact: port

Why

A port reviewed beside the aggregate it serves is reviewed with that aggregate's specification; a port in a product-wide folder is reviewed by nobody. A port whose consumers sit in one module is declared in that module's layer folder beside the aggregate it serves, and only a port another module calls belongs in the owning module's Contracts/Ports/ folder. Collecting every port in one folder because they share the word "port" is grouping by technical type. litespec/backend/application#organize-application-by-use-case rejects that grouping one level down for the same reason.

You review a port declared beside its aggregate with that aggregate's specification. Nobody reviews a port in a product-wide folder. Where a port serves aggregates in one module but is called from another, the declaring module is the one that owns the aggregates. That module is the one whose rules change when the port changes.

Checka reviewer answers

Reviewed forWhich ports sit above the lowest folder holding every consumer that names them? Look at each port beside its consumers.

Marked guidance: a reviewer answers this question.

Call External Systems Through Their Ports#

ruleruleguidance
litespec/backend/application#call-external-systems-through-their-ports
Statement

Each calls effect MUST run in an event handler or job through the port operation its record names, after the commit of the command that raised its trigger.

Applies toartifact: command-handler, job, reaction

Why

A command handler changes only its aggregate under litespec/backend/application#dispatch-only-declared-effect-edges, so a provider call runs in an event handler or job after the commit. The calls effect names one operation on one external system, and the operations entry states its timeout, its timeout behavior, its retries, its response, and its cache. The port carries that operation with project-owned types and carries a mark naming its system on the port interface, and Infrastructure implements it, so a provider change stops at the adapter. The stack reader counts a call as a calls edge only through such a marked port; any other injected interface is internal work. A call on a marked port that no calls entry declares fails effect-undeclared. A provider call whose result the command needs before it can answer is a departure: the use case records the intent first, for example a pending charge, and an event handler calls the provider. A project that keeps an inline call records an exception naming the compensation that undoes it.

Example

An event handler that calls an expense payout operation names it through the record's calls entry and the port method; a direct provider call fails the check.

Checka reviewer answers

Reviewed forWhich calls effects run outside an event handler or job, bypass their port, or run before the commit? Look at each provider call beside its record's calls entry.

Marked guidance: a reviewer answers this question.

Send Notifications Through the Messenger Port#

ruleruleguidance
litespec/backend/application#send-notifications-through-the-messenger-port
Statement

Each sends effect MUST run in an event handler through the messenger port, after the commit of the command that raised its trigger.

Applies toartifact: command-handler, reaction

Why

A command handler changes only its aggregate under litespec/backend/application#dispatch-only-declared-effect-edges, so a notification send runs in an event handler after the commit. The notification record owns the template, the addresses, the channel, the consent rule, the frequency, the dedup key, and the claims each message keeps. The sends edge names the notification, and the notification record carries its suppression in suppressedWhen. The messenger port carries the send with project-owned types. A send no edge declares fails effect-undeclared, and a second send of a once notification with the same dedup key sends nothing.

Example

The due-notice event handler sends the loan-due notification through the messenger port; the template and consent rule stay on the notification record.

Checka reviewer answers

Reviewed forWhich sends effects run outside an event handler, bypass the messenger port, or run before the commit? Look at each send beside its record's sends entry.

Marked guidance: a reviewer answers this question.

Emit Declared Integration Events After Commit#

ruleruleguidance
litespec/backend/application#emit-declared-integration-events-after-commit
Statement

Each emits effect MUST leave the commit as a domain event the aggregate raised, staged in the outbox with the business work.

Applies toartifact: aggregate

Why

A command handler changes only its aggregate under litespec/backend/application#dispatch-only-declared-effect-edges, so an emits effect is a domain event the aggregate raises, which persistence stages in the outbox in the same commit. The emits entry names the integration event with its timing, and the outbox extension stages the message with the business work so both commit together and delivery follows. An emit no entry declares fails effect-undeclared. Emitting outside the outbox delivers facts the commit did not store.

Example

The reimburse-expense aggregate raises its reimbursed fact, and persistence stages it in the outbox with the business work; the record's emits entry names the event.

Checka reviewer answers

Reviewed forWhich emits effects leave outside a staged domain event in the outbox? Look at each emit beside its record's emits entry.

Marked guidance: a reviewer answers this question.

Expose Store State Through Ports#

ruleruleguidance
litespec/backend/application#expose-store-state-through-ports
Statement

A store aggregate MUST reach code only through a port naming its owner, with no aggregate root.

Applies toartifact: port

Why

A store aggregate is state no invariant guards: a library-owned table, a suppression list, or a delivery log. With no invariant, there is no root, no state record, and no domain repository; the port names the owning system and Infrastructure implements it. Staff actions on that state stay ordinary use cases calling the port.

Example

The suppression list reaches code through a suppression port owned by the communications module, with no aggregate root.

Checka reviewer answers

Reviewed forWhich store aggregates grow an aggregate root or bypass their owner-naming port? Look at each store aggregate's code path.

Marked guidance: a reviewer answers this question.

Read Settings Through Typed Options#

ruleruleguidance
litespec/backend/application#read-settings-through-typed-options
Statement

A handler MUST read a setting record through a typed options value, or through stored tenant data where the record's setBy is tenant.

Applies toartifact: command-handler, query-handler

Why

A setting record owns its type, its default, its range, its unit, and what happens at the bound. Decider-set and operator-set values bind to typed options the host validates at startup, so a bad value fails before traffic arrives. Tenant-set values are data nobody reviews: they live in stored tenant state the handler reads per tenant. A raw configuration lookup bypasses the type, the range, and the bound, so no handler uses one.

Example

The receipt threshold binds to a spending options value; the handler reads the bound and refuses past it with atBound: refuse.

Checka reviewer answers

Reviewed forWhich handlers read a setting through a raw lookup instead of a typed options value or stored tenant data? Look at each setting read in the handlers.

Marked guidance: a reviewer answers this question.

Declare Every Pipeline Position Beside the Message#

ruleruleguidance
litespec/backend/application#declare-every-pipeline-position-beside-the-message
Statement

A message MUST declare a value or an exemption for every pipeline position the baseline and the selected extensions require.

Applies toartifact: use-case

Why

A pipeline stage that guesses a message's position guesses differently per message. Declaring a value or an exemption beside the message makes each position explicit, and composition refuses to start when a message states neither. The baseline holds one position: authorization, declared from the record's access on the message itself. The audit extension adds the audit selection position under litespec/ext/audit#declare-audit-selection-on-every-command.

Checka reviewer answers

Reviewed forWhich messages miss a value or an exemption for a required pipeline position? Look at each message beside the baseline and selected extensions.

Marked guidance: a reviewer answers this question.

Keep Reaction Implementations Explicit#

ruleruleguidance
litespec/backend/application#keep-reaction-implementations-explicit
Statement

A reaction implementation MUST answer the trigger its reaction record names, in one class carrying one mark naming that record.

Applies toartifact: reaction

Why

A reaction without an explicit implementation leaves its trigger, its target, and its failure handling to whoever reads the code. One event handler class per reaction record answers the trigger the record names in triggeredBy, whether a domain event or an integration event, and dispatches only through the record's effects entries. The class never writes an aggregate directly: an event handler that needs a write runs a command. It honors the record's reads, its idempotency, and its onFailure, and the mark is the link the stack reader follows. The class lives in the On{Fact} folder for the fact it handles inside the module UseCases/ folder, named under litespec/workspace/naming#group-event-handlers-by-the-fact-they-handle. The automation page states the trigger fields. An entry with after-commit timing uses durable delivery through the outbox extension.

Example

The class SendDueNoticeOnLoanBorrowedHandler in UseCases/OnLoanBorrowed/ carries one mark naming reaction/lending.send-due-notice-on-loan-borrowed, answers event/loan.loan-borrowed, and sends notification/lending.loan-due.

Checka reviewer answers

Reviewed forWhich event handlers answer outside one class with one mark in their UseCases/On{Fact} folder? Look at each event handler class and its mark.

Marked guidance: a reviewer answers this question.

Run Each Job Within Its Declared Selection#

ruleruleguidance
litespec/backend/application#run-each-job-within-its-declared-selection
Statement

A scheduled or manual job MUST run its declared commands once per item in its record's selection, honoring its batch size, lease, and cursor.

Applies toartifact: job

Why

The job record owns what each run selects, how many items each batch takes, the lease that keeps two runs apart, and the cursor where a run resumes. The job runs its effects entries once per item, records its evidence where the record states, and follows its onFailure when an item fails. A manual job carries no aggregate: it states its runsWhere and its guard, and runs as interactive platform work. One execution host runs the job under litespec/backend/architecture#declare-the-execution-host-for-work-that-outlives-a-request.

Example

The erase-expired-data job selects decided expenses past retention in batches of 200 under its lease, runs one erase command per expense, and resumes from its cursor.

Checka reviewer answers

Reviewed forWhich scheduled or manual jobs run outside their selection, batch size, lease, or cursor? Look at each job run beside its record.

Marked guidance: a reviewer answers this question.

Issue Per-Record Work from a Job#

rulerulecheckable
litespec/backend/application#issue-per-record-work-from-a-job
Statement

Work over many records MUST run as a job that runs one command per record, each in its own unit of work, never as a command handler that changes records in a loop.

Applies toartifact: job

Why

One failing record otherwise rolls back the batch, and the batch holds locks for its whole length. The job runs each command within its declared selection under litespec/backend/application#run-each-job-within-its-declared-selection, and persistence commits each record alone under litespec/backend/persistence#commit-each-command-pipeline-once.

Example

The erase-expired-data job runs one erase command per expense, each in its own unit of work, instead of a command looping over expenses.

Checkchecked by a tool

Checked bydotnet build reports LS2015 for a command handler with a repository Store, Add, Update or Delete call inside a loop body.

Marked checkable: the tool named here checks it.

Dispatch Only Declared Effect Edges#

rulerulecheckable
litespec/backend/application#dispatch-only-declared-effect-edges
Statement

A command handler MUST change only its aggregate, leaving every declared runs, sends, calls, and emits edge to an event handler or job after the commit.

Applies toartifact: command-handler, job, reaction

Why

An undeclared edge is the defect: an event handler or job reaches a use case, notification, integration event, or external system the records never named, so review sees behavior the specification hides. Each declared entry names exactly one verb, its timing, and its failure handling, and the stack reader fails effect-undeclared on any dispatch the record does not declare. An emits effect is a domain event the aggregate raises, staged in the outbox in the same commit under litespec/backend/application#emit-declared-integration-events-after-commit; sends, calls, and runs effects run in an event handler or job after the commit. A sends or calls effect is after-commit or async, never inline. An inline runs entry states whether it shares the caller's transaction, and consistency: atomic never spans an async edge. The effect graph holds no cycles except through domain events with a stated end condition. A saga carried by a workflow engine maps each engine step to exactly one use case with its compensation through the workflow-engine extension.

Example

The reimburse-expense aggregate raises its reimbursed fact, and persistence stages integration-event/expense.reimbursed in the outbox because its record declares that emits entry; a second emit the record does not declare fails the check.

Checkchecked by a tool

Checked bylitespec model check reports effect-undeclared (level S) for a dispatch a handler, event handler, or job takes that its record declares no edge for; a declared runs, sends, or calls edge runs after the commit, and an emits edge leaves as a domain event staged in the outbox.

Marked checkable: the tool named here checks it.

Declare Audit Values Beside the Message#

rulerulecheckable
litespec/backend/application#declare-audit-values-beside-the-message
Statement

Each audited command MUST declare beside the message the audit target and reason that the message carries, leaving the handler to add only the values it learns while running.

Applies toartifact: use-case

Why

A value read from the message is known before the handler runs, so the pipeline records it on every outcome, including a denial. Declaring the target and the reason beside the message evaluates both before the handler runs. The handler adds only a generated identity or a reason composed from loaded state.

Example

The approve-expense command declares its expense id as the audit target with the reason the message carries, and the handler adds only the approval reference it learns while running.

Checkchecked by a tool

Checked bydotnet build reports LS2013 for a WithTarget or WithReason call on IAuditScope inside a command handler whose argument reads only the message.

Marked checkable: the tool named here checks it.

Take One Timestamp per Message#

ruleruleguidance
litespec/backend/application#take-one-timestamp-per-message
Statement

Each handler MUST read the message timestamp from the scoped clock the pipeline stamps once per message.

Applies toartifact: command-handler, query-handler

Why

A handler that reads the clock two or three times in one decision records different times for one act, sometimes around an outside call. One timestamp per message fixes the moment the message starts, and every decision in the handling answers to the same time. The shared clock stays for infrastructure only.

Example

The approve handler stamps its approval with the handled clock, not with a fresh clock read.

Checka reviewer answers

Reviewed forWhich handlers read time outside the scoped clock stamped once per message? Look at the clock reads in each handler.

Marked guidance: a reviewer answers this question.

Declare Exclusive Holds Beside the Message#

ruleruleguidance
litespec/backend/application#declare-exclusive-holds-beside-the-message
Statement

Each command needing exclusive access MUST declare its hold beside the message, with its key and its patience.

Applies toartifact: use-case

Why

A lock copied into each handler carries its own key, patience, and refusal at every site, and each copy drifts. Declaring the hold beside the message takes it in a pipeline step after validation and releases it on completion, refusing with the conflict code when the wait runs out.

Example

The refund command declares its per-order hold with fifteen seconds of patience.

Checka reviewer answers

Reviewed forWhich commands needing exclusive access miss their hold key and patience beside the message? Look at each such command message.

Marked guidance: a reviewer answers this question.

Refuse a Lost Race as a Conflict#

ruleruleguidance
litespec/backend/application#refuse-a-lost-race-as-a-conflict
Statement

Each command that loses a race at commit MUST end with its use case's conflict refusal.

Applies toartifact: command-handler

Why

A duplicate key or a stale version reaches the commit as a store fault, and the pipeline classifies it as the use case's named conflict refusal, so the caller learns what happened and may retry. No reservation, re-read, or retry lives in the handler. The store enforces uniqueness under litespec/backend/persistence#enforce-uniqueness-in-the-store and versions every aggregate under litespec/backend/persistence#version-every-aggregate.

Example

Two clerks register the same book barcode at once; the second commit violates the barcode unique index, and the command refuses with book-barcode-taken.

Checka reviewer answers

Reviewed forWhich commands losing a race at commit end outside their use case conflict refusal? Look at the race outcomes beside the use case refusals.

Marked guidance: a reviewer answers this question.

Load a Required Record or Refuse It#

ruleruleguidance
litespec/backend/application#load-a-required-record-or-refuse-it
Statement

Each handler needing a record MUST load it through the getter that throws the aggregate not-found refusal, and use the finder only for optional records.

Applies toartifact: command-handler, query-handler

Why

A find followed by a thrown not-found repeats the aggregate decision in every handler, and each copy names its own failure. The required getter throws the aggregate own not-found refusal, so the handler states that the record is required by the call it makes.

Example

The approve handler loads its expense with GetByIdAsync, while the optional coupon lookup uses FindByIdAsync.

Checka reviewer answers

Reviewed forWhich required records load through the finder, or which optional lookups throw? Look at each load call in the handlers.

Marked guidance: a reviewer answers this question.

Parse Input in the Validator#

ruleruleguidance
litespec/backend/application#parse-input-in-the-validator
Statement

Each validator MUST parse every value object in the input, so the handler construction cannot fail on input.

Applies toartifact: validator

Why

A validator that checks presence only leaves parsing to the handler, so one input is judged in two layers and the handler throws on caller data. Parsing in the validator keeps business refusals in the domain and input failures in validation, where each belongs.

Example

The invite validator parses the email address with TryCreate, and the handler creates it without a check.

Checka reviewer answers

Reviewed forWhich validators leave a value object unparsed for the handler? Look at each validator beside its input.

Marked guidance: a reviewer answers this question.

Validate Query Input#

ruleruleguidance
litespec/backend/application#validate-query-input
Statement

Each query carrying paging, filters, or ranges MUST declare a query validator run by the pipeline.

Applies toartifact: use-case

Why

An unvalidated page size or range reaches the store unchecked, and each query handler clamps its own input or none does. A query validator runs in the same pipeline step as command validators, so reads and writes answer to the same input rule.

Example

The list-expenses query validates its page size through its query validator.

Checka reviewer answers

Reviewed forWhich queries with paging, filters, or ranges lack a query validator? Look at each such query beside the pipeline validators.

Marked guidance: a reviewer answers this question.

Send Messages After Commit#

rulerulecheckable
litespec/backend/application#send-messages-after-commit
Statement

Each command MUST send its messages from an event handler after the commit, never from the command handler.

Applies toartifact: command-handler, reaction

Why

A message that leaves before its transaction commits promises work even when the transaction then fails. The aggregate raises its fact, and an event handler for that fact sends, so no message leaves for a write that never landed. A sends effect is after-commit or async, never inline.

Example

The submit-expense aggregate raises its submitted fact, and an event handler sends the approver notice after the commit.

Checkchecked by a tool

Checked bylitespec model check reports effect-timing-invalid (level S) for a command use case declaring a sends or calls effect with inline timing.

Marked checkable: the tool named here checks it.

Build Collaborators Through the Container#

ruleruleguidance
litespec/backend/application#build-collaborators-through-the-container
Statement

Each handler needing a collaborator MUST take it through the constructor from the container, never build it with new.

Applies toartifact: command-handler, job, query-handler, reaction

Why

A step class built with new hides its dependencies, and a handler that looks small is really hundreds of lines behind its helpers. Injected collaborators keep their dependencies visible in the constructor.

Example

The reimburse handler takes its expense repository through its constructor, while a step built with new that takes a repository is reported.

Checka reviewer answers

Reviewed forWhich handlers, jobs, or step classes hide collaborators behind new or take more than ten constructor dependencies? Look at each constructor the change adds.

Marked guidance: a reviewer answers this question.

Read No Repository Inside a Loop#

rulerulecheckable
litespec/backend/application#read-no-repository-inside-a-loop
Statement

Each handler and job MUST read no repository inside a loop body.

Applies toartifact: command-handler, job, query-handler

Why

A read inside a loop turns one handling into as many round trips as there are items, and the slowdown hides behind small test data. A batch read or a scoped query loads every item at once, so the handling costs one trip whatever the data holds.

Example

The reminder job loads due loans with one scoped query instead of reading each loan inside its loop.

Checkchecked by a tool

Checked bylitespec model check reports repository-read-invalid (level A) for a repository read inside a loop body.

Marked checkable: the tool named here checks it.

Conventions#

Use This Use-Case Layout#

conventiondefaultguidance
litespec/backend/application#use-this-use-case-layout
Default

Place each use case in a folder named for it inside the UseCases/ folder of its module and aggregate, with the use case files flat inside and shared types in the node layer folders.

ReplacementA project names its own Application folder layout in its documentation.

Applies toartifact: any

Why

One folder per use case inside its UseCases/ folder holds everything a reviewer needs to judge it in one place, and the module tree lets a reader move from a handler to its endpoint without a map. A use case file's layer comes from its role suffix, so Application files sit beside the endpoint and transport models in the one folder.

Example

The layout reads as follows:

Text
{Product}/
  Application/                            product layer folder: pipeline positions and pipeline stages, and nothing else
  Posts/                                  single aggregate whose name matches the module: use cases sit in the module UseCases folder
    Contracts/
      Identifiers/  Exceptions/  IntegrationEvents/  Commands/  Ports/
    Application/                          a port several Posts use cases name sits here
    UseCases/
      CreateDraft/                        one folder per use case, flat: Command, Query, Result, Handler, Validator,
                                          Endpoint, RequestModel, ResponseModel, mapping
      GetPost/
      ListPosts/
        ListPostsQueryResultItem          a result part named after its result
      OnPostPublished/                    one folder per fact, holding every handler of that fact
        NotifySubscribersOnPostPublishedHandler   one event handler class with its mark
    Jobs/
  Customers/                              two aggregates: use cases nest under each aggregate UseCases folder
    Contracts/
      Identifiers/  Exceptions/  IntegrationEvents/  Commands/  Ports/
    CustomerAccounts/
      Application/                        aggregate-owned types sit in the aggregate layer folder
      UseCases/
        RestrictAccount/
    Consents/
      UseCases/
        GrantConsent/
    UseCases/                             use cases no single aggregate owns, with one On{Fact} folder per reaction
    Jobs/

Module, aggregate, and use case order, including the single-aggregate flat form, follow litespec/backend/architecture#organize-code-by-module-aggregate-and-use-case.

Only a use case or an event handler folder gets a folder below its UseCases/ folder. Any other type sits in the node layer folder that owns it, chosen by litespec/backend/architecture#place-a-type-at-the-lowest-folder-that-holds-its-consumers, so every folder name below a UseCases/ folder names one use case or one fact. An event handler lives in the On{Fact} folder for the fact it handles inside the module UseCases/ folder, named under litespec/workspace/naming#group-event-handlers-by-the-fact-they-handle, with its mark under litespec/backend/application#keep-reaction-implementations-explicit. A folder named for a service, a technical kind, or a caller breaks that reading; it is drift rather than a second convention.

Checka reviewer answers

Reviewed forWhich use-case paths, event handler folders, or result part names break the layout? Look at each path under the modules.

Marked guidance: a reviewer answers this question.

Keep Messages Immutable#

conventiondefaultguidance
litespec/backend/application#keep-messages-immutable
Default

Declare commands, queries, results, and validation errors as immutable values.

ReplacementA project names its own message-shape rules in its documentation.

Applies toartifact: use-case

Why

Immutable values give commands, queries, results, and validation errors a shape no handler mutates mid-pipeline. Typed identifiers and shared-kernel value objects cross the boundary; an aggregate-owned value object does not, so the message carries a primitive and the handler reconstructs it.

Checka reviewer answers

Reviewed forWhich commands, queries, results, or validation errors mutate? Look at each message declaration.

Marked guidance: a reviewer answers this question.

Return Use-Case Results#

conventiondefaultguidance
litespec/backend/application#return-use-case-results
Default

Return only the values the caller needs from an Application result.

ReplacementA project names its own result-shape rules in its documentation.

Applies toartifact: use-case

Why

A result carries only what the caller needs. An aggregate, session, provider response, or transport result leaving Application makes an inner concern part of the outward contract, and every change to that concern breaks callers.

Checka reviewer answers

Reviewed forWhich Application results return more than the caller needs? Look at each result beside its caller.

Marked guidance: a reviewer answers this question.

Keep Mappings at the Owning Boundary#

conventiondefaultguidance
litespec/backend/application#keep-mappings-at-the-owning-boundary
Default

Own domain-to-result projection in Application, transport mapping in WebApi, and view models in the frontend.

ReplacementA project names its own mapping homes in its documentation.

Applies toartifact: any

Why

A mapping changes when its output shape changes, so it lives with the boundary that defines that shape: domain-to-result projection in Application, transport mapping in WebApi, view models in the frontend. A mapping kept elsewhere breaks when the owning boundary evolves.

Checka reviewer answers

Reviewed forWhich conversions live outside the layer owning their output shape? Look at each conversion beside its output.

Marked guidance: a reviewer answers this question.

Run Heavy Work as a Request Then a Build#

conventiondefaultguidance
litespec/backend/application#run-heavy-work-as-a-request-then-a-build
Default

Record the request behind a command that assembles a large artifact, and let a job build the artifact for the requester to read back.

ReplacementA project names its own request-and-build shape for large artifacts in its documentation.

Applies toartifact: command-handler, job

Why

A command that reads many repositories to assemble a large response holds the caller waiting and times out under load. Recording the request answers at once, the build runs in the background, and the requester reads the state of its own request. Which artifact counts as large depends on the data and the load of the project, so that bound sits in the shape the project names.

Example

The export-expenses command records its request, and a job builds the file the requester then downloads.

Checka reviewer answers

Reviewed forWhich commands assembling a large artifact skip the recorded request and building job? Look at each heavy command.

Marked guidance: a reviewer answers this question.