# ADR 0002: Models, Schemas, and DTOs Architecture

| Field | Value |
|-------|-------|
| Status | Draft |
| Date | 2025-11-03 |
| Owners | TBD |
| Participants | TBD |
| Related ADRs | [ADR-0001](./0001-implement-data-access-api-requests.md) |
| Tags | architecture, data-access, models, schemas, dto, validation |

---

## Summary (Decision in One Sentence)
Encapsulate all generated Swagger DTOs within data-access libraries, use domain-specific models even when names collide, define full Zod schemas with partial validation at service-level for request data only, and export identical cross-domain models from common/data-access.

## Context & Problem Statement

**Current Issues:**
- Generated DTOs (`@generated/swagger/*`) directly imported in 50+ feature/UI files
- Same interface names (e.g., `PayerDTO`, `BranchDTO`) with different properties across 10 APIs
- Union type workarounds (`Product = CatProductDTO | CheckoutProductDTO | OmsProductDTO`) lose type safety
- Inconsistent Zod schema coverage - some types validated, others not
- Type compatibility issues between models and schemas with identical interfaces
- Component-local type redefinitions instead of shared models
- No clear pattern for partial validation (validate some fields, send all data)

**Example Conflicts:**
```typescript
// checkout-api: Minimal PayerDTO (3 properties)
export interface PayerDTO {
  payerNumber?: string;
  payerStatus?: PayerStatus;
  payerType?: PayerType;
}

// crm-api: Full PayerDTO (17 properties)
export interface PayerDTO {
  payerNumber?: string;
  address?: AddressDTO;
  communicationDetails?: CommunicationDetailsDTO;
  // ... 14 more fields
}

// Feature components use aliasing as workaround
import { PayerDTO as CheckoutPayer } from '@generated/swagger/checkout-api';
import { PayerDTO as CrmPayer } from '@generated/swagger/crm-api';
```

**Goals:**
- Encapsulate generated code as implementation detail
- Eliminate type name conflicts across domains
- Standardize validation patterns
- Support partial validation while sending complete data
- Improve type safety and developer experience

**Constraints:**
- Must integrate with 10 existing Swagger generated clients
- Cannot break existing feature/UI components
- Must support domain-driven architecture
- Validation overhead must remain minimal

**Scope:**
- Model definitions and exports
- Schema architecture and validation strategy
- DTO encapsulation boundaries
- Common vs domain-specific type organization

## Decision

Implement a **four-layer type architecture** for all data-access libraries:

### 1. Generated Layer (Hidden)
- **Location:** `/generated/swagger/[api-name]/`
- **Visibility:** NEVER imported outside data-access libraries
- **Purpose:** Implementation detail, source of truth from backend

### 2. Model Layer (Public API)
- **Location:** `libs/[domain]/data-access/src/lib/models/`
- **Pattern:** Type aliases re-exporting generated DTOs
- **Naming:** Use domain context (e.g., `Product` in both catalogue and checkout)
- **Rule:** Each domain has its own models, even if names collide across domains

### 3. Schema Layer (Validation)
- **Location:** `libs/[domain]/data-access/src/lib/schemas/`
- **Pattern:** Full Zod schemas defining ALL fields
- **Validation:** Derive partial validation schemas using `.pick()` or `.partial()`
- **Purpose:** Runtime validation + type inference

### 4. Common Layer (Shared Types)
- **Location:** `libs/common/data-access/src/lib/models/` and `schemas/`
- **Rule:** Only for models **identical across all APIs** (same name, properties, types, optionality)
- **Examples:** `EntityStatus`, `NotificationChannel` (if truly identical)

### Validation Strategy
1. **Request Validation Only:** Validate data BEFORE sending to backend
2. **Service-Level Validation:** Perform validation in service methods
3. **Partial Validation:** Validate only required fields, send all data
4. **Full Schema Definition:** Define complete schemas even when partial validation used
5. **No Response Validation:** Trust backend responses without validation

### Export Rules
```typescript
// ✅ Data-access exports
export * from './lib/models';     // Type aliases over DTOs
export * from './lib/schemas';    // Zod schemas
export * from './lib/services';   // Business logic

// ❌ NEVER export generated code
// export * from '@generated/swagger/catalogue-api';
```

## Rationale

**Why Encapsulate Generated DTOs:**
- **Single Responsibility:** Data-access owns API integration details
- **Change Isolation:** API changes don't ripple through feature layers
- **Clear Boundaries:** Domain logic separated from transport layer
- **Migration Safety:** Can swap generated clients without breaking features

