Skip to content
StableAudienceDevSécuritéOwner@platform-teamDernière revue2026-05-22

Orchestrator — architecture interne

Détail de la structure du service Orchestrator, ses responsabilités, ses interfaces externes et sa configuration réseau. Pour la vue d'ensemble du rôle, voir Orchestrator — overview.

Orchestrator Service

Responsabilités

  1. Orchestrer les workflows multi-services
  2. Gérer les transactions distribuées (Saga pattern)
  3. Implémenter les compensations en cas d'échec
  4. Centraliser la logique des processus métier

Structure du Service

services/orchestrator/src/
├── core/
│   └── constants/
│       └── injection-tokens.ts

├── domain/
│   └── interfaces/services/
│       ├── customer.service.interface.ts      # ICustomerService
│       ├── ledger.service.interface.ts        # ILedgerService
│       └── notification.service.interface.ts  # INotificationService

├── application/
│   └── use-cases/
│       ├── organizations/
│       │   ├── create-organization.use-case.ts
│       │   ├── add-organization-member.use-case.ts
│       │   └── onboard-organization.use-case.ts
│       ├── individuals/
│       │   ├── create-individual-account.use-case.ts
│       │   └── complete-kyc.use-case.ts
│       └── transactions/
│           ├── process-transfer.use-case.ts
│           ├── process-deposit.use-case.ts
│           └── process-withdrawal.use-case.ts

├── infrastructure/
│   └── adapters/http/
│       ├── base-http.adapter.ts
│       ├── customer-http.adapter.ts
│       ├── ledger-http.adapter.ts
│       └── notification-http.adapter.ts

└── presentation/
    └── controllers/
        ├── organizations.controller.ts
        ├── individuals.controller.ts
        └── transactions.controller.ts

Exemple Concret : Création d'une Organization

Workflow

Implémentation

typescript
// application/use-cases/organizations/create-organization.use-case.ts

@Injectable()
export class CreateOrganizationUseCase {
  constructor(
    @Inject(CUSTOMER_SERVICE)
    private readonly customerService: ICustomerService,

    @Inject(LEDGER_SERVICE)
    private readonly ledgerService: ILedgerService,

    @Inject(NOTIFICATION_SERVICE)
    private readonly notificationService: INotificationService,
  ) {}

  async execute(input: CreateOrganizationInput): Promise<CreateOrganizationResult> {
    let corporate: Corporate | null = null;
    let adminPerson: Person | null = null;
    let account: Account | null = null;

    try {
      // ═══════════════════════════════════════════════════════════
      // STEP 1: Créer l'entité Corporate
      // ═══════════════════════════════════════════════════════════
      corporate = await this.customerService.createCorporate({
        name: input.companyName,
        registrationNumber: input.registrationNumber,
        country: input.country,
        industry: input.industry,
      });

      // ═══════════════════════════════════════════════════════════
      // STEP 2: Créer l'Admin Person et le lier au Corporate
      // ═══════════════════════════════════════════════════════════
      adminPerson = await this.customerService.createPerson({
        firstName: input.admin.firstName,
        lastName: input.admin.lastName,
        email: input.admin.email,
        phone: input.admin.phone,
        corporateId: corporate.id,
        role: CorporateRole.ADMIN,
      });

      // ═══════════════════════════════════════════════════════════
      // STEP 3: Créer le compte Nex pour le Corporate
      // ═══════════════════════════════════════════════════════════
      account = await this.ledgerService.createAccount({
        entityId: corporate.id,
        entityType: EntityType.CORPORATE,
        currency: input.currency || 'XAF',
        name: `Compte Principal - ${input.companyName}`,
      });

      // ═══════════════════════════════════════════════════════════
      // STEP 4: Créer le Wallet principal
      // ═══════════════════════════════════════════════════════════
      const wallet = await this.ledgerService.createWallet({
        accountId: account.id,
        type: WalletType.CORPORATE_MAIN,
        name: 'Wallet Principal',
      });

      // ═══════════════════════════════════════════════════════════
      // STEP 5: Envoyer les notifications
      // ═══════════════════════════════════════════════════════════
      await this.notificationService.send({
        type: NotificationType.EMAIL,
        template: 'CORPORATE_WELCOME',
        recipient: input.admin.email,
        data: {
          companyName: input.companyName,
          adminName: `${input.admin.firstName} ${input.admin.lastName}`,
          accountNumber: account.accountNumber,
        },
      });

      // ═══════════════════════════════════════════════════════════
      // SUCCESS: Retourner le résultat
      // ═══════════════════════════════════════════════════════════
      return {
        success: true,
        data: {
          corporateId: corporate.id,
          adminPersonId: adminPerson.id,
          accountId: account.id,
          walletId: wallet.id,
          accountNumber: account.accountNumber,
        },
      };

    } catch (error) {
      // ═══════════════════════════════════════════════════════════
      // COMPENSATION: Rollback en cas d'échec
      // ═══════════════════════════════════════════════════════════
      await this.compensate({ corporate, adminPerson, account }, error);
      throw error;
    }
  }

