# BFF Integration Layer Patterns ## Overview The BFF (Backend for Frontend) integration layer encapsulates all external system interactions and infrastructure concerns. This document defines the patterns and best practices for creating integration services. --- ## Architecture Principles ### Domain Layer vs Integration Layer **Domain Layer** (`packages/domain/`): - Business types and validation schemas - Raw provider types (data structures from external systems) - Transformation mappers (raw → domain) - Business validation logic - **No infrastructure concerns** **Integration Layer** (`apps/bff/src/integrations/`): - Query builders (SOQL, GraphQL, etc.) - Connection services - Integration services - HTTP/API clients - **No business logic** --- ## Integration Service Pattern ### Purpose Integration services encapsulate all interactions with a specific external system feature, providing a clean abstraction for the application layer. ### Structure ```typescript @Injectable() export class SalesforceOrderService { constructor( private readonly sf: SalesforceConnection, @Inject(Logger) private readonly logger: Logger ) {} async getOrderById(orderId: string): Promise { // 1. Build query (infrastructure concern) const soql = this.buildOrderQuery(orderId); // 2. Execute query const rawData = await this.sf.query(soql); // 3. Use domain mapper (single transformation!) return DomainProviders.Salesforce.transformOrder(rawData); } } ``` ### Key Characteristics 1. **Encapsulation**: All system-specific logic hidden from application layer 2. **Single Transformation**: Uses domain mappers, no additional mapping 3. **Returns Domain Types**: Application layer works with domain types only 4. **Infrastructure Details**: Query building, field selection, etc. stay here --- ## Query Builder Pattern ### Location Query builders belong in **integration layer utils**, not in domain. **Correct**: `apps/bff/src/integrations/salesforce/utils/order-query-builder.ts` **Wrong**: `packages/domain/orders/providers/salesforce/query.ts` ❌ ### Example ```typescript /** * Build field list for Order queries * Infrastructure concern - not business logic */ export function buildOrderSelectFields( additional: string[] = [] ): string[] { const fields = [ "Id", "AccountId", "Status", // ... all Salesforce field names ]; return UNIQUE([...fields, ...additional]); } ``` --- ## Data Flow ### Clean Architecture Flow ``` ┌─────────────────────────────────────────┐ │ Controller (HTTP) │ │ - API endpoints │ │ - Request/Response formatting │ └──────────────┬──────────────────────────┘ │ ┌──────────────▼──────────────────────────┐ │ Orchestrator (Application) │ │ - Workflow coordination │ │ - Uses integration services │ │ - Works with domain types │ └──────────────┬──────────────────────────┘ │ ┌──────────┴──────────┐ │ │ ┌───▼───────────┐ ┌──────▼──────────────┐ │ Domain │ │ Integration │ │ (Business) │ │ (Infrastructure) │ │ │ │ │ │ • Types │ │ • SF OrderService │ │ • Schemas │ │ • Query Builders │ │ • Mappers ────┼──┤ • Connections │ │ • Validators │ │ • API Clients │ └───────────────┘ └─────────────────────┘ ``` ### Single Transformation Principle **One transformation path**: ``` Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directly └────────── Single transformation ──────────┘ ``` **No double transformation**: ``` ❌ Query → Raw Data → Domain Mapper → Domain Type → BFF Mapper → ??? ``` --- ## Integration Service Examples ### Example 1: Salesforce Order Service ```typescript import { SalesforceConnection } from "./salesforce-connection.service"; import { buildOrderSelectFields } from "../utils/order-query-builder"; import { Providers } from "@customer-portal/domain/orders"; @Injectable() export class SalesforceOrderService { constructor( private readonly sf: SalesforceConnection, @Inject(Logger) private readonly logger: Logger ) {} async getOrderById(orderId: string): Promise { // Build query using integration layer utils const fields = buildOrderSelectFields(["Account.Name"]).join(", "); const soql = `SELECT ${fields} FROM Order WHERE Id = '${orderId}'`; // Execute query const result = await this.sf.query(soql); if (!result.records?.[0]) return null; // Use domain mapper - single transformation! return Providers.Salesforce.transformSalesforceOrderDetails( result.records[0], [] ); } } ``` ### Example 2: WHMCS Client Service ```typescript import { WhmcsConnection } from "./whmcs-connection.service"; import { Providers } from "@customer-portal/domain/customer"; @Injectable() export class WhmcsClientService { constructor( private readonly whmcs: WhmcsConnection, @Inject(Logger) private readonly logger: Logger ) {} async getClientById(clientId: number): Promise { // Build request parameters const params = { clientid: clientId }; // Execute API call const rawClient = await this.whmcs.call("GetClientsDetails", params); if (!rawClient) return null; // Use domain mapper - single transformation! return Providers.Whmcs.transformWhmcsClient(rawClient); } } ``` --- ## When to Create an Integration Service ### Create When ✅ You need to query/update data from an external system ✅ The logic involves system-specific query construction ✅ Multiple orchestrators need the same external data ✅ You want to encapsulate provider-specific complexity ### Don't Create When ❌ Simple pass-through without any system-specific logic ❌ One-time queries that won't be reused ❌ Logic that belongs in domain (business rules) --- ## Orchestrator Usage Pattern ### Before (Direct Queries - Wrong) ```typescript @Injectable() export class OrderOrchestrator { constructor(private readonly sf: SalesforceConnection) {} async getOrder(orderId: string) { // ❌ Building queries in orchestrator const soql = `SELECT Id, Status FROM Order WHERE Id = '${orderId}'`; const result = await this.sf.query(soql); // ❌ Orchestrator knows about Salesforce structure return DomainMapper.transform(result.records[0]); } } ``` ### After (Integration Service - Correct) ```typescript @Injectable() export class OrderOrchestrator { constructor( private readonly salesforceOrderService: SalesforceOrderService ) {} async getOrder(orderId: string) { // ✅ Clean delegation to integration service return this.salesforceOrderService.getOrderById(orderId); // ✅ Receives domain type, uses directly } } ``` --- ## Mapper Usage Pattern ### Direct Domain Mapper Usage (Correct) ```typescript import { Providers } from "@customer-portal/domain/orders"; // ✅ Use domain mapper directly - no wrapper service const whmcsItems = Providers.Whmcs.mapFulfillmentOrderItems(items); ``` ### Redundant Service Wrapper (Wrong) ```typescript // ❌ Don't create service wrappers that just delegate @Injectable() export class OrderWhmcsMapper { mapOrderItemsToWhmcs(items: OrderItem[]) { // Just wraps domain mapper - no value added! return Providers.Whmcs.mapFulfillmentOrderItems(items); } } ``` --- ## Module Configuration ### Integration Module ```typescript @Module({ imports: [ConfigModule], providers: [ SalesforceConnection, SalesforceOrderService, SalesforceAccountService, ], exports: [ SalesforceConnection, SalesforceOrderService, // Export for use in application layer ], }) export class SalesforceModule {} ``` ### Application Module ```typescript @Module({ imports: [ SalesforceModule, // Import integration modules WhmcsModule, ], providers: [ OrderOrchestrator, // Uses integration services ], }) export class OrdersModule {} ``` --- ## Testing Integration Services ### Unit Test Pattern ```typescript describe("SalesforceOrderService", () => { let service: SalesforceOrderService; let mockConnection: jest.Mocked; beforeEach(() => { mockConnection = { query: jest.fn(), } as any; service = new SalesforceOrderService(mockConnection, mockLogger); }); it("should transform raw SF data using domain mapper", async () => { const mockRawOrder = { Id: "123", Status: "Active" }; mockConnection.query.mockResolvedValue({ records: [mockRawOrder], }); const result = await service.getOrderById("123"); // Verify domain mapper was used (single transformation) expect(result).toBeDefined(); expect(result.id).toBe("123"); }); }); ``` --- ## Benefits of This Pattern ### Architecture Cleanliness - ✅ Clear separation of concerns - ✅ Domain stays pure (no infrastructure) - ✅ Integration complexity encapsulated ### Code Quality - ✅ Easier to test (mock integration services) - ✅ Easier to swap providers (change integration layer only) - ✅ No duplication (single transformation path) ### Developer Experience - ✅ Clear patterns to follow - ✅ No confusion about where code belongs - ✅ Self-documenting architecture --- ## Migration Checklist When adding new external system integration: - [ ] Create integration service in `apps/bff/src/integrations/{provider}/services/` - [ ] Move query builders to `apps/bff/src/integrations/{provider}/utils/` - [ ] Define raw types in domain `packages/domain/{feature}/providers/{provider}/raw.types.ts` - [ ] Define mappers in domain `packages/domain/{feature}/providers/{provider}/mapper.ts` - [ ] Export integration service from provider module - [ ] Use integration service in orchestrators - [ ] Use domain mappers directly (no wrapper services) - [ ] Test integration service independently --- ## Related Documentation - [Domain Package README](../../packages/domain/README.md) - [ORDERS-ARCHITECTURE-REVIEW.md](../ORDERS-ARCHITECTURE-REVIEW.md) - [Schema-First Approach](../../packages/domain/SCHEMA-FIRST-COMPLETE.md) --- **Last Updated**: October 2025 **Status**: ✅ Active Pattern