**Why Domain-Specific Models (Not Shared):**
- **Type Safety:** Each domain gets exact DTO shape from its API
- **No Name Conflicts:** `Payer` in checkout vs CRM have different meanings
- **Semantic Clarity:** Same name doesn't mean same concept across domains
- **Avoids Union Types:** Union types lose specificity and auto-completion

**Why Full Schemas with Partial Validation:**
- **Documentation:** Full schema serves as reference for all available fields
- **Flexibility:** Can derive different validation schemas (create vs update vs patch)
- **Type Safety:** `z.infer` provides complete type information
- **Reusability:** Pick different fields for different operations
- **Future-Proof:** New validations can be added without schema rewrites

**Why Service-Level Validation:**
- **Centralized Logic:** All API calls validated consistently
- **Early Failure:** Errors caught before network requests
- **Logged Context:** Validation failures logged with structured data
- **User Feedback:** Services can map validation errors to user messages

**Why No Response Validation:**
- **Performance:** No overhead on every API response
- **Backend Trust:** Backend is source of truth, already validated
- **Simpler Code:** Less boilerplate in services
- **Faster Development:** Focus on request contract, not response parsing

**Evidence Supporting Decision:**
- Analysis shows 50+ files importing generated DTOs (architecture violation)
- `BranchDTO` exists in 7 APIs with subtle differences
- Existing ADR-0001 establishes service patterns this extends
- Current union type patterns (`Product = A | B | C`) cause type narrowing issues

## Consequences

### Positive
- **Clear Architecture:** Generated code hidden behind stable public API
- **No Name Conflicts:** Domain models isolated by library boundaries
- **Type Safety:** Each domain gets precise types from its API
- **Validation Consistency:** All requests validated, responses trusted
- **Developer Experience:** Auto-completion works, no aliasing needed
- **Maintainability:** API changes isolated to data-access layer
- **Performance:** Minimal validation overhead, no response parsing

### Negative
- **Migration Effort:** 50+ files need import updates
- **Learning Curve:** Team must understand model vs DTO distinction
- **Schema Maintenance:** Every model needs corresponding full schema
- **Potential Duplication:** Similar models across domains (by design)
- **Validation Cost:** ~1-2ms overhead per validated request

### Neutral
- **Code Volume:** More files (models + schemas) but better organized
- **Common Models Rare:** Most types will be domain-specific, not common

### Risks & Mitigation
- **Risk:** Developers might accidentally import generated DTOs
  - **Mitigation:** ESLint rule to prevent `@generated/swagger/*` imports outside data-access

- **Risk:** Unclear when model should be common vs domain-specific
  - **Mitigation:** Decision tree in documentation (see below)

- **Risk:** Partial validation might miss critical fields
  - **Mitigation:** Code review focus on validation schemas, tests for edge cases

## Detailed Design Elements

### Decision Tree: Where Should a Model Live?

```
┌─────────────────────────────────────────────────────────────┐
│ Is the DTO IDENTICAL in all generated APIs?                │
│ (same name, properties, types, optional/required status)   │
└──┬───────────────────────────────────────────────┬──────────┘
   │ YES                                           │ NO
   │                                               │
   ▼                                               ▼
┌──────────────────────────────┐  ┌───────────────────────────────┐
│ libs/common/data-access/     │  │ Is it used in multiple        │
│   models/[type].ts           │  │ domains?                      │
│                              │  └───┬───────────────────────┬───┘
│ Export once, import in all   │      │ YES                   │ NO
│ domain data-access libs      │      │                       │
└──────────────────────────────┘      ▼                       ▼
                            ┌──────────────────┐  ┌─────────────────┐
                            │ Create separate  │  │ Single domain's │
                            │ model in EACH    │  │ data-access     │
                            │ domain's         │  │ library         │
                            │ data-access      │  └─────────────────┘
                            └──────────────────┘
                            Example: Product exists in
                            catalogue, checkout, oms
                            with different shapes
```

### Pattern 1: Domain-Specific Model (Most Common)

```typescript
// libs/catalogue/data-access/src/lib/models/product.ts
import { ProductDTO } from '@generated/swagger/catalogue-api';

/**
 * Product model for catalogue domain.
 *
 * Represents a product in the product catalogue with full details
 * including pricing, availability, and metadata.
 */
export type Product = ProductDTO;
```

