feat: Add Fulfillment Side Effects Service for order processing notifications and cache management feat: Create base validation interfaces and implement various order validators feat: Develop Internet Order Validator to check eligibility and prevent duplicate services feat: Implement SIM Order Validator to ensure residence card verification and activation fee presence feat: Create SKU Validator to validate product SKUs against the Salesforce pricebook feat: Implement User Mapping Validator to ensure necessary account mappings exist before ordering feat: Enhance Users Service with methods for user profile management and summary retrieval
13 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: UsersService,
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
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:
- Too many responsibilities: Validation + transformation + execution + side effects
- Difficult to test: Mocking 10+ dependencies
- Long methods: Single methods exceeding 100 lines
- Mixed concerns: Business logic mixed with infrastructure
Extraction Patterns
Extract concerns into specialized services:
// 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.tssuffix - Class names: Use PascalCase without
Servicesuffix for Orchestrators/Aggregators
// ✅ 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