Document the architectural decision for classifying BFF services: - Facades: unified entry points for integration subsystems - Orchestrators: coordinate workflows across multiple systems - Aggregators: read-only data composition from multiple sources - Services: single-responsibility operations Includes dependency rules, naming conventions, and implementation examples. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
8.6 KiB
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
// ✅ 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
// ✅ 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
// ✅ GOOD: Aggregator for read-only data composition
@Injectable()
export class MeStatusAggregator {
constructor(
private readonly users: UsersFacade,
private readonly orders: OrderOrchestrator,
private readonly payments: WhmcsPaymentService
) {}
async getStatusForUser(userId: string): Promise<MeStatus> {
// 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:
// 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:
// 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
// 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
// ❌ 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