```typescript
// libs/catalogue/data-access/src/lib/schemas/product.schema.ts
import { z } from 'zod';

/**
 * Full Zod schema for Product entity.
 * Defines all fields available in the Product model.
 */
export const ProductSchema = z.object({
  id: z.string().optional(),
  name: z.string().min(1).max(255),
  contributor: z.string().optional(),
  price: z.number().positive().optional(),
  description: z.string().max(2000).optional(),
  categoryId: z.string().optional(),
  stockQuantity: z.number().int().nonnegative().optional(),
  imageUrl: z.string().url().optional(),
  isActive: z.boolean().optional(),
  metadata: z.record(z.string(), z.unknown()).optional(),
});

/**
 * Validation schema for creating a product.
 * Validates only required fields: name must be present and valid.
 * Other fields are sent to API but not validated.
 */
export const CreateProductSchema = ProductSchema.pick({
  name: true,
});

/**
 * Validation schema for updating a product.
 * Requires id and name, other fields optional.
 */
export const UpdateProductSchema = ProductSchema.pick({
  id: true,
  name: true,
}).required();

/**
 * Inferred types from schemas
 */
export type Product = z.infer<typeof ProductSchema>;
export type CreateProductInput = z.input<typeof CreateProductSchema>;
export type UpdateProductInput = z.input<typeof UpdateProductSchema>;
```

```typescript
// libs/catalogue/data-access/src/lib/services/products.service.ts
import { inject, Injectable } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { ProductService, ProductDTO } from '@generated/swagger/catalogue-api';
import { CreateProductSchema, UpdateProductSchema } from '../schemas';
import { logger } from '@isa/core/logging';

@Injectable({ providedIn: 'root' })
export class ProductsService {
  readonly #log = logger(ProductsService);
  readonly #productService = inject(ProductService);

  /**
   * Creates a new product.
   * Validates required fields before sending to API.
   * Sends all product data (validated and unvalidated fields).
   */
  async createProduct(product: ProductDTO): Promise<ProductDTO | null> {
    // Validate only required fields (name)
    const validationResult = CreateProductSchema.safeParse(product);

    if (!validationResult.success) {
      this.#log.error('Product validation failed', {
        errors: validationResult.error.format(),
        product
      });
      throw new Error(
        `Invalid product data: ${validationResult.error.message}`
      );
    }

    this.#log.debug('Creating product', { name: product.name });

    // Send ALL product data to API (including unvalidated fields)
    const response = await firstValueFrom(
      this.#productService.createProduct(product)
    );

    // No response validation - trust backend
    if (response.error) {
      this.#log.error('Failed to create product', {
        error: response.message
      });
      throw new Error(response.message);
    }

    return response.result ?? null;
  }

  /**
   * Updates an existing product.
   * Validates id and name are present.
   */
  async updateProduct(
    id: string,
    product: ProductDTO
  ): Promise<ProductDTO | null> {
    // Validate required fields for update (id + name)
    const validationResult = UpdateProductSchema.safeParse({ id, ...product });

    if (!validationResult.success) {
      this.#log.error('Product update validation failed', {
        errors: validationResult.error.format()
      });
      throw new Error(
        `Invalid product update: ${validationResult.error.message}`
      );
    }

    this.#log.debug('Updating product', { id, name: product.name });

    const response = await firstValueFrom(
      this.#productService.updateProduct(id, product)
    );

    if (response.error) {
      this.#log.error('Failed to update product', {
        error: response.message,
        id
      });
      throw new Error(response.message);
    }

    return response.result ?? null;
  }

  /**
   * Fetches a product by ID.
   * No validation needed for GET requests.
   */
  async getProduct(id: string): Promise<ProductDTO | null> {
    this.#log.debug('Fetching product', { id });

    const response = await firstValueFrom(
      this.#productService.getProduct(id)
    );

    if (response.error) {
      this.#log.error('Failed to fetch product', {
        error: response.message,
        id
      });
      throw new Error(response.message);
    }

    // No response validation
    return response.result ?? null;
  }
}
```

### Pattern 2: Common Model (Identical Across All APIs)

