Named Slices of Reusable Domain Logic
Some behaviour is the same across several aggregates without belonging to any one of them. Retryable is the same shape on PaymentProcessing, EmailDispatch, WebhookDelivery — same retry-count field, same back-off strategy, same expiry policy. Re-implementing it three times invites drift; pulling it into a shared abstract base couples three aggregates that should not depend on each other. @Behavior from @frenchexdev/ddd-behavior reifies the third option: a named, mixin-style slice of behaviour that aggregates may compose with.
What @Behavior Reifies
A behaviour is a vocabulary contribution: it adds named methods, named fields, named invariants to whatever aggregate composes it. Retryable contributes attempts: number, recordFailure(), isExhausted(), nextBackoffAt(): Date. Three aggregates that compose Retryable all gain those methods; the implementation is shared; the contract is the behaviour's, not each aggregate's.
The pattern is operationally close to TypeScript mixins but with discipline. Named in the ubiquitous language: the behaviour has a stakeholder-recognisable name (Retryable, Auditable, Reversible), not a technical one (RetryMixin, AuditHelper). State-free at the behaviour level: any state declared by the behaviour belongs to the composing aggregate; the behaviour describes the methods and the shape, the aggregate holds the values. Composable: an aggregate may compose multiple behaviours, and the analyzer will eventually verify that the behaviours' contributions do not conflict.
The Runtime: ddd-behavior
The runtime is at the M4/M5 stub milestone. The decorator surface and the composition mechanism will land when a consumer drives the design — TypeScript's mixin shapes are non-trivial, and the corpus is holding the decision for a real use case rather than guessing.
The expected shape (sketched, not shipped):
// Sketch — the runtime decorator is not yet exported.
@Behavior({ name: 'Retryable', invariants: ['attempts >= 0', 'expiresAt > now'] })
export class RetryableBehavior {
attempts: number = 0;
expiresAt: string;
recordFailure(): void { this.attempts += 1; }
isExhausted(maxAttempts: number): boolean { return this.attempts >= maxAttempts; }
}
@AggregateRoot({ /* ... */ })
@Compose(RetryableBehavior)
export class PaymentProcessing { /* gains attempts, recordFailure, isExhausted */ }// Sketch — the runtime decorator is not yet exported.
@Behavior({ name: 'Retryable', invariants: ['attempts >= 0', 'expiresAt > now'] })
export class RetryableBehavior {
attempts: number = 0;
expiresAt: string;
recordFailure(): void { this.attempts += 1; }
isExhausted(maxAttempts: number): boolean { return this.attempts >= maxAttempts; }
}
@AggregateRoot({ /* ... */ })
@Compose(RetryableBehavior)
export class PaymentProcessing { /* gains attempts, recordFailure, isExhausted */ }The @Compose decorator (also not yet shipped) is the mechanism — at codegen time, the aggregate's Base class will be assembled with the behaviour's methods folded in.
Behavior vs. Domain Service
The two patterns overlap in scope and the distinction is worth dwelling on. A @DomainService is invoked from outside an aggregate — the application service or another aggregate calls it. A @Behavior is composed into an aggregate — methods become part of the aggregate's surface, not a separate object.
Use a domain service when the operation spans multiple aggregates or has no natural home. Use a behaviour when the same vocabulary (the same field names, the same method shapes) needs to appear on multiple aggregates that are otherwise unrelated. The two compose: a behaviour may invoke a domain service in its method body; a domain service may operate on aggregates that compose behaviours.
Cross-Links
- Composes into
@AggregateRootand@Entity, contributing methods and field shapes. - Distinct from
@DomainService— behaviour is into, service is from outside. - Pairs with
@DesignByContractwhen the behaviour declares invariants the composing aggregate must satisfy. - Will be registered by the
@Module's export list so the analyzer can validate composition surfaces.
Back to the series index.