Assist_Design/docs/decisions/007-service-classification.md
barsa be164cf287 feat: Implement Me Status Aggregator to consolidate user status data
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
2026-01-19 11:25:30 +09:00

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:

  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:

// 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 trackingWorkflowStepTrackerService (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
// ✅ 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