```typescript
// libs/common/data-access/src/lib/models/notification-channel.ts
import { NotificationChannel as CheckoutNotificationChannel } from '@generated/swagger/checkout-api';
import { NotificationChannel as CrmNotificationChannel } from '@generated/swagger/crm-api';
import { NotificationChannel as OmsNotificationChannel } from '@generated/swagger/oms-api';

/**
 * NotificationChannel is identical across all APIs.
 *
 * Verification:
 * - checkout-api: type NotificationChannel = 0 | 1 | 2 | 4
 * - crm-api: type NotificationChannel = 0 | 1 | 2 | 4
 * - oms-api: type NotificationChannel = 0 | 1 | 2 | 4
 *
 * All three definitions are identical, so we export once from common.
 */
export type NotificationChannel =
  | CheckoutNotificationChannel
  | CrmNotificationChannel
  | OmsNotificationChannel;

// Alternative if truly identical (pick one as canonical):
// export type NotificationChannel = CheckoutNotificationChannel;
```

```typescript
// libs/common/data-access/src/lib/schemas/notification-channel.schema.ts
import { z } from 'zod';

/**
 * Zod schema for NotificationChannel enum.
 *
 * Values:
 * - 0: Email
 * - 1: SMS
 * - 2: Push Notification
 * - 4: Phone Call
 */
export const NotificationChannelSchema = z.union([
  z.literal(0),
  z.literal(1),
  z.literal(2),
  z.literal(4),
]);

export type NotificationChannel = z.infer<typeof NotificationChannelSchema>;
```

```typescript
// Domain data-access libs re-export from common
// libs/checkout/data-access/src/lib/models/index.ts
export { NotificationChannel } from '@isa/common/data-access';

// libs/crm/data-access/src/lib/schemas/index.ts
export { NotificationChannelSchema } from '@isa/common/data-access';
```

### Pattern 3: Multiple Domain Models (Same Name, Different Structure)

When DTOs with the same name have different structures, keep them separate:

```typescript
// libs/checkout/data-access/src/lib/models/payer.ts
import { PayerDTO } from '@generated/swagger/checkout-api';

/**
 * Payer model for checkout domain.
 *
 * Minimal payer information needed during checkout flow.
 * Contains only basic identification and status.
 */
export type Payer = PayerDTO;
```

```typescript
// libs/crm/data-access/src/lib/models/payer.ts
import { PayerDTO } from '@generated/swagger/crm-api';

/**
 * Payer model for CRM domain.
 *
 * Full payer entity with complete address, organization,
 * communication details, and payment settings.
 * Used for payer management and administration.
 */
export type Payer = PayerDTO;
```

**Components import from their respective domain:**
```typescript
// libs/checkout/feature/cart/src/lib/cart.component.ts
import { Payer } from '@isa/checkout/data-access'; // 3-field version

// libs/crm/feature/payers/src/lib/payer-details.component.ts
import { Payer } from '@isa/crm/data-access'; // 17-field version
```

### Pattern 4: Partial Validation with Full Schema

**Scenario:** Product has many fields, but only `name` is required for creation.

```typescript
// Full schema defines ALL fields
export const ProductSchema = z.object({
  id: z.string().optional(),
  name: z.string().min(1).max(255),
  contributor: z.string().optional(),
  price: z.number().positive().optional(),
  description: z.string().optional(),
  categoryId: z.string().optional(),
  stockQuantity: z.number().int().nonnegative().optional(),
  imageUrl: z.string().url().optional(),
  isActive: z.boolean().optional(),
  metadata: z.record(z.string(), z.unknown()).optional(),
  // ... potentially 20+ more fields
});

// Validation schema picks only required field
export const CreateProductSchema = ProductSchema.pick({
  name: true,
});

// Service validates partial, sends complete
async createProduct(product: ProductDTO): Promise<ProductDTO | null> {
  // Validate: only name is checked
  CreateProductSchema.parse(product);

  // Send: all fields (name, contributor, price, description, etc.)
  const response = await this.api.createProduct(product);

  return response.result;
}
```

**Why not `.passthrough()`?**
```typescript
// ❌ Avoid this approach
const CreateProductSchema = z.object({
  name: z.string().min(1),
}).passthrough();

// Type inference loses other fields
type Inferred = z.infer<typeof CreateProductSchema>;
// Result: { name: string } & { [key: string]: unknown }
// Lost: contributor, price, description types

// ✅ Prefer this approach
const CreateProductSchema = ProductSchema.pick({ name: true });

// Full ProductSchema defined elsewhere provides complete type
type Product = z.infer<typeof ProductSchema>;
// Result: { id?: string; name: string; contributor?: string; ... }
```

### Export Structure

