docs: add ADR-007 for service classification patterns
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>
This commit is contained in:
parent
a23a5593f7
commit
b885e9c34b
236
docs/decisions/007-service-classification.md
Normal file
236
docs/decisions/007-service-classification.md
Normal file
@ -0,0 +1,236 @@
|
||||
# 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: 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:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [ADR-006: Thin Controllers](./006-thin-controllers.md)
|
||||
- [BFF Integration Patterns](../development/bff/integration-patterns.md)
|
||||
Loading…
x
Reference in New Issue
Block a user