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
355 lines
13 KiB
Markdown
355 lines
13 KiB
Markdown
# 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<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:
|
|
|
|
```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)
|