Type to search pages and rules.

Browse pages

Naming and Code Style

Names are the first documentation a reader meets: a name states the business intent and architectural role without opening the file. In a LiteSpec codebase every architectural role carries a fixed suffix, every type an aggregate owns starts with its root's name, and every frontend file follows one casing map. This page states those suffixes, anchors, and casings, with the exception-naming, boolean, async, and language-version rules beside them, for anyone who names code, and especially for agents working across sessions.

Markdown14 provisions

Stack: any Layer: workspace

Concepts#

Anchored Names#

An aggregate root is the single type through which outside code changes an aggregate. Every type the aggregate owns starts its qualified name with the root's full name, so the name reveals its owner without folder context. PostPublishedEvent and PostState.Published both lead with Post; a namespace-level PublishedState does not.

Role Suffixes#

Each architectural role carries a fixed suffix: a command handler ends in CommandHandler, a result part carries its result name, an endpoint in Endpoint. The Role Suffixes table below lists every role with its pattern and one example. Full suffixes keep coordination apart from projection and boundary data apart from business objects. In a use case folder the suffix also states the file's layer: a type whose name ends in Endpoint, RequestModel, or ResponseModel is WebApi, and one ending in Command, Query, Result, Handler, or Validator is Application.

RolePatternExample
Command message{UseCase}CommandCreateDraftCommand
Command result{UseCase}CommandResultCreateDraftCommandResult
Command handler{UseCase}CommandHandlerCreateDraftCommandHandler
Command validator{UseCase}CommandValidatorCreateDraftCommandValidator
Query message{UseCase}QueryGetPostQuery
Query result{UseCase}QueryResultGetPostQueryResult
Query result part{UseCase}QueryResult{Part}ListPostsQueryResultItem
Command result part{UseCase}CommandResult{Part}SubmitExpenseCommandResultLine
Query handler{UseCase}QueryHandlerGetPostQueryHandler
Query validator{UseCase}QueryValidatorGetPostQueryValidator
Aggregate state base{Aggregate}StatePostState
Aggregate state case, flat{Aggregate}{State}StatePostPublishedState
Aggregate state case, nested{Aggregate}State.{State}PostState.Published
Domain event{Aggregate}{PastFact}EventPostPublishedEvent
Event handler{Action}On{Fact}HandlerNotifySubscribersOnPostPublishedHandler
Repository implementation{Aggregate}RepositoryPostRepository
Endpoint{UseCase}EndpointCreateDraftEndpoint
HTTP request model{UseCase}RequestModelCreateDraftRequestModel
HTTP response model{UseCase}ResponseModelCreateDraftResponseModel
HTTP response item model{UseCase}ResponseItemModelListPostsResponseItemModel
HTTP pagination modelPaginationModelPaginationModel
Polymorphic transport model base{Concept}ModelFulfillmentOutcomeModel
Polymorphic transport model case{Case}{Concept}ModelShippedFulfillmentOutcomeModel
Persistence configuration{PersistedType}ConfigurationPostConfiguration
Registration class{Layer}ServiceRegistrationInfrastructureServiceRegistration

Standards#

Anchor Aggregate-Owned Types on the Aggregate Root#

ruleruleguidance
litespec/workspace/naming#anchor-aggregate-owned-types-on-the-aggregate-root
Statement

An aggregate-owned type's qualified name MUST start with its aggregate root's full name.

Applies toartifact: aggregate

Why

Leading with the root name reveals the owner without folder context. The rule covers events, states, child entities, values, and discriminated unions. A type nested inside a declaring type inherits that type's prefix through its qualified name, so PostState.Published satisfies the rule while a flat PostStatePublishedState repeats the anchor twice.

Example

PostPublishedEvent and PostState.Published both lead with Post. PublishedState declared at namespace level does not.

Checka reviewer answers

Reviewed forEach aggregate-owned type name in apps/api/src begins with its aggregate root name, through nesting or prefix.

Marked guidance: a reviewer answers this question.

Use Architectural Suffixes#