```typescript
// libs/catalogue/data-access/src/index.ts

// Public API exports
export * from './lib/models';      // Type aliases over DTOs
export * from './lib/schemas';     // Zod schemas
export * from './lib/services';    // Business logic
export * from './lib/resources';   // Angular resources (optional)
export * from './lib/stores';      // State management (optional)
export * from './lib/helpers';     // Utilities (optional)

// ❌ NEVER export generated code
// This would break encapsulation:
// export * from '@generated/swagger/catalogue-api';
```

```typescript
// libs/catalogue/data-access/src/lib/models/index.ts

// Re-export all domain models
export * from './product';
export * from './category';
export * from './supplier';
export * from './inventory';

// May also re-export common models
export { EntityStatus, NotificationChannel } from '@isa/common/data-access';
```

```typescript
// libs/catalogue/data-access/src/lib/schemas/index.ts

// Re-export all domain schemas
export * from './product.schema';
export * from './category.schema';
export * from './supplier.schema';
export * from './inventory.schema';

// May also re-export common schemas
export { EntityStatusSchema } from '@isa/common/data-access';
```

## Code Examples

### Complete Example: Order in OMS Domain

```typescript
// libs/oms/data-access/src/lib/models/order.ts
import { OrderDTO } from '@generated/swagger/oms-api';

export type Order = OrderDTO;
```

```typescript
// libs/oms/data-access/src/lib/schemas/order.schema.ts
import { z } from 'zod';
import { OrderItemSchema } from './order-item.schema';
import { OrderStatusSchema } from '@isa/common/data-access';

export const OrderSchema = z.object({
  id: z.string().optional(),
  orderNumber: z.string().min(1),
  customerId: z.string().uuid(),
  items: z.array(OrderItemSchema).min(1),
  status: OrderStatusSchema.optional(),
  totalAmount: z.number().nonnegative().optional(),
  shippingAddress: z.string().optional(),
  billingAddress: z.string().optional(),
  createdAt: z.string().datetime().optional(),
  updatedAt: z.string().datetime().optional(),
});

export const CreateOrderSchema = OrderSchema.pick({
  customerId: true,
  items: true,
});

export const UpdateOrderStatusSchema = OrderSchema.pick({
  id: true,
  status: true,
}).required();

export type Order = z.infer<typeof OrderSchema>;
export type CreateOrderInput = z.input<typeof CreateOrderSchema>;
export type UpdateOrderStatusInput = z.input<typeof UpdateOrderStatusSchema>;
```

```typescript
// libs/oms/data-access/src/lib/services/orders.service.ts
import { inject, Injectable } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { OrderService, OrderDTO } from '@generated/swagger/oms-api';
import { CreateOrderSchema, UpdateOrderStatusSchema } from '../schemas';
import { logger } from '@isa/core/logging';

@Injectable({ providedIn: 'root' })
export class OrdersService {
  readonly #log = logger(OrdersService);
  readonly #orderService = inject(OrderService);

  async createOrder(order: OrderDTO): Promise<OrderDTO | null> {
    const validationResult = CreateOrderSchema.safeParse(order);

    if (!validationResult.success) {
      this.#log.error('Order validation failed', {
        errors: validationResult.error.format()
      });
      throw new Error(
        `Invalid order: ${validationResult.error.message}`
      );
    }

    this.#log.debug('Creating order', {
      customerId: order.customerId,
      itemCount: order.items?.length
    });

    const response = await firstValueFrom(
      this.#orderService.createOrder(order)
    );

    if (response.error) {
      this.#log.error('Failed to create order', {
        error: response.message
      });
      throw new Error(response.message);
    }

    return response.result ?? null;
  }

  async updateOrderStatus(
    id: string,
    status: string
  ): Promise<OrderDTO | null> {
    UpdateOrderStatusSchema.parse({ id, status });

    this.#log.debug('Updating order status', { id, status });

    const response = await firstValueFrom(
      this.#orderService.updateOrderStatus(id, status)
    );

    if (response.error) {
      this.#log.error('Failed to update order status', {
        error: response.message,
        id
      });
      throw new Error(response.message);
    }

    return response.result ?? null;
  }
}
```

### Usage in Feature Component

