Files
ISA-Frontend/docs/architecture/adr/0002-models-schemas-dtos-architecture.md
Lorenz Hilpert 89b3d9aa60 Merged PR 2000: open tasks
Related work items: #5309
2025-11-06 10:01:41 +00:00

30 KiB

ADR 0002: Models, Schemas, and DTOs Architecture

Field Value
Status Draft
Date 2025-11-03
Owners TBD
Participants TBD
Related ADRs ADR-0001
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:

// 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

// ✅ 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)

// 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;
// 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>;
// 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)

// 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;
// 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>;
// 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:

// 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;
// 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:

// 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.

// 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()?

// ❌ 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

// 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';
// 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';
// 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

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

export type Order = OrderDTO;
// 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>;
// 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

// 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:

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:

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.