no-misplaced-class
Each class lives in the folder of its kind, in a file whose name ends with that kind, one class per file.
- Rule
tactical/no-misplaced-class- Category
- Tactical: how building blocks are written
- Reports
- A class in the wrong folder or file, two classes in one file
- Applies to
- Every class that extends a building block, in every bounded context and the shared kernel
- Turn off
"tactical/no-misplaced-class": "off"
Why
OrderId and Order are declared together in domain/order.ts, because that is where the task started. The next developer looks for the identifier in value-objects/ and does not find it; the next agent creates a second one there. The shared layout only helps if it holds.
The fix
The kind of a class decides where it lives: Order extends AggregateRoot, so it is in domain/aggregates/order.aggregate.ts, alone. Finding a concept takes no search, a review shows at a glance what a change touches, and an agent puts new code where the existing code is.
What it checks
Two things, in every file:
| Extends | Folder | File name ends with |
|---|---|---|
AggregateRoot | domain/aggregates/ | .aggregate.ts |
Entity | domain/entities/ | .entity.ts |
Identifier | domain/value-objects/ | .identifier.ts |
ValueObject | domain/value-objects/ | .value-object.ts |
DomainEvent | domain/events/ | .event.ts |
DomainError | domain/errors/ | .error.ts |
DomainService | domain/services/ | .service.ts |
CommandRepositoryQueryRepository | domain/repositories/ | .repository.ts |
Port, abstract | domain/ports/ | .port.ts |
CommandHandler | application/commands/ | .command.ts |
QueryHandler | application/queries/ | .query.ts |
EventTranslator | application/translators/ | .translator.ts |
| a port, concrete | driven/<technology>/adapters/ | .adapter.ts |
A class that implements OpenHostService or AntiCorruptionLayer without extending a port lives under driving/, with any file name. Classes that extend no building block, such as a controller or a module, are not placed by this rule.
What it reports
src/ordering/domain/order.ts:3
tactical/no-misplaced-class: OrderId belongs in
domain/value-objects/*.identifier.ts.
src/ordering/domain/order.ts:5
tactical/no-misplaced-class: Order shares its file
with OrderId: one class per file.
src/ordering/domain/order.ts:5
tactical/no-misplaced-class: Order belongs in
domain/aggregates/*.aggregate.ts.Fix it
Move each class to the file of its kind
Split the file, then move each class to the folder and file name the message gives. Import the others from their new place.
import { AggregateRoot, Identifier } from "@alveolus/core";
export class OrderId extends Identifier<string, "OrderId"> {}
export class Order extends AggregateRoot<OrderId> {}import { AggregateRoot } from "@alveolus/core";
import type { OrderId } from "../value-objects/order-id.identifier";
export class Order extends AggregateRoot<OrderId> {}Keep the types of a class in its file
A snapshot, a command input or an error union is not a class: it stays next to the class it belongs to, and the rule does not report it.
export interface PlaceOrder {
readonly orderId: string;
}
export type PlaceOrderError =
| OrderNotFound
| OrderAlreadyPlaced
| EmptyOrder;
export class PlaceOrderHandler extends CommandHandler<
PlaceOrder,
void,
PlaceOrderError
> { … }Turn it off
rules: { "tactical/no-misplaced-class": "off" },On an existing project, prefer a baseline: new code keeps the layout while you move the old one.
See also
- Project layout: folders and file names
tactical/no-plain-class, so that every class has a kind- Rules, every rule by category