```typescript
// libs/oms/feature/orders/src/lib/create-order.component.ts
import { Component, inject, signal } from '@angular/core';
import { OrdersService, Order, CreateOrderInput } from '@isa/oms/data-access';

@Component({
  selector: 'app-create-order',
  template: `
    <form (ngSubmit)="submit()">
      <!-- Form fields -->
      @if (error()) {
        <div class="error">{{ error() }}</div>
      }
      <button type="submit">Create Order</button>
    </form>
  `
})
export class CreateOrderComponent {
  readonly #ordersService = inject(OrdersService);

  error = signal<string | null>(null);

  async submit() {
    try {
      // Build order data
      const orderInput: CreateOrderInput = {
        customerId: this.customerId,
        items: this.items,
        // Optional fields can be included
        shippingAddress: this.shippingAddress,
        billingAddress: this.billingAddress,
      };

      // Service validates required fields, sends all data
      const created = await this.#ordersService.createOrder(orderInput);

      // Navigate to order details...
    } catch (err) {
      this.error.set(err instanceof Error ? err.message : 'Failed to create order');
    }
  }
}
```

## Migration Strategy

### Phase 1: New Development (Immediate)
- All new models follow this ADR
- All new schemas use full definition + partial validation
- All new services validate requests only

### Phase 2: Incremental Migration (Ongoing)
- When touching existing code, update imports
- Replace generated DTO imports with data-access model imports
- Add validation schemas for existing services

### Phase 3: Cleanup (Future)
- Add ESLint rule preventing `@generated/swagger/*` imports outside data-access
- Automated codemod to fix remaining violations
- Remove union type workarounds

### Migration Checklist (Per Domain)

- [ ] Create `models/` folder with type aliases over generated DTOs
- [ ] Create `schemas/` folder with full Zod schemas
- [ ] Add partial validation schemas (`.pick()` for required fields)
- [ ] Update services to validate before API calls
- [ ] Export models and schemas from data-access index
- [ ] Update feature components to import from data-access
- [ ] Remove direct `@generated/swagger/*` imports
- [ ] Verify no union types for different shapes
- [ ] Move truly identical models to common/data-access

## Open Questions / Follow-Ups

### For Team Discussion

1. **ESLint Rule Priority:** Should we add the ESLint rule immediately or after migration?
   - Immediate: Prevents new violations
   - After migration: Less friction during transition

2. **Validation Error Handling:** How should services communicate validation errors to UI?
   - Throw generic Error (current approach)
   - Custom ValidationError class with structured field errors
   - Return Result<T, E> pattern instead of throwing

3. **Common Model Criteria:** Should we require 100% identical or allow minor differences?
   - Strict: Must be byte-for-byte identical
   - Lenient: Same semantic meaning, slight type differences OK

4. **Schema Generation:** Should we auto-generate Zod schemas from Swagger specs?
   - Pro: Less manual work, stays in sync
   - Con: Generated schemas might not match domain needs

5. **Response Validation:** Any exceptions where we SHOULD validate responses?
   - Critical paths (payments, checkout)?
   - External APIs (not our backend)?

### Dependent Decisions

- [ ] Define custom ValidationError class structure
- [ ] Decide on ESLint rule configuration
- [ ] Document common model approval process
- [ ] Create code generation tooling (if desired)

## Decision Review & Revalidation

**Review Triggers:**
- After 3 months of adoption (2025-02-03)
- When migration >50% complete
- If validation overhead becomes measurable performance issue
- If new backend API patterns emerge

**Success Metrics:**
- Zero `@generated/swagger/*` imports outside data-access (ESLint violations)
- 100% of services have request validation
- <5% of models in common/data-access (most are domain-specific)
- Developer survey shows improved clarity (>80% satisfaction)

**Failure Criteria (Revert Decision):**
- Validation overhead >10ms per request (current: ~1-2ms)
- Common models >30% (suggests wrong criteria)
- Excessive developer friction (>50% negative feedback)

## Status Log

| Date | Change | Author |
|------|--------|--------|
| 2025-11-03 | Created (Draft) | TBD |

## References

**Related ADRs:**
- [ADR-0001: Implement data-access API Requests](./0001-implement-data-access-api-requests.md) - Establishes service patterns this extends

**Existing Codebase:**
- `/generated/swagger/` - 10 generated API clients
- `libs/*/data-access/` - 7 existing data-access libraries
- `libs/common/data-access/` - Shared types and utilities

**External Documentation:**
- [Zod Documentation](https://zod.dev/) - Schema validation library
- [ng-swagger-gen](https://github.com/cyclosproject/ng-swagger-gen) - OpenAPI client generator

**Migration Resources:**
- Comprehensive guide: `/docs/architecture/models-schemas-dtos-guide.md`
- Example implementations in catalogue, oms, crm data-access libraries

---
> Document updates MUST reference this ADR number in commit messages: `ADR-0002:` prefix.
> Keep this document updated through all lifecycle stages.
