Le package ddd

Les primitives communes à tous les bounded contexts

Bootcode IWA-S04 — Semaine 18, Jour 1

Objectifs de la leçon

1. Explorer le package

Connaître le rôle de chaque fichier de packages/tools/ddd/

2. AggregateRoot & applyChange

Comprendre le lien entre AggregateRoot, applyChange et @Handle

3. Primitives en action

ValueObject, DomainEvent, Usecase, Query, Mapper, DomainError

4. Diagramme de classes

Être capable de dessiner l'architecture du package

Plan du cours

1

Vue d'ensemble du package

Les briques de base du codebase

2

AggregateRoot

La classe mère de tout agrégat event-sourcé

3

ValueObject, DomainEvent, Usecase

Les primitives du domaine et de l'orchestration

4

Query, Mapper, DomainError

Lecture, persistence et erreurs typées

5

Un vrai agrégat du codebase

Rendre tout ça concret

Module 1

Vue d'ensemble du package

packages/tools/ddd/ — le socle commun

Pourquoi un package ddd ?

On évite de réinventer la roue dans chaque bounded context

Le package ddd centralise les abstractions que tous les contextes métiers partagent.

🧱

AggregateRoot

Tout agrégat en hérite

📦

ValueObject

Valeurs immuables

DomainEvent

Événements métier

⚙️

Usecase

Orchestration

🗺️

Mapper

Persistence

🛡️

DomainError

Erreurs typées

Module 2

AggregateRoot

La classe mère de tout agrégat

AggregateRoot : la base de tout

props

L'état mutable de l'agrégat, typé et encapsulé

uncommittedChanges

Les événements nouvellement produits, pas encore persistés

applyChange(event)

Enregistre l'événement ET appelle le handler @Handle

@Handle(EventClass)

Décorateur qui relie un événement à sa mutation d'état

💡 Tout agrégat métier hérite de cette classe. C'est le coeur du package.

AggregateRoot en code

Le squelette du package packages/tools/ddd/AggregateRoot.ts

// packages/tools/ddd/AggregateRoot.ts

export abstract class AggregateRoot<Props> {

protected props: Props;

private uncommittedChanges: DomainEvent[] = [];

protected constructor(props: Props) {

this.props = props;

}

protected applyChange(event: DomainEvent): void {

this.uncommittedChanges.push(event);

this.handleEvent(event);

}

private handleEvent(event: DomainEvent): void {

// @Handle relie la classe d'événement à sa méthode

}

getUncommittedEvents(): DomainEvent[] {

return [...this.uncommittedChanges];

}

}

💡 applyChange fait 2 choses : stocker l'événement ET muter l'état.

Le décorateur @Handle

Relie un événement à la méthode qui mute l'état.

// Décorateur

export function Handle(eventClass: any) {

return function (

target: any,

propertyKey: string

) {

const meta =

Reflect.getMetadata('handles', target) || {};

meta[eventClass.name] = propertyKey;

Reflect.defineMetadata(

'handles', meta, target

);

};

}

// Utilisation dans un agrégat

class BankAccount extends AggregateRoot<BankAccountProps> {

@Handle(MoneyDeposited)

onMoneyDeposited(event: MoneyDeposited) {

this.props.balance += event.payload.amount;

}

}

🚨 Attention : @Handle est pour la mutation SYNCHRONE dans l'agrégat, pas un handler asynchrone externe.

Module 3

ValueObject & DomainEvent

Données immuables et messages métier

ValueObject

Identifié par ses attributs, pas par un ID. Immuable et comparable par valeur.

// packages/tools/ddd/ValueObject.ts

export abstract class ValueObject<Props> {

protected constructor(

readonly props: Props

) {}

equals(other: ValueObject<Props>): boolean {

return JSON.stringify(this.props) ===

JSON.stringify(other.props);

}

}

// Exemple : Email

class Email extends ValueObject<{ value: string }> {

static create(value: string): Email {

return new Email({ value });

}

}

💡 Deux ValueObjects avec les mêmes attributs sont égaux.

DomainEvent

Représente quelque chose qui s'est passé dans le passé. Immutable. Porte un payload et des métadonnées.

// packages/tools/ddd/DomainEvent.ts

export abstract class DomainEvent {

public readonly id: string;

public readonly occurredAt: Date;

public readonly aggregateId: string;

protected constructor(

aggregateId: string,

public readonly payload: any

) {

this.id = crypto.randomUUID();

this.occurredAt = new Date();

this.aggregateId = aggregateId;

}

}

💡 Un événement = nom au passé + payload + métadonnées (id, date, aggregateId).

Module 4

Usecase, Query, Mapper, DomainError

Orchestration, lecture, persistence et erreurs

Usecase

Orchestre une action métier : autorisation, validation, appel du domaine, persistence.

// packages/tools/ddd/Usecase.ts

