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.
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.
| Role | Pattern | Example |
|---|---|---|
| Command message | {UseCase}Command | CreateDraftCommand |
| Command result | {UseCase}CommandResult | CreateDraftCommandResult |
| Command handler | {UseCase}CommandHandler | CreateDraftCommandHandler |
| Command validator | {UseCase}CommandValidator | CreateDraftCommandValidator |
| Query message | {UseCase}Query | GetPostQuery |
| Query result | {UseCase}QueryResult | GetPostQueryResult |
| Query result part | {UseCase}QueryResult{Part} | ListPostsQueryResultItem |
| Command result part | {UseCase}CommandResult{Part} | SubmitExpenseCommandResultLine |
| Query handler | {UseCase}QueryHandler | GetPostQueryHandler |
| Query validator | {UseCase}QueryValidator | GetPostQueryValidator |
| Aggregate state base | {Aggregate}State | PostState |
| Aggregate state case, flat | {Aggregate}{State}State | PostPublishedState |
| Aggregate state case, nested | {Aggregate}State.{State} | PostState.Published |
| Domain event | {Aggregate}{PastFact}Event | PostPublishedEvent |
| Event handler | {Action}On{Fact}Handler | NotifySubscribersOnPostPublishedHandler |
| Repository implementation | {Aggregate}Repository | PostRepository |
| Endpoint | {UseCase}Endpoint | CreateDraftEndpoint |
| HTTP request model | {UseCase}RequestModel | CreateDraftRequestModel |
| HTTP response model | {UseCase}ResponseModel | CreateDraftResponseModel |
| HTTP response item model | {UseCase}ResponseItemModel | ListPostsResponseItemModel |
| HTTP pagination model | PaginationModel | PaginationModel |
| Polymorphic transport model base | {Concept}Model | FulfillmentOutcomeModel |
| Polymorphic transport model case | {Case}{Concept}Model | ShippedFulfillmentOutcomeModel |
| Persistence configuration | {PersistedType}Configuration | PostConfiguration |
| Registration class | {Layer}ServiceRegistration | InfrastructureServiceRegistration |
Standards#
Anchor Aggregate-Owned Types on the Aggregate Root#
ruleruleguidancelitespec/workspace/naming#anchor-aggregate-owned-types-on-the-aggregate-rootAn 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#
rulerulecheckablelitespec/workspace/naming#use-architectural-suffixesAn 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#
rulerulecheckablelitespec/workspace/naming#name-result-parts-after-their-resultEach 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#
rulerulecheckablelitespec/workspace/naming#group-event-handlers-by-the-fact-they-handleEach 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#
rulerulecheckablelitespec/workspace/naming#name-repository-variables-after-their-interfaceEach 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#
ruleruleguidancelitespec/workspace/naming#name-asynchronous-operations-completelyAn 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#
ruleruleguidancelitespec/workspace/naming#name-exceptions-by-failed-ruleAn 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#
ruleadviceguidancelitespec/workspace/naming#use-intent-revealing-boolean-namesA 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#
ruleadviceguidancelitespec/workspace/naming#use-current-language-featuresProduction 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#
ruleruleguidancelitespec/workspace/naming#use-predictable-frontend-namesA 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#
conventiondefaultguidancelitespec/workspace/naming#align-business-names-across-layersUse 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#
conventiondefaultguidancelitespec/workspace/naming#keep-namespaces-aligned-with-foldersStart 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#
conventionadviceguidancelitespec/workspace/naming#avoid-generic-type-namesAvoid 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#
conventiondefaultguidancelitespec/workspace/naming#derive-boundary-names-from-the-ubiquitous-termDerive 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.