Skip to content

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:

1One class per fileThe types that belong to a class, such as its snapshot or its command input, stay in its file.
2The folder and file of its kindA class that extends a building block is where the table below says, whatever its name.
ExtendsFolderFile name ends with
AggregateRootdomain/aggregates/.aggregate.ts
Entitydomain/entities/.entity.ts
Identifierdomain/value-objects/.identifier.ts
ValueObjectdomain/value-objects/.value-object.ts
DomainEventdomain/events/.event.ts
DomainErrordomain/errors/.error.ts
DomainServicedomain/services/.service.ts
CommandRepository
QueryRepository
domain/repositories/.repository.ts
Port, abstractdomain/ports/.port.ts
CommandHandlerapplication/commands/.command.ts
QueryHandlerapplication/queries/.query.ts
EventTranslatorapplication/translators/.translator.ts
a port, concretedriven/<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.

❌ Avoid: src/ordering/domain/order.ts
ts
import { AggregateRoot, Identifier } from "@alveolus/core";

export class OrderId extends Identifier<string, "OrderId"> {}

export class Order extends AggregateRoot<OrderId> {}
✅ Prefer: src/ordering/domain/aggregates/order.aggregate.ts
ts
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.

src/ordering/application/commands/place-order.command.ts
ts
export interface PlaceOrder {
	readonly orderId: string;
}

export type PlaceOrderError =
	| OrderNotFound
	| OrderAlreadyPlaced
	| EmptyOrder;

export class PlaceOrderHandler extends CommandHandler<
	PlaceOrder,
	void,
	PlaceOrderError
> { … }

Turn it off ​

alveolus.config.ts
ts
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 ​

Released under the MIT License.