Skip to content

no-plain-class ​

The domain and the application contain building blocks, and nothing else: every class extends one from @alveolus/core.

Rule
tactical/no-plain-class
Category
Tactical: how building blocks are written
Reports
A plain class, a free function or an enum
Applies to
Files in domain/ and application/, in every bounded context and the shared kernel
Turn off
"tactical/no-plain-class": "off"

Why ​

Rounding an amount needs a few lines, so a roundAmount function lands in pricing.ts. The next helper goes next to it, then a utils.ts appears, and the model slowly dissolves into code that has no defined place and that no other rule knows how to treat.

The fix

Every element of the domain and the application is a building block. Rounding belongs to Money, a value object; a rule that no object owns goes in a DomainService. Each one has a home that people and agents can find, and the rest of the rules know what it is.

What it checks ​

Every top-level declaration of a file in domain/ or application/:

DeclarationAllowed when
A classIt extends a building block of @alveolus/core, directly or through your own base class.
A function, or a constant holding oneNever.
An enumNever.

The building blocks to extend are, in the domain, AggregateRoot, Entity, ValueObject, Identifier, DomainEvent, DomainError, DomainService, a Port or a repository; in the application, CommandHandler, QueryHandler or EventTranslator.

AllowedTypes, interfaces and constants holding data, and functions written inside a method.
Not checkedAdapters in driven/ and driving/, and composition roots: they may hold any class or function.

What it reports ​

src/ordering/domain/services/pricing.ts:1
  tactical/no-plain-class: PriceHelper extends no building
  block: extend AggregateRoot, Entity, ValueObject, Identifier,
  DomainEvent, DomainError, DomainService or a Port.

src/ordering/domain/services/pricing.ts:3
  tactical/no-plain-class: The function roundAmount floats
  outside any class: make it a method of a value object or of
  a DomainService.

src/ordering/domain/services/pricing.ts:7
  tactical/no-plain-class: The enum OrderStatus has no place
  here: use a union of literal types, or a ValueObject when it
  has behaviour.

In the application, the message lists CommandHandler, QueryHandler or EventTranslator.

Fix it ​

Find the building block it belongs to ​

So that each piece of logic has a known home, replace each declaration with the building block that owns it:

Instead ofWrite
A helper functionA method of the value object it works on, or of a DomainService when no object owns it.
A plain class (policy, calculator, specification)A DomainService.
A factory functionA static method of the aggregate, entity or value object.
A mapper in the applicationAn EventTranslator, or a mapper in an adapter.
An enumA union of literal types, or a value object when the values have behaviour.
❌ Avoid: src/ordering/domain/services/pricing.ts
ts
export class PriceHelper {}

export function roundAmount(amount: number): number {
	return Math.round(amount * 100) / 100;
}

export enum OrderStatus {
	Draft,
	Placed,
}
✅ Prefer: src/ordering/domain/value-objects/money.value-object.ts
ts
import { ValueObject } from "@alveolus/core";

export class Money extends ValueObject<{ readonly amount: number }> {
	rounded(): Money {
		return new Money({
			amount: Math.round(this.props.amount * 100) / 100,
		});
	}
}

Return business failures as domain errors ​

So that callers see in the signature what can go wrong, a business failure is not a subclass of Error: it is a DomainError, returned in a Result. A bug stays a plain new Error("…"), thrown.

❌ Avoid: src/ordering/domain/errors/price-too-high.error.ts
ts
export class PriceTooHigh extends Error {}
✅ Prefer: src/ordering/domain/errors/price-too-high.error.ts
ts
import { DomainError } from "@alveolus/core";

export class PriceTooHigh extends DomainError<{ readonly max: number }> {}

Turn it off ​

alveolus.config.ts
ts
rules: { "tactical/no-plain-class": "off" },

On an existing project, prefer a baseline: new code keeps to building blocks while you move the old helpers.

See also ​

Released under the MIT License.