Assist_Design/docs/decisions/003-map-once-use-everywhere.md

148 lines
4.4 KiB
Markdown

# ADR-003: Map Once, Use Everywhere
**Date**: 2025-01-15
**Status**: Accepted
## Context
The Customer Portal integrates with multiple external systems (WHMCS, Salesforce, Freebit) that each have their own data structures. These raw API responses need to be transformed into normalized domain types.
Common anti-patterns:
- Transforming data multiple times at different layers
- Creating "transformer services" that wrap domain mappers
- Leaking raw provider types into application code
## Decision
**Transform external data exactly once** in domain mappers. Everything else uses the normalized domain types directly.
```
External API → Integration Service → Domain Mapper → Domain Type → Use Directly
SINGLE transformation
```
## Rationale
### Why Single Transformation?
1. **No drift**: One transformation = one place where the mapping can be wrong
2. **No confusion**: Developers know exactly where to look for mapping logic
3. **No wrapper services**: Domain mappers are functions, not services. No need for `InvoiceTransformerService` that just calls `transformWhmcsInvoice()`
4. **Simpler data flow**: Raw data → domain type. No intermediate representations.
### The Anti-Pattern: Multiple Transformations
```typescript
// ❌ BAD: Multiple transformation layers
const raw = await whmcsApi.getInvoice(id);
const intermediate = customNormalize(raw); // First transform
const domainType = transformWhmcsInvoice(intermediate); // Second transform
const viewModel = mapToViewModel(domainType); // Third transform
```
### The Pattern: Single Transformation
```typescript
// ✅ GOOD: Single transformation
const raw = await whmcsApi.getInvoice(id);
const invoice = transformWhmcsInvoice(raw); // One transform
// Use invoice directly everywhere
```
### Alternatives Considered
| Approach | Pros | Cons |
| ------------------------- | --------------------------- | -------------------------------- |
| **Multiple layers** | "Separation of concerns" | Complexity, drift, hard to trace |
| **Transformer services** | Injectable, testable | Unnecessary indirection |
| **Single transformation** | Simple, traceable, no drift | Mappers must handle all cases |
## Consequences
### Positive
- Clear, traceable data flow
- No wrapper services to maintain
- One place for all transformation logic
- Domain types used consistently everywhere
### Negative
- Domain mappers must handle all edge cases
- Can't easily add "view-specific" transformations (but this is usually a sign of missing domain concepts)
## Implementation
### Integration Service Pattern
```typescript
// apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts
@Injectable()
export class WhmcsInvoiceService {
async getInvoice(id: number): Promise<Invoice> {
// 1. Fetch raw data
const raw = await this.whmcsClient.getInvoice({ invoiceid: id });
// 2. Transform ONCE with domain mapper
return transformWhmcsInvoice(raw, {
defaultCurrencyCode: this.currency.code,
});
// 3. Return domain type - used directly everywhere
}
}
```
### Domain Mapper
```typescript
// packages/domain/billing/providers/whmcs/mapper.ts
export function transformWhmcsInvoice(raw: unknown, context: TransformContext): Invoice {
// Validate input
const validated = whmcsInvoiceRawSchema.parse(raw);
// Transform to domain model
const result: Invoice = {
id: validated.invoiceid,
status: mapInvoiceStatus(validated.status),
total: {
amount: parseFloat(validated.total),
currency: context.defaultCurrencyCode,
},
// ... all mappings in one place
};
// Validate output
return invoiceSchema.parse(result);
}
```
### What NOT to Do
```typescript
// ❌ DON'T create wrapper services
@Injectable()
export class InvoiceTransformerService {
transform(raw: WhmcsInvoice): Invoice {
return transformWhmcsInvoice(raw); // Pointless wrapper
}
}
// ❌ DON'T transform multiple times
const raw = await api.fetch();
const normalized = this.normalizer.normalize(raw);
const mapped = this.mapper.map(normalized);
const enriched = this.enricher.enrich(mapped);
```
## Related
- [BFF Integration Patterns](../development/bff/integration-patterns.md)
- [Domain Structure](../development/domain/structure.md)
- [ADR-004: Domain Provider Isolation](./004-domain-provider-isolation.md)