# Domain-First Structure with Providers **Last Updated**: January 2026 **Status**: ✅ Implemented --- ## Quick Reference | Aspect | Location | | -------------------- | ------------------------------------------------------------ | | **Domain package** | `packages/domain/` | | **Schemas** | `packages/domain//schema.ts` | | **Provider mappers** | `packages/domain//providers//mapper.ts` | | **Raw types** | `packages/domain//providers//raw.types.ts` | **Import patterns**: ```typescript // App/Portal code - domain types only import { Invoice, invoiceSchema } from "@customer-portal/domain/billing"; // BFF integration code - includes provider mappers import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers"; ``` **Key principle**: Transform once in domain mappers, use domain types everywhere else. --- ## 🎯 Architecture Philosophy **Core Principle**: Domain-first organization where each business domain owns its: - **contract.ts** - Normalized types (provider-agnostic) - **schema.ts** - Runtime validation (Zod) - **providers/** - Provider-specific adapters (raw types + mappers) **Why This Works**: - Domain-centric matches business thinking - Provider isolation prevents leaking implementation details - Adding new providers = adding new folders (no refactoring) - Single package (`@customer-portal/domain`) for all types **Import rules**: See [`docs/development/domain/import-hygiene.md`](import-hygiene.md). --- ## 📦 Package Structure ``` packages/domain/ ├── billing/ │ ├── contract.ts # Invoice, InvoiceItem, InvoiceStatus │ ├── schema.ts # invoiceSchema, INVOICE_STATUS const │ ├── index.ts # Public exports │ └── providers/ │ └── whmcs/ │ ├── raw.types.ts # WhmcsInvoiceRaw (API response) │ └── mapper.ts # transformWhmcsInvoice() │ ├── subscriptions/ │ ├── contract.ts # Subscription, SubscriptionStatus │ ├── schema.ts │ ├── index.ts │ └── providers/ │ └── whmcs/ │ ├── raw.types.ts │ └── mapper.ts │ ├── payments/ │ ├── contract.ts # PaymentMethod, PaymentGateway │ ├── schema.ts │ ├── index.ts │ └── providers/ │ └── whmcs/ │ ├── raw.types.ts │ └── mapper.ts │ ├── sim/ │ ├── contract.ts # SimDetails, SimUsage │ ├── schema.ts │ ├── index.ts │ └── providers/ │ └── freebit/ │ ├── raw.types.ts │ └── mapper.ts │ ├── orders/ │ ├── contract.ts # Order, OrderItem │ ├── schema.ts │ ├── index.ts │ └── providers/ │ ├── salesforce/ # Read orders │ │ ├── raw.types.ts │ │ └── mapper.ts │ └── whmcs/ # Create orders │ ├── raw.types.ts │ └── mapper.ts │ ├── services/ │ ├── contract.ts # Service catalog products (UI view model) │ ├── schema.ts │ ├── index.ts │ └── providers/ │ └── salesforce/ │ ├── raw.types.ts # SalesforceProduct2 │ └── mapper.ts │ ├── common/ │ ├── types.ts # Address, Money, BaseEntity │ ├── identifiers.ts # UserId, OrderId (branded types) │ ├── api.ts # ApiResponse, PaginatedResponse │ ├── schema.ts # Common schemas │ └── index.ts │ └── toolkit/ ├── formatting/ │ └── currency.ts # formatCurrency() ├── validation/ │ └── helpers.ts # Validation utilities ├── typing/ │ └── patterns.ts # AsyncState, etc. └── index.ts ``` --- ## 📝 Import Patterns ### **Application Code (Domain Only)** ```typescript // Import normalized domain types import { Invoice, invoiceSchema, INVOICE_STATUS } from "@customer-portal/domain/billing"; import { Subscription } from "@customer-portal/domain/subscriptions"; import { Address } from "@customer-portal/domain/customer"; // Use domain types const invoice: Invoice = { id: 123, status: INVOICE_STATUS.PAID, // ... }; // Validate const validated = invoiceSchema.parse(rawData); ``` ### **Integration Code (Needs Provider Specifics)** ```typescript // Import domain + provider import { Invoice, invoiceSchema } from "@customer-portal/domain/billing"; import { transformWhmcsInvoice, whmcsInvoiceRawSchema, type WhmcsInvoiceRaw, } from "@customer-portal/domain/billing/providers"; // Transform raw API data const whmcsData: WhmcsInvoiceRaw = await whmcsApi.getInvoice(id); const invoice: Invoice = transformWhmcsInvoice(whmcsData); ``` --- ## 🏗️ Domain File Templates ### **contract.ts** ```typescript /** * {Domain} - Contract * * Normalized types for {domain} that all providers must map to. */ // Status enum (if applicable) export const {DOMAIN}_STATUS = { ACTIVE: "Active", // ... } as const; export type {Domain}Status = (typeof {DOMAIN}_STATUS)[keyof typeof {DOMAIN}_STATUS]; // Main entity export interface {Domain} { id: number; status: {Domain}Status; // ... normalized fields } ``` ### **schema.ts** ```typescript /** * {Domain} - Schemas * * Zod validation for {domain} types. */ import { z } from "zod"; export const {domain}StatusSchema = z.enum([...]); export const {domain}Schema = z.object({ id: z.number(), status: {domain}StatusSchema, // ... field validation }); ``` ### **providers/{provider}/raw.types.ts** ```typescript /** * {Provider} {Domain} Provider - Raw Types * * Actual API response structure from {Provider}. */ import { z } from "zod"; export const {provider}{Domain}RawSchema = z.object({ // Raw API fields (different naming, types, structure) }); export type {Provider}{Domain}Raw = z.infer; ``` ### **providers/{provider}/mapper.ts** ```typescript /** * {Provider} {Domain} Provider - Mapper * * Transforms {Provider} raw data into normalized domain types. */ import type { {Domain} } from "../../contract"; import { {domain}Schema } from "../../schema"; import { type {Provider}{Domain}Raw, {provider}{Domain}RawSchema } from "./raw.types"; export function transform{Provider}{Domain}(raw: unknown): {Domain} { // 1. Validate raw data const validated = {provider}{Domain}RawSchema.parse(raw); // 2. Transform to domain model const result: {Domain} = { id: validated.someId, status: mapStatus(validated.rawStatus), // ... map all fields }; // 3. Validate domain model return {domain}Schema.parse(result); } ``` --- ## 🎓 Key Patterns ### **1. Co-location** Everything about a domain lives together: ``` billing/ ├── contract.ts # What billing IS ├── schema.ts # How to validate it └── providers/ # Where it comes FROM ``` ### **2. Provider Isolation** Raw types and mappers stay in `providers/`: ```typescript // ✅ GOOD - Isolated import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers"; // ❌ BAD - Would leak WHMCS details into app code import { WhmcsInvoiceRaw } from "@somewhere/global"; ``` ### **3. Schema-Driven** Domain schemas define the contract: ```typescript // Contract (types) export interface Invoice { ... } // Schema (validation) export const invoiceSchema = z.object({ ... }); // Provider mapper validates against schema return invoiceSchema.parse(transformedData); ``` ### **4. Provider Agnostic** App code never knows about providers: ```typescript // ✅ App only knows domain function displayInvoice(invoice: Invoice) { // Doesn't care if it came from WHMCS, Salesforce, or Stripe } // ✅ Service layer handles providers async function getInvoice(id: number): Promise { const raw = await whmcsClient.getInvoice(id); return transformWhmcsInvoice(raw); // Provider-specific } ``` --- ## 🔄 Adding a New Provider Example: Adding Stripe as an invoice provider **1. Create provider folder:** ```bash mkdir -p packages/domain/billing/providers/stripe ``` **2. Add raw types:** ```typescript // billing/providers/stripe/raw.types.ts export const stripeInvoiceRawSchema = z.object({ id: z.string(), status: z.enum(["draft", "open", "paid", "void"]), // ... Stripe's structure }); ``` **3. Add mapper:** ```typescript // billing/providers/stripe/mapper.ts export function transformStripeInvoice(raw: unknown): Invoice { const stripe = stripeInvoiceRawSchema.parse(raw); return invoiceSchema.parse({ id: parseInt(stripe.id), status: mapStripeStatus(stripe.status), // ... transform to domain model }); } ``` **4. Use in service:** ```typescript // No changes to domain contract needed! import { transformStripeInvoice } from "@customer-portal/domain/billing/providers"; const invoice = transformStripeInvoice(stripeData); ``` --- ## ✨ Benefits 1. **Domain-Centric** - Matches business thinking 2. **Provider Isolation** - No leaking of implementation details 3. **Co-location** - Everything about billing is in `billing/` 4. **Scalable** - New provider = new folder 5. **Single Package** - One `@customer-portal/domain` 6. **Type-Safe** - Schema validation at boundaries 7. **Provider-Agnostic** - App code doesn't know providers exist --- ## 📋 Domain Module Inventory | Module | Key Types | Key Schemas | Provider(s) | Purpose | | --------------- | ------------------------------------ | ------------------------- | ----------------- | --------------------- | | `auth` | `AuthenticatedUser`, `Session` | `authenticatedUserSchema` | — | Authentication types | | `address` | `Address`, `JapanAddress` | `addressSchema` | japanpost | Address management | | `billing` | `Invoice`, `InvoiceItem` | `invoiceSchema` | whmcs | Invoice management | | `checkout` | `CheckoutSession`, `CartItem` | `checkoutSchema` | — | Checkout flow | | `common` | `Money`, `ApiResponse` | `moneySchema` | — | Shared utilities | | `customer` | `Customer`, `CustomerProfile` | `customerSchema` | salesforce | Customer data | | `dashboard` | `DashboardStats` | `dashboardSchema` | — | Dashboard data | | `get-started` | `GetStartedFlow`, `EligibilityCheck` | `getStartedSchema` | — | Signup flow | | `notifications` | `Notification` | `notificationSchema` | — | Notifications | | `opportunity` | `Opportunity` | `opportunitySchema` | salesforce | Opportunity lifecycle | | `orders` | `Order`, `OrderDetails`, `OrderItem` | `orderSchema` | salesforce, whmcs | Order management | | `payments` | `PaymentMethod`, `PaymentGateway` | `paymentMethodSchema` | whmcs | Payment methods | | `services` | `CatalogProduct`, `ServiceCategory` | `catalogProductSchema` | salesforce | Product catalog | | `sim` | `SimDetails`, `SimUsage` | `simDetailsSchema` | freebit | SIM management | | `subscriptions` | `Subscription`, `SubscriptionStatus` | `subscriptionSchema` | whmcs, salesforce | Active services | | `support` | `Case`, `CaseMessage` | `caseSchema` | salesforce | Support cases | | `toolkit` | `Formatting`, `AsyncState` | — | — | Utilities | --- ## 📚 Related Documentation - [Import Hygiene](./import-hygiene.md) - Import rules and ESLint enforcement - [Domain Packages](./packages.md) - Package internal structure - [Domain Types](./types.md) - Unified type system - [BFF Integration Patterns](../bff/integration-patterns.md) - How BFF uses domain mappers - [Architecture Decisions](../../decisions/README.md) - Why these patterns exist