rulerulecheckable
litespec/workspace/naming#use-architectural-suffixes
Statement

An architectural type MUST carry the suffix its role assigns in the Role Suffixes table.

Applies toartifact: state-record, domain-event, use-case, command-handler, query-handler, validator, reaction, repository, endpoint, job

Example

An event handler folder starts with On and names the business fact without the Event suffix: OnPostPublished holds NotifySubscribersOnPostPublishedHandler for PostPublishedEvent (litespec/workspace/naming#group-event-handlers-by-the-fact-they-handle). The On prefix already marks the handler, and folder names carry no technical suffix, so OnPostPublishedEvent is wrong. The event type keeps its Event suffix (litespec/workspace/naming#anchor-aggregate-owned-types-on-the-aggregate-root).

Application types keep their full suffix: {UseCase}Result, {UseCase}Handler, and {UseCase}Validator shorten away the command-or-query distinction the full name carries. A result part carries the full name of its result, as litespec/workspace/naming#name-result-parts-after-their-result states, not a bare summary name like PostSummary. The full role suffix distinguishes command coordination from query projection without opening the file.

HTTP transport types keep their full suffix too: {UseCase}Request and {UseCase}Response shorten away the boundary marking. Model marks the type as passive boundary data rather than an operation or rich business object. Other project-owned, passive HTTP DTOs also name their concrete role and end in Model, such as ListPostsResponseItemModel or PaginationModel.

A polymorphic transport model mirrors a Domain discriminated union (litespec/backend/api#mirror-a-domain-closed-set-as-a-transport-model-of-the-same-shape). Its abstract base names the concept and each sealed case names its case, with both ending in Model: FulfillmentOutcomeModel and ShippedFulfillmentOutcomeModel.

The transport name drops the Domain union's aggregate prefix, such as OrderFulfillmentOutcome, while its discriminator string keeps the Domain union's stable case code unchanged.

Checkchecked by a tool

Checked bydotnet build reports LS2005 for a handler missing its command or query suffix, LS2006 for an endpoint missing its suffix, and LS2007 for a reaction or job whose name hides its kind, in the dotnet stack; each other type name in apps/api/src and each frontend file name carries the suffix its role assigns in the Role Suffixes table.

Marked checkable: the tool named here checks it.

Name Result Parts After Their Result#

rulerulecheckable
litespec/workspace/naming#name-result-parts-after-their-result
Statement

Each type nested in a use-case result MUST be named after the result it belongs to, as {UseCase}QueryResult{Part} or {UseCase}CommandResult{Part}.

Applies toartifact: use-case

Why

A result part named for its own shape instead of its result collides with every other use case shaping the same thing: two summaries named PostSummary answer different queries with different fields. Leading with the full result name keeps each part beside the result it belongs to in every listing, and keeps command coordination apart from query projection without opening the file.

Example

ListPostsQueryResultItem belongs to ListPostsQueryResult, and ReadLoanQueryResultHold belongs to ReadLoanQueryResult.

Checkchecked by a tool

Checked bydotnet build reports LS2009 for a type a use-case result reaches through its properties whose name does not start with the result name, in the dotnet stack.

Marked checkable: the tool named here checks it.

Group Event Handlers by the Fact They Handle#

rulerulecheckable
litespec/workspace/naming#group-event-handlers-by-the-fact-they-handle
Statement

Each event handler MUST live in a folder named On{Fact}, the event name without its Event suffix, inside the UseCases/ folder of the subscriber module, and be named {Action}On{Fact}Handler.

Applies toartifact: reaction

Why

The folder answers "what happens when this fact is recorded?" with one listing: one folder holds every handler of that fact in that module. The subscriber is the module whose records the handler changes, or for a handler that changes nothing, the module that owns the fact. A folder named for the handler instead scatters one fact across as many folders as handlers.

Example

Lending/UseCases/OnLoanBorrowed/SendDueNoticeOnLoanBorrowedHandler sends the due notice when a loan is borrowed.

Checkchecked by a tool

Checked bylitespec model check reports code-placement-drift (level S) for an event handler declared outside the On{Fact} folder its event names.

Marked checkable: the tool named here checks it.

Name Repository Variables After Their Interface#

rulerulecheckable
litespec/workspace/naming#name-repository-variables-after-their-interface
Statement

Each repository parameter, field or local MUST be named after its interface without the I prefix, ending in Repository.

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

Why

A short plural name hides which aggregate the variable guards: orders could be a list, a query, or the repository, and each reader guesses. Naming the variable after its interface keeps one name per repository at every use site, so a reviewer reads the dependency from the name alone.

Example

A parameter of type IExpenseRepository is named expenseRepository, and a field of type ILoanRepository is named loanRepository.

Checkchecked by a tool

Checked bydotnet build reports LS2010 for a repository parameter, field, or local whose name does not follow its interface, in the dotnet stack.

Marked checkable: the tool named here checks it.

Name Asynchronous Operations Completely#

ruleruleguidance
litespec/workspace/naming#name-asynchronous-operations-completely
Statement

An asynchronous operation MUST reveal its asynchrony in its name and accept cancellation explicitly when the operation is cancelable.

Applies toartifact: command-handler, query-handler, validator, reaction, port, repository, endpoint, migration, job

Why

A caller decides from the signature whether to await and what to cancel. Naming the asynchrony and accepting cancellation explicitly put both answers in the signature. An operation is cancelable when it performs I/O or awaits; anything else has nothing to cancel.

Checka reviewer answers

Reviewed forEach asynchronous operation name reveals asynchrony, and each cancelable operation signature takes cancellation explicitly; read the public signatures in apps/api/src.

Marked guidance: a reviewer answers this question.

Name Exceptions by Failed Rule#

ruleruleguidance
litespec/workspace/naming#name-exceptions-by-failed-rule
Statement

An exception name MUST state the rule that failed, using {DomainType}{Reason}Exception for a Domain rejection.

Applies toartifact: aggregate

Why

An exception name is the first diagnosis: it states which rule failed on which type. {DomainType}{Reason}Exception puts both in the name. DomainType is the aggregate root or an anchored owned type, so the name always starts with the aggregate root.

Checka reviewer answers

Reviewed forEach exception name states its failed rule, with Domain rejections in {DomainType}{Reason}Exception form leading with the aggregate root.

Marked guidance: a reviewer answers this question.

Use Intent-Revealing Boolean Names#

ruleadviceguidance
litespec/workspace/naming#use-intent-revealing-boolean-names
Statement

A boolean property or method MUST use Is, Has, Can, or a precise verb.

Applies toartifact: aggregate, state-record, value-object, domain-event, use-case, command-handler, query-handler, validator, reaction, port, repository, endpoint, migration, job

Why

A boolean answers a yes-or-no question, so its name asks one. Is, Has, Can, or a precise verb make the question explicit: HasLines and CanPublish state a condition. LinesPresent and CheckPublish state neither a question nor an answer, so the caller reads the body to learn the polarity.

Checka reviewer answers

Reviewed forEach boolean property and method name in apps/api/src starts with Is, Has, Can, or a precise verb.

Marked guidance: a reviewer answers this question.

Use Current Language Features#

ruleadviceguidance
litespec/workspace/naming#use-current-language-features
Statement

Production code MUST target the pinned language version and use its current construct over an older equivalent.

Applies toartifact: aggregate, state-record, value-object, domain-event, use-case, command-handler, query-handler, validator, reaction, port, repository, endpoint, migration, job

Why

A pinned language version gives every writer the same constructs. Using the current construct over its older equivalent keeps one idiom across the codebase instead of one per author. In the dotnet stack, collection expressions, pattern matching, and primary constructors express the same intent with less incidental code.

Checka reviewer answers

Reviewed forProduction code targets the pinned language version and uses its current constructs; read the toolchain pins and the language idioms in apps/api/src.

Marked guidance: a reviewer answers this question.

Use Predictable Frontend Names#

ruleruleguidance
litespec/workspace/naming#use-predictable-frontend-names
Statement

A frontend folder MUST use kebab-case, a component file and its export PascalCase, and a React hook file use-{name}.

Applies toartifact: component, route, form

Why

Kebab-case folders, PascalCase components with matching exports, and use-{name} hooks give every file one predictable name. One mapping lets a reader move between the documentation tree and the source tree without translation, and lets an agent derive the file from the feature name.

Checka reviewer answers

Reviewed forEach frontend folder uses kebab-case, each component file and export uses PascalCase, and each React hook file uses use-{name}.

Marked guidance: a reviewer answers this question.

Conventions#

Align Business Names Across Layers#

conventiondefaultguidance
litespec/workspace/naming#align-business-names-across-layers
Default

Use one business name for a module across Domain, Application, endpoints, features, and documentation.

ReplacementA project whose module carries different names per layer records its own per-layer names in its project documentation.

Applies toartifact: record:module

Why

One business name per module keeps every layer pointing at the same word. The Posts module maps to Shop/Posts, features/posts, and .litespec/domain/modules/posts without translation. A second name in any layer reads as a second concept that does not exist.

Checka reviewer answers

Reviewed forOne business name for each module spans Domain, Application, endpoints, features, and documentation; where replaced, the project documentation names the project's own per-layer names.

Marked guidance: a reviewer answers this question.

Keep Namespaces Aligned with Folders#

conventiondefaultguidance
litespec/workspace/naming#keep-namespaces-aligned-with-folders
Default

Start a namespace with the product name and follow the folders beneath the project root.

ReplacementA project whose layout needs another root records its own namespace root in its project documentation.

Applies toartifact: aggregate, state-record, value-object, domain-event, use-case, command-handler, query-handler, validator, reaction, port, repository, endpoint, migration, job

Why

Starting the namespace with the product name and following the folders beneath it puts each type exactly where its namespace says. Including src or apps couples the type name to a workspace layout decision. The project root is the directory holding the project file, so every folder above it stays outside the namespace.

Checka reviewer answers

Reviewed forEach namespace starts with its product name and follows the folders beneath the project root; where replaced, the project documentation names the project's own namespace root.

Marked guidance: a reviewer answers this question.

Avoid Generic Type Names#

conventionadviceguidance
litespec/workspace/naming#avoid-generic-type-names
Default

Avoid Manager, Helper, Processor, Common, Utility, BaseService, DataService, MessageBus, Engine, Facade, Wrapper, and Factory when a narrower name exists.

ReplacementA project that keeps a listed name records the name and its reason in its project documentation.

Applies toartifact: aggregate, state-record, value-object, domain-event, use-case, command-handler, query-handler, validator, reaction, port, repository, endpoint, migration, job

Why

A generic name invites unrelated responsibility, because nothing in it excludes the next addition. A narrower name states what the type does and, by exclusion, what it does not. Factory is generic on a class and exact on a named static aggregate factory method, so the default names the class and not the method.

Checka reviewer answers

Reviewed forEach type name in apps/api/src avoids the listed generic names where a narrower name exists; where replaced, the project documentation names the kept name and its reason.

Marked guidance: a reviewer answers this question.

Derive Boundary Names from the Ubiquitous Term#

conventiondefaultguidance
litespec/workspace/naming#derive-boundary-names-from-the-ubiquitous-term
Default

Derive route segments and JSON field names from the current aggregate or ubiquitous term.

ReplacementA project with client-facing names that predate its glossary records its own boundary naming in its project documentation.

Applies toartifact: use-case, endpoint

Why

Route segments and JSON field names are the contract clients read. Deriving them from the current aggregate or ubiquitous term, the Domain's shared word for the concept, keeps the contract speaking the Domain language. Renaming a Domain concept then renames its boundary names, so the contract and the language stay aligned.

Checka reviewer answers

Reviewed forEach route segment and JSON field name derives from its current aggregate or ubiquitous term; where replaced, the project documentation names the project's own boundary naming.

Marked guidance: a reviewer answers this question.