export abstract class Usecase<Input, Output> {

abstract execute(input: Input): Promise<Output>;

}

// Exemple concret

class DepositMoneyUsecase extends Usecase<

{ accountId: string; amount: number },

void

> {

constructor(

private repository: BankAccountRepository

) {}

async execute(input): Promise<void> {

const account = await this.repository.findById(input.accountId);

account.deposit(input.amount);

await this.repository.save(account);

}

}

Query

Représente une question qu'on pose au système. Le C du CQRS côté lecture.

// packages/tools/ddd/Query.ts

export abstract class Query<Result> {

abstract execute(): Promise<Result>;

}

// Exemple concret

class GetAccountBalanceQuery extends Query<number> {

constructor(

private readonly accountId: string,

private readonly readModel: AccountReadModel

) {}

async execute(): Promise<number> {

return this.readModel.getBalance(this.accountId);

}

}

💡 Usecase = écriture. Query = lecture. On sépare les responsabilités.

Mapper

Fait le pont entre le domaine et la persistence. Convertit Entity/Aggregat ↔ DTO de persistence.

// packages/tools/ddd/Mapper.ts

export interface Mapper<Domain, Persistence> {

toDomain(raw: Persistence): Domain;

toPersistence(domain: Domain): Persistence;

}

// Exemple : BankAccountMapper

class BankAccountMapper implements Mapper<BankAccount, BankAccountRow> {

toDomain(raw: BankAccountRow): BankAccount {

return BankAccount.restore(raw.id, raw.events);

}

toPersistence(account: BankAccount): BankAccountRow {

return {

id: account.props.id,

events: account.getUncommittedEvents(),

};

}

}

DomainError

Erreur métier typée avec un code machine-readable et un message humain.

// packages/tools/ddd/DomainError.ts

export abstract class DomainError extends Error {

protected constructor(

public readonly code: string,

message: string

) {

super(message);

}

}

// Exemple d'usage

class InsufficientFundsError extends DomainError {

constructor() {

super(

'INSUFFICIENT_FUNDS',

'Le solde est insuffisant pour effectuer ce retrait.'

);

}

}

🛡️ Chaque invariant métier mérite SA propre DomainError.

Module 5

Un vrai agrégat du codebase

Le package ddd en action

Exemple : User dans IAC

Un vrai agrégat qui hérite de AggregateRoot et utilise plusieurs primitives.

// bounded-contexts/iac/domain/User.ts

class User extends AggregateRoot<UserProps> {

static init(email: Email): User {

const user = new User({ id: crypto.randomUUID(), email });

user.applyChange(new UserCreated(user.props.id, { email: email.props.value }));

return user;

}

changeEmail(newEmail: Email): void {

this.applyChange(new UserEmailChanged(

this.props.id,

{ email: newEmail.props.value }

));

}

@Handle(UserCreated)

onUserCreated(event: UserCreated) {

this.props.email = Email.create(event.payload.email);

}

@Handle(UserEmailChanged)

onUserEmailChanged(event: UserEmailChanged) {

this.props.email = Email.create(event.payload.email);

}

}

💡 Ici on voit AggregateRoot + ValueObject (Email) + DomainEvent + @Handle travailler ensemble.

Diagramme du package ddd

L'architecture en une image.

AggregateRoot

applyChange, @Handle

DomainEvent

id, occurredAt, aggregateId

ValueObject

immutable, equals()

Usecase

écriture / commande

Query

lecture

DomainError

code + message

Mapper ↔ Repository ↔ Usecase ↔ AggregateRoot

Le Mapper fait le pont entre le monde persistence et le domaine

Pièges courants

❌ Trop de primitives d'un coup

Les étudiants paniquent face à AggregateRoot, ValueObject, DomainEvent, Usecase, Mapper...

✅ Une par une

On va utiliser chaque primitive au moment où on en a besoin. Pas besoin de tout retenir aujourd'hui.

❌ Confondre @Handle et handler asynchrone

@Handle est une mutation SYNCHRONE dans l'agrégat. Les handlers asynchrones arrivent en S20.

✅ @Handle = mutation d'état interne

Il relie un événement à la méthode qui met à jour props. Rien à voir avec les subscribers.

❌ applyChange est de la magie

Si on ne comprend pas le flux, on ne sait pas où est l'état.

✅ applyChange = enregistrer + muter

Deux étapes claires : push dans uncommittedChanges, puis appel du handler @Handle.

À retenir !

1. Package ddd = socle commun

Tous les bounded contexts partagent ces primitives.

2. AggregateRoot est la base

props, uncommittedChanges, applyChange, @Handle.

3. ValueObject & DomainEvent

Immuables : valeurs comparées par contenu, événements au passé.

4. Usecase, Query, Mapper, DomainError

Ces abstractions structurent l'architecture CQRS et la gestion d'erreurs.

Demain : on plonge dans le mécanisme d'event sourcing en profondeur.