  /**
   * Compensation (rollback) en cas d'échec
   * Supprime les entités créées dans l'ordre inverse
   */
  private async compensate(
    created: { corporate: Corporate | null; adminPerson: Person | null; account: Account | null },
    originalError: Error,
  ): Promise<void> {
    const compensationErrors: Error[] = [];

    // Rollback Account (si créé)
    if (created.account) {
      try {
        await this.ledgerService.deleteAccount(created.account.id);
      } catch (e) {
        compensationErrors.push(e);
      }
    }

    // Rollback Person (si créé)
    if (created.adminPerson) {
      try {
        await this.customerService.deletePerson(created.adminPerson.id);
      } catch (e) {
        compensationErrors.push(e);
      }
    }

    // Rollback Corporate (si créé)
    if (created.corporate) {
      try {
        await this.customerService.deleteCorporate(created.corporate.id);
      } catch (e) {
        compensationErrors.push(e);
      }
    }

    // Log les erreurs de compensation pour investigation
    if (compensationErrors.length > 0) {
      console.error('Compensation errors:', compensationErrors);
      // TODO: Alerter l'équipe ops pour intervention manuelle
    }
  }
}

Controller

typescript
// presentation/controllers/organizations.controller.ts

@Controller('organizations')
@ApiTags('Organizations')
export class OrganizationsController {
  constructor(
    private readonly createOrganizationUseCase: CreateOrganizationUseCase,
    private readonly addMemberUseCase: AddOrganizationMemberUseCase,
  ) {}

  @Post()
  @ApiOperation({ summary: 'Créer une nouvelle organization' })
  @ApiResponse({ status: 201, type: CreateOrganizationResponseDto })
  async create(
    @Body() dto: CreateOrganizationDto,
    @CurrentUser() user: AuthenticatedUser,
  ): Promise<ApiResponse<CreateOrganizationResponseDto>> {
    const result = await this.createOrganizationUseCase.execute({
      ...dto,
      createdBy: user.userId,
    });

    return {
      success: true,
      code: 'ORGANIZATION_CREATED',
      data: result.data,
    };
  }

  @Post(':corporateId/members')
  @ApiOperation({ summary: 'Ajouter un membre à l\'organization' })
  async addMember(
    @Param('corporateId', ParseUUIDPipe) corporateId: string,
    @Body() dto: AddMemberDto,
    @CurrentUser() user: AuthenticatedUser,
  ): Promise<ApiResponse<AddMemberResponseDto>> {
    const result = await this.addMemberUseCase.execute({
      corporateId,
      ...dto,
      addedBy: user.userId,
    });

    return {
      success: true,
      code: 'MEMBER_ADDED',
      data: result.data,
    };
  }
}

Interfaces des Services Externes

typescript
// domain/interfaces/services/customer.service.interface.ts

export interface ICustomerService {
  // Corporate
  createCorporate(input: CreateCorporateInput): Promise<Corporate>;
  getCorporate(id: string): Promise<Corporate | null>;
  updateCorporate(id: string, input: UpdateCorporateInput): Promise<Corporate>;
  deleteCorporate(id: string): Promise<void>;

  // Person
  createPerson(input: CreatePersonInput): Promise<Person>;
  getPerson(id: string): Promise<Person | null>;
  deletePerson(id: string): Promise<void>;

  // KYC
  submitKycDocuments(personId: string, documents: KycDocument[]): Promise<KycSubmission>;
  getKycStatus(personId: string): Promise<KycStatus>;
}

// domain/interfaces/services/ledger.service.interface.ts

export interface ILedgerService {
  // Account
  createAccount(input: CreateAccountInput): Promise<Account>;
  getAccount(id: string): Promise<Account | null>;
  getAccountsByEntity(entityId: string, entityType: EntityType): Promise<Account[]>;
  deleteAccount(id: string): Promise<void>;

  // Wallet
  createWallet(input: CreateWalletInput): Promise<Wallet>;
  getWallet(id: string): Promise<Wallet | null>;

  // Transactions
  transfer(input: TransferInput): Promise<Transaction>;
  getTransaction(id: string): Promise<Transaction | null>;
}

// domain/interfaces/services/notification.service.interface.ts

export interface INotificationService {
  send(input: SendNotificationInput): Promise<NotificationResult>;
  sendBulk(inputs: SendNotificationInput[]): Promise<NotificationResult[]>;
}

Configuration Réseau

Docker Compose (Services internes)

yaml
# Les services métier ne sont PAS exposés publiquement
services:
  customer-profiles-kyc:
    # Pas de "ports:" = non accessible de l'extérieur
    networks:
      - nxpay-internal

  ledger-wallets:
    networks:
      - nxpay-internal

  notifications:
    networks:
      - nxpay-internal

  # Seuls ces services sont exposés
  api-gateway:
    ports:
      - "3000:3000"
    networks:
      - nxpay-internal
      - nxpay-public

  orchestrator:
    # Accessible uniquement via API Gateway
    networks:
      - nxpay-internal

networks:
  nxpay-internal:
    internal: true  # Pas d'accès internet
  nxpay-public:

Variables d'Environnement (Orchestrator)

env
# Services internes (noms Docker)
CUSTOMER_SERVICE_URL=http://customer-profiles-kyc:3000
LEDGER_SERVICE_URL=http://ledger-wallets:3000
NOTIFICATION_SERVICE_URL=http://notifications:3000
CONFIGURATION_SERVICE_URL=http://configuration:3000

Nex — Plateforme fintech CEMAC