# ADR-007: Service Classification (Facades, Orchestrators, Aggregators) **Date**: 2025-01-15 **Status**: Accepted ## Context The BFF layer communicates with multiple external systems (WHMCS, Salesforce, Freebit, JapanPost) and coordinates complex workflows. Without clear naming conventions, services become: - Difficult to understand at a glance - Hard to know what they're responsible for - Inconsistent in their patterns Many "services" were actually orchestrating multiple systems but named generically (e.g., `SalesforceService`, `FreebitOperationsService`). ## Decision **Classify services by their architectural role** using clear naming conventions: | Type | Suffix | Purpose | Dependencies | | ---------------- | --------------- | ------------------------------------------------ | ----------------------------------------- | | **Facade** | `*Facade` | Unified entry point for an integration subsystem | Multiple services within same integration | | **Orchestrator** | `*Orchestrator` | Coordinate workflows across multiple systems | Multiple services/integrations | | **Aggregator** | `*Aggregator` | Combine read-only data from multiple sources | Multiple services (read-only) | | **Service** | `*Service` | Single-responsibility operations | 1-2 integrations max | ## Rationale ### Why Facades? Integration modules (WHMCS, Salesforce, Freebit) contain multiple internal services. A **Facade** provides: - **Single entry point**: Consumers don't need to know internal structure - **Consistent interface**: Unified error handling, logging, queueing - **Encapsulation**: Internal service changes don't affect consumers ```typescript // ✅ GOOD: Facade abstracts internal complexity @Injectable() export class WhmcsConnectionFacade { // Single entry point for ALL WHMCS API operations // Handles queueing, error handling, request prioritization } // Consumers use the facade constructor(private readonly whmcs: WhmcsConnectionFacade) {} ``` ### Why Orchestrators? Complex workflows span multiple systems. An **Orchestrator**: - Coordinates multi-step operations - Manages transaction boundaries - Handles cross-system error recovery ```typescript // ✅ GOOD: Orchestrator for cross-system workflow @Injectable() export class OrderFulfillmentOrchestrator { constructor( private readonly salesforce: SalesforceFacade, private readonly whmcs: WhmcsOrderService, private readonly freebit: FreebitFacade ) {} async executeFulfillment(orderId: string) { // Coordinates SF → WHMCS → Freebit workflow } } ``` ### Why Aggregators? Dashboard and profile endpoints combine data from multiple sources. An **Aggregator**: - Is **read-only** (no mutations) - Combines data from multiple services - Handles partial failures gracefully ```typescript // ✅ GOOD: Aggregator for read-only data composition @Injectable() export class MeStatusAggregator { constructor( private readonly users: UsersService, private readonly orders: OrderOrchestrator, private readonly payments: WhmcsPaymentService ) {} async getStatusForUser(userId: string): Promise { // Combines user, order, payment data for dashboard } } ``` ### Alternatives Considered | Approach | Pros | Cons | | ----------------------------------------------- | ---------------------- | -------------------------------------- | | **Generic `*Service`** | Familiar | No architectural clarity | | **Layer-based (Repository/Service/Controller)** | Simple | Doesn't capture orchestration patterns | | **Role-based naming** | Clear responsibilities | More files, learning curve | ## Consequences ### Positive - Clear responsibilities from class name - Easier onboarding (developers know what each type does) - Consistent patterns across modules - Better testability (mock entire facades) ### Negative - Refactoring existing services required - Developers must learn classification rules - More specific naming conventions to follow ## Implementation ### Integration Facades Located in `integrations/{provider}/facades/`: ``` integrations/ ├── whmcs/ │ ├── facades/ │ │ └── whmcs.facade.ts # WhmcsConnectionFacade │ └── services/ # Internal services ├── salesforce/ │ ├── facades/ │ │ └── salesforce.facade.ts # SalesforceFacade │ └── services/ └── freebit/ ├── facades/ │ └── freebit.facade.ts # FreebitFacade └── services/ ``` ### Module Orchestrators Located in module services or dedicated orchestrators folder: ```typescript // modules/orders/services/order-fulfillment-orchestrator.service.ts @Injectable() export class OrderFulfillmentOrchestrator {} // modules/subscriptions/sim-management/services/sim-orchestrator.service.ts @Injectable() export class SimOrchestrator {} ``` ### Aggregators Currently in their modules, planned for dedicated `aggregators/` folder: ```typescript // modules/me-status/me-status.service.ts @Injectable() export class MeStatusAggregator {} // modules/users/infra/user-profile.service.ts @Injectable() export class UserProfileAggregator {} ``` ### Dependency Rules ``` ┌─────────────────────────────────────────────────────────────────┐ │ ALLOWED IMPORTS │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Controllers │ │ ↓ (can import) │ │ Orchestrators / Aggregators / Facades │ │ ↓ (can import) │ │ Services │ │ ↓ (can import) │ │ Integration Facades │ │ ↓ (can import) │ │ Integration Entity Services │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ FORBIDDEN │ │ │ │ ✗ Controllers → Integration Services (use facades) │ │ ✗ Services → Orchestrators (wrong direction) │ │ ✗ Aggregators → Mutation methods │ │ ✗ Integration Services → Module Services │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Naming Conventions ### DO ```typescript // Integration entry points export class WhmcsConnectionFacade {} export class SalesforceFacade {} export class FreebitFacade {} // Cross-system workflows export class OrderFulfillmentOrchestrator {} export class SimOrchestrator {} // Read-only data composition export class MeStatusAggregator {} export class UserProfileAggregator {} // Single-responsibility operations export class WhmcsInvoiceService {} export class SalesforceOrderService {} ``` ### DON'T ```typescript // ❌ Generic names that hide complexity export class SalesforceService {} // What does it do? export class FreebitOperationsService {} // Operations = everything? export class MeStatusService {} // Service doing aggregation // ❌ Orchestrators named as services export class SimOrchestratorService {} // Drop the Service suffix ``` ## Facade vs Orchestrator: Key Differences A common source of confusion is when to use a **Facade** vs an **Orchestrator**. Here's the distinction: | Aspect | Facade | Orchestrator | | -------------------- | -------------------------------------- | ------------------------------ | | **Scope** | Single integration (WHMCS, Salesforce) | Multiple integrations/systems | | **Purpose** | Abstract internal complexity | Coordinate workflows | | **Dependency** | Owns internal services | Consumes facades and services | | **Mutation Pattern** | Direct API calls | Transaction coordination | | **Error Handling** | System-specific errors | Cross-system rollback/recovery | | **Example** | `WhmcsConnectionFacade` | `OrderFulfillmentOrchestrator` | ### When to Use Each **Use a Facade when:** - You're building an entry point for a single external system - You want to encapsulate multiple services within one integration - Consumers shouldn't know about internal service structure **Use an Orchestrator when:** - You're coordinating operations across multiple systems - You need distributed transaction patterns - Failure in one system requires compensation in another ## When to Split an Orchestrator **Guideline: Consider splitting when an orchestrator exceeds ~300 lines.** Signs an orchestrator needs refactoring: 1. **Too many responsibilities**: Validation + transformation + execution + side effects 2. **Difficult to test**: Mocking 10+ dependencies 3. **Long methods**: Single methods exceeding 100 lines 4. **Mixed concerns**: Business logic mixed with infrastructure ### Extraction Patterns Extract concerns into specialized services: ```typescript // BEFORE: Monolithic orchestrator @Injectable() export class OrderFulfillmentOrchestrator { // 700 lines doing everything } // AFTER: Focused orchestrator with extracted services @Injectable() export class OrderFulfillmentOrchestrator { constructor( private readonly stepTracker: WorkflowStepTrackerService, private readonly sideEffects: FulfillmentSideEffectsService, private readonly validator: OrderFulfillmentValidator ) {} // ~200 lines of pure orchestration } ``` **Common extractions:** - **Step tracking** → `WorkflowStepTrackerService` (infrastructure) - **Side effects** → `*SideEffectsService` (events, notifications, cache) - **Validation** → `*Validator` (composable validators) - **Error handling** → `*ErrorService` (error classification, recovery) ## File Naming Conventions ### Services | Type | File Name Pattern | Class Name Pattern | | ------------ | ---------------------------------- | ---------------------- | | Facade | `{domain}.facade.ts` | `{Domain}Facade` | | Orchestrator | `{domain}-orchestrator.service.ts` | `{Domain}Orchestrator` | | Aggregator | `{domain}.aggregator.ts` | `{Domain}Aggregator` | | Service | `{domain}.service.ts` | `{Domain}Service` | | Validator | `{domain}.validator.ts` | `{Domain}Validator` | ### Examples ``` # Integration facades integrations/whmcs/facades/whmcs.facade.ts → WhmcsConnectionFacade integrations/salesforce/facades/salesforce.facade.ts → SalesforceFacade # Module orchestrators modules/orders/services/order-fulfillment-orchestrator.service.ts → OrderFulfillmentOrchestrator modules/auth/application/auth-orchestrator.service.ts → AuthOrchestrator # Aggregators modules/me-status/me-status.aggregator.ts → MeStatusAggregator modules/users/infra/user-profile.service.ts → UserProfileAggregator # Validators modules/orders/validators/internet-order.validator.ts → InternetOrderValidator modules/orders/validators/sim-order.validator.ts → SimOrderValidator ``` ### File vs Class Naming - **File names**: Use kebab-case with `.service.ts`, `.facade.ts`, `.aggregator.ts` suffix - **Class names**: Use PascalCase without `Service` suffix for Orchestrators/Aggregators ```typescript // ✅ GOOD // File: order-fulfillment-orchestrator.service.ts export class OrderFulfillmentOrchestrator {} // File: me-status.aggregator.ts export class MeStatusAggregator {} // ❌ AVOID // File: order-fulfillment-orchestrator.ts (missing .service) export class OrderFulfillmentOrchestratorService {} // Don't add Service suffix ``` ## Related - [ADR-006: Thin Controllers](./006-thin-controllers.md) - [BFF Integration Patterns](../development/bff/integration-patterns.md)