| name | DDD Bounded Context Generator |
| description | Génère des bounded contexts DDD complets avec architecture en couches (Domain, Application, Infrastructure, Presentation). À utiliser lors de la création de nouvelles features backend, bounded contexts, domain entities, ou quand l'utilisateur mentionne "DDD", "bounded context", "domain model", "clean architecture", "layered architecture". |
| allowed-tools | Read, Write, Edit, Glob, Grep, Bash |
DDD Bounded Context Generator
🎯 Mission
Créer des bounded contexts backend suivant rigoureusement les principes DDD (Domain-Driven Design) avec une architecture en couches propre et maintenable.
🏗️ Architecture DDD du Projet
Philosophie DDD
Le backend suit les principes DDD avec une séparation stricte des responsabilités :
- Bounded Contexts : Chaque feature majeure est un bounded context isolé
- Layered Architecture : 4 couches avec dépendances unidirectionnelles vers l'intérieur
- Rich Domain Models : Les entités contiennent la logique métier
- Framework-Agnostic Domain : Le domain ne dépend d'aucun framework
Structure d'un Bounded Context
volley-app-backend/src/[bounded-context]/
├── domain/
│ ├── entities/ # Entités riches avec logique métier
│ ├── value-objects/ # Value Objects immuables
│ ├── repositories/ # Interfaces de repositories (PAS d'implémentation)
│ ├── services/ # Domain Services pour logique complexe
│ └── exceptions/ # Exceptions métier custom
├── application/
│ ├── commands/ # Opérations d'écriture (CQRS)
│ │ └── create-foo/
│ │ ├── create-foo.command.ts
│ │ └── create-foo.handler.ts
│ ├── queries/ # Opérations de lecture (CQRS)
│ │ └── get-foo/
│ │ ├── get-foo.query.ts
│ │ └── get-foo.handler.ts
│ └── read-models/ # DTOs optimisés pour l'UI
├── infrastructure/
│ ├── persistence/
│ │ ├── repositories/ # Implémentations des repositories
│ │ └── mappers/ # Mappers Domain ↔ Prisma
│ └── [external-services]/
├── presentation/
│ └── controllers/ # Controllers HTTP (NestJS)
├── tests/
│ ├── unit/
│ │ ├── domain/ # Tests des entités et services
│ │ └── application/ # Tests des handlers
│ └── integration/ # Tests Handler → Repository → DB
└── [bounded-context].module.ts
📐 Layered Architecture - Règles Strictes
Flow de Dépendances (CRITIQUE)
Presentation → Application → Domain ← Infrastructure
- ✅ Autorisé : Les couches externes dépendent des couches internes
- ❌ INTERDIT : Le Domain ne doit JAMAIS dépendre des couches externes
1. Domain Layer (Cœur Métier)
Responsabilité : Contenir toute la logique métier de l'application
Contenu :
- Entities : Modèles riches avec méthodes métier
- Value Objects : Objets immuables représentant des concepts métier
- Repository Interfaces : Contrats pour la persistence (PAS d'implémentation)
- Domain Services : Logique métier complexe impliquant plusieurs entités
- Domain Exceptions : Exceptions métier custom
Règles STRICTES :
- ✅ Pure TypeScript (aucune dépendance externe)
- ✅ Logique métier encapsulée dans les entités
- ✅ Value Objects immuables et validés
- ✅ Interfaces de repositories uniquement
- ❌ JAMAIS de dépendances vers NestJS
- ❌ JAMAIS de dépendances vers Prisma
- ❌ JAMAIS de dépendances vers les couches externes
- ❌ JAMAIS de code infrastructure (DB, HTTP, etc.)
Template d'Entité Domain :
// domain/entities/subscription.entity.ts
import { SubscriptionPlan } from '../value-objects/subscription-plan.vo';
import { SubscriptionStatus } from '../value-objects/subscription-status.vo';
export class Subscription {
constructor(
private readonly id: string,
private readonly clubId: string,
private plan: SubscriptionPlan,
private status: SubscriptionStatus,
private readonly startDate: Date,
private endDate: Date | null,
private currentTeamsCount: number,
) {
this.validate();
}
// Factory method pour création
static create(clubId: string, plan: SubscriptionPlan): Subscription {
return new Subscription(
crypto.randomUUID(),
clubId,
plan,
SubscriptionStatus.ACTIVE,
new Date(),
null,
0,
);
}
// Validation des invariants
private validate(): void {
if (!this.id) throw new Error('Subscription ID is required');
if (!this.clubId) throw new Error('Club ID is required');
if (this.currentTeamsCount < 0) {
throw new Error('Teams count cannot be negative');
}
}
// Logique métier : Peut-on créer une nouvelle équipe ?
canCreateTeam(): boolean {
if (!this.isActive()) return false;
if (!this.plan.hasTeamLimit()) return true; // Unlimited
return this.currentTeamsCount < this.plan.getMaxTeams();
}
// Logique métier : Upgrade du plan
upgrade(newPlan: SubscriptionPlan): void {
if (!newPlan.isUpgradeFrom(this.plan)) {
throw new Error('Cannot downgrade subscription');
}
this.plan = newPlan;
}
// Getters (pas de setters !)
getId(): string {
return this.id;
}
getClubId(): string {
return this.clubId;
}
getPlan(): SubscriptionPlan {
return this.plan;
}
isActive(): boolean {
return this.status.isActive();
}
// Méthodes de modification retournent une nouvelle instance (immutabilité)
incrementTeamsCount(): void {
if (!this.canCreateTeam()) {
throw new Error('Team limit reached for current plan');
}
this.currentTeamsCount++;
}
}
Template Value Object :
// domain/value-objects/subscription-plan.vo.ts
export class SubscriptionPlan {
private static readonly PLANS = {
FREE: { name: 'Free', maxTeams: 1, price: 0 },
PRO: { name: 'Pro', maxTeams: 3, price: 9.99 },
UNLIMITED: { name: 'Unlimited', maxTeams: -1, price: 29.99 },
};
private constructor(private readonly planName: string) {
if (!Object.keys(SubscriptionPlan.PLANS).includes(planName)) {
throw new Error(`Invalid plan: ${planName}`);
}
}
static FREE = new SubscriptionPlan('FREE');
static PRO = new SubscriptionPlan('PRO');
static UNLIMITED = new SubscriptionPlan('UNLIMITED');
static fromString(planName: string): SubscriptionPlan {
return new SubscriptionPlan(planName);
}
hasTeamLimit(): boolean {
return this.getMaxTeams() !== -1;
}
getMaxTeams(): number {
return SubscriptionPlan.PLANS[this.planName].maxTeams;
}
isUpgradeFrom(otherPlan: SubscriptionPlan): boolean {
const currentPrice = SubscriptionPlan.PLANS[this.planName].price;
const otherPrice = SubscriptionPlan.PLANS[otherPlan.planName].price;
return currentPrice > otherPrice;
}
toString(): string {
return this.planName;
}
}
Template Repository Interface :
// domain/repositories/subscription.repository.interface.ts
import { Subscription } from '../entities/subscription.entity';
export interface ISubscriptionRepository {
create(subscription: Subscription): Promise<Subscription>;
findById(id: string): Promise<Subscription | null>;
findByClubId(clubId: string): Promise<Subscription | null>;
update(subscription: Subscription): Promise<Subscription>;
delete(id: string): Promise<void>;
}
// Token pour injection de dépendances
export const SUBSCRIPTION_REPOSITORY = Symbol('ISubscriptionRepository');
2. Application Layer (Orchestration)
Responsabilité : Orchestrer la logique métier via des use cases (Commands/Queries)
Contenu :
- Commands : Opérations d'écriture (Create, Update, Delete)
- Queries : Opérations de lecture (Get, List, Search)
- Handlers : Exécutent les commands/queries
- Read Models : DTOs optimisés pour l'UI
Règles :
- ✅ Utiliser CQRS (Command Query Responsibility Segregation)
- ✅ Un handler par command/query
- ✅ Valider les inputs avec class-validator
- ✅ Orchestrer les entités domain (pas de logique métier ici)
- ✅ Retourner des IDs pour les commands, Read Models pour les queries
- ✅ Dépendre uniquement du Domain Layer
- ❌ JAMAIS de logique métier (celle-ci est dans le Domain)
- ❌ JAMAIS d'accès direct à Prisma (utiliser les repositories)
Voir la Skill cqrs-command-query pour plus de détails sur les Commands/Queries
3. Infrastructure Layer (Implémentation Technique)
Responsabilité : Implémenter les interfaces du Domain Layer
Contenu :
- Repository Implementations : Implémentent les interfaces du domain
- Mappers : Convertissent Domain Entities ↔ Prisma Models
- External Services : Intégrations externes (APIs, files, etc.)
Règles :
- ✅ Implémenter les interfaces du domain
- ✅ Utiliser Prisma ici (et UNIQUEMENT ici)
- ✅ Créer des mappers pour Domain ↔ Prisma
- ✅ Gérer les erreurs de persistence
- ❌ JAMAIS de logique métier
- ❌ JAMAIS exposer Prisma en dehors de cette couche
Template Repository Implementation :
// infrastructure/persistence/repositories/subscription.repository.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { ISubscriptionRepository } from '../../../domain/repositories/subscription.repository.interface';
import { Subscription } from '../../../domain/entities/subscription.entity';
import { SubscriptionMapper } from '../mappers/subscription.mapper';
@Injectable()
export class SubscriptionRepository implements ISubscriptionRepository {
constructor(private readonly prisma: PrismaService) {}
async create(subscription: Subscription): Promise<Subscription> {
const prismaData = SubscriptionMapper.toPrisma(subscription);
const created = await this.prisma.subscription.create({
data: prismaData,
});
return SubscriptionMapper.toDomain(created);
}
async findById(id: string): Promise<Subscription | null> {
const subscription = await this.prisma.subscription.findUnique({
where: { id },
});
return subscription ? SubscriptionMapper.toDomain(subscription) : null;
}
async findByClubId(clubId: string): Promise<Subscription | null> {
const subscription = await this.prisma.subscription.findFirst({
where: { clubId },
});
return subscription ? SubscriptionMapper.toDomain(subscription) : null;
}
async update(subscription: Subscription): Promise<Subscription> {
const prismaData = SubscriptionMapper.toPrisma(subscription);
const updated = await this.prisma.subscription.update({
where: { id: subscription.getId() },
data: prismaData,
});
return SubscriptionMapper.toDomain(updated);
}
async delete(id: string): Promise<void> {
await this.prisma.subscription.delete({
where: { id },
});
}
}
Voir la Skill prisma-mapper pour plus de détails sur les Mappers
4. Presentation Layer (HTTP/API)
Responsabilité : Gérer les requêtes/réponses HTTP
Contenu :
- Controllers : Endpoints HTTP avec NestJS
- DTOs : Validation des inputs HTTP (class-validator)
- Guards : Authentification et autorisation
Règles :
- ✅ Controllers TRÈS fins (HTTP uniquement)
- ✅ Déléguer immédiatement aux Handlers (Application Layer)
- ✅ Valider les inputs avec class-validator
- ✅ Transformer les outputs en JSON
- ✅ Gérer les erreurs HTTP
- ❌ JAMAIS de logique métier
- ❌ JAMAIS d'accès direct aux repositories
- ❌ JAMAIS d'accès direct à
DatabaseServiceou Prisma - ❌ JAMAIS d'accès direct à la base de données
- ✅ TOUJOURS passer par une Query/Command → Handler → Repository
Template Controller :
// presentation/controllers/subscriptions.controller.ts
import { Controller, Post, Body, Get, Param, Put, Delete, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { CreateSubscriptionCommand } from '../../application/commands/create-subscription/create-subscription.command';
import { CreateSubscriptionHandler } from '../../application/commands/create-subscription/create-subscription.handler';
import { GetSubscriptionQuery } from '../../application/queries/get-subscription/get-subscription.query';
import { GetSubscriptionHandler } from '../../application/queries/get-subscription/get-subscription.handler';
@Controller('subscriptions')
@UseGuards(JwtAuthGuard)
export class SubscriptionsController {
constructor(
private readonly createHandler: CreateSubscriptionHandler,
private readonly getHandler: GetSubscriptionHandler,
) {}
@Post()
async create(@Body() command: CreateSubscriptionCommand) {
const id = await this.createHandler.execute(command);
return { id };
}
@Get(':id')
async findOne(@Param('id') id: string) {
const query = new GetSubscriptionQuery(id);
return this.getHandler.execute(query);
}
}
🔧 Module Configuration (NestJS)
Template Module :
// [bounded-context].module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
// Presentation
import { SubscriptionsController } from './presentation/controllers/subscriptions.controller';
// Application - Commands
import { CreateSubscriptionHandler } from './application/commands/create-subscription/create-subscription.handler';
// Application - Queries
import { GetSubscriptionHandler } from './application/queries/get-subscription/get-subscription.handler';
// Infrastructure
import { SubscriptionRepository } from './infrastructure/persistence/repositories/subscription.repository';
import { SUBSCRIPTION_REPOSITORY } from './domain/repositories/subscription.repository.interface';
@Module({
imports: [PrismaModule],
controllers: [SubscriptionsController],
providers: [
// Repository binding
{
provide: SUBSCRIPTION_REPOSITORY,
useClass: SubscriptionRepository,
},
// Handlers
CreateSubscriptionHandler,
GetSubscriptionHandler,
],
exports: [
SUBSCRIPTION_REPOSITORY,
],
})
export class ClubManagementModule {}
✅ Checklist de Validation
Avant de finaliser un bounded context, vérifier :
Domain Layer
- Entities contiennent la logique métier
- Value Objects sont immuables
- Pas d'imports NestJS ou Prisma
- Repository interfaces uniquement (pas d'implémentations)
- Validation des invariants dans les constructeurs
- Factory methods pour la création d'entités
Application Layer
- Commands pour les écritures, Queries pour les lectures
- Handlers bien séparés (un handler par command/query)
- Pas de logique métier (délégation au domain)
- Validation avec class-validator
- Read Models séparés des entités domain
Infrastructure Layer
- Repository implementations utilisent Prisma
- Mappers pour Domain ↔ Prisma
- Aucune logique métier
- Prisma confiné à cette couche
Presentation Layer
- Controllers très fins (HTTP uniquement)
- Délégation immédiate aux handlers
- DTOs pour validation des inputs
- Gestion des erreurs HTTP
Module
- Repositories injectés via DI (useClass)
- Handlers enregistrés comme providers
- Exports pour réutilisation dans d'autres modules
🎓 Exemples Concrets du Projet
Bounded Context Existant : club-management
Structure complète :
- Domain : Club, Subscription, Invitation entities
- Application : create-club, subscribe-to-plan, get-club, etc.
- Infrastructure : ClubRepository, SubscriptionRepository, Mappers
- Presentation : ClubsController, SubscriptionsController
Référence : volley-app-backend/src/club-management/
Bounded Context Existant : training-management
Structure complète avec CQRS avancé
Référence : volley-app-backend/src/training-management/
🚨 Erreurs Courantes à Éviter
❌ Entités anémiques : Ne pas mettre la logique métier dans les entités
- ✅ FAIRE :
subscription.canCreateTeam() - ❌ NE PAS FAIRE :
if (subscription.currentTeams < subscription.maxTeams)
- ✅ FAIRE :
❌ Domain qui dépend de Prisma : Jamais d'import Prisma dans le domain
- ✅ FAIRE : Repository interface dans domain
- ❌ NE PAS FAIRE :
import { PrismaClient } from '@prisma/client'dans domain
❌ Logique métier dans les Controllers : Controllers doivent être fins
- ✅ FAIRE :
await this.createHandler.execute(command) - ❌ NE PAS FAIRE : Validation métier dans le controller
- ✅ FAIRE :
❌ Accès direct à DatabaseService dans les Controllers : VIOLATION GRAVE!
- ❌ NE PAS FAIRE :
constructor(private readonly database: DatabaseService) {} async method() { const user = await this.database.user.findUnique(...); // ❌ INTERDIT! } - ✅ FAIRE : Créer une Query + QueryHandler
constructor(private readonly queryBus: QueryBus) {} async method() { const query = new GetUserQuery(userId); const user = await this.queryBus.execute(query); // ✅ Correct } - Pourquoi ? : Le controller ne doit JAMAIS connaître la DB. Toute lecture/écriture passe par CQRS (Query/Command → Handler → Repository)
- ❌ NE PAS FAIRE :
❌ Handlers qui contiennent de la logique métier : Les handlers orchestrent
- ✅ FAIRE :
subscription.upgrade(newPlan)(logique dans l'entité) - ❌ NE PAS FAIRE : Logique d'upgrade dans le handler
- ✅ FAIRE :
📚 Skills Complémentaires
Pour aller plus loin :
- cqrs-command-query : Détails sur les Commands/Queries/Handlers
- ddd-testing : Standards de tests pour DDD
- prisma-mapper : Patterns de mappers Domain ↔ Prisma
Rappel : L'objectif de DDD est de créer un code maintenable où la logique métier est centralisée dans le Domain Layer, isolée de toute infrastructure technique.