# TypeScript Code Patterns & Principles

## Review Checklist

Before submitting code, ensure:

- [ ] Code prioritizes readability and maintainability ([Core Philosophy](#core-philosophy))
- [ ] Types are explicit where helpful, inferred where obvious ([Type Inference](#leverage-type-inference))
- [ ] No `any` without documentation ([No Any](#no-any-without-documentation))
- [ ] Using `unknown` over `any` when needed ([Prefer Unknown](#prefer-unknown-over-any))
- [ ] Strict null checks enabled and handled ([Strict Null Checks](#strict-null-checks))
- [ ] Interfaces for objects, type aliases for unions ([Data Structures](#prefer-interfaces-for-object-types))
- [ ] Discriminated unions for state management ([State Patterns](#discriminated-unions-for-state))
- [ ] Const assertions for literal types ([Const Assertions](#const-assertions-for-literals))
- [ ] Functions follow single responsibility ([Single Responsibility](#single-responsibility))
- [ ] Pure functions where possible ([Pure Functions](#prefer-pure-functions))
- [ ] Guard clauses over nested conditionals ([Guard Clauses](#guard-clauses-over-nested-conditionals))
- [ ] Result types for expected errors ([Error Handling](#result-types-over-exceptions))
- [ ] Custom error classes implemented ([Custom Errors](#custom-error-classes))
- [ ] Async/await over promise chains ([Async Patterns](#asyncawait-over-promise-chains))
- [ ] Parallel execution where appropriate ([Parallel Execution](#parallel-execution-when-possible))
- [ ] Props validated with explicit types ([Component Patterns](#props-validation))
- [ ] Composition over configuration ([Composition](#composition-over-configuration))
- [ ] Testing behavior not implementation ([Testing Patterns](#test-behavior-not-implementation))
- [ ] Arrange-Act-Assert pattern used ([Test Structure](#arrange-act-assert-pattern))
- [ ] Barrel exports for clean APIs ([Module Organization](#barrel-exports))
- [ ] Dependencies point inward ([Dependency Direction](#dependency-direction))
- [ ] Expensive operations memoized ([Performance](#memoization-for-expensive-operations))
- [ ] Lazy loading implemented ([Lazy Loading](#lazy-loading))
- [ ] Naming conventions followed ([Code Style](#naming-conventions))
- [ ] Files organized consistently ([File Organization](#file-organization))
- [ ] Public APIs documented with JSDoc ([Documentation](#jsdoc-for-public-apis))
- [ ] Complex logic has explanatory comments ([Inline Comments](#inline-comments-for-complex-logic))
- [ ] Gradual migration strategy used ([Migration](#gradual-migration-strategy))
- [ ] Feature flags for breaking changes ([Feature Flags](#feature-flags-for-breaking-changes))
- [ ] Repositories implement standard interface ([Repository Pattern](#repository-pattern))
- [ ] Repository methods follow naming conventions ([Repository Naming](#repository-method-naming-conventions))
- [ ] Repositories handle errors with Result types ([Repository Errors](#repository-error-handling))
- [ ] Repository transactions properly managed ([Transactions](#repository-transaction-patterns))
- [ ] Repositories properly mocked in tests ([Testing Repositories](#testing-repositories))

## Core Philosophy

Our TypeScript codebase prioritizes type safety, maintainability, and developer experience. We write code that is explicit rather than clever, favoring readability over brevity. Every decision should make the codebase easier to understand, test, and modify.

## Type System Principles

### Leverage Type Inference
Let TypeScript infer types when obvious, but be explicit when it adds clarity or catches errors earlier.

**Why:** Reduces code verbosity without sacrificing type safety. TypeScript's inference engine is sophisticated enough to understand most common patterns, and being explicit only when necessary strikes the right balance between readability and safety.

```typescript
// Good - obvious type
const userCount = 42;

// Good - explicit for clarity
const processUser = (id: string): Promise<User> => {
  return fetchUser(id);
};
```

### No Any Without Documentation
The `any` type defeats TypeScript's purpose. When absolutely necessary, document why and plan for removal.

**Why:** Using `any` disables all type checking for that value, making it easy to introduce runtime errors that TypeScript was specifically designed to prevent. Documentation ensures technical debt is visible and tracked.

```typescript
// Bad
const processData = (data: any) => { };

// Acceptable with documentation
const legacyApiResponse = data as any; // TODO: Type after legacy API migration Q2 2025
```

### Prefer Unknown Over Any
Use `unknown` for truly unknown types and narrow with type guards.

**Why:** Unlike `any`, `unknown` forces you to perform type checking before using the value, maintaining type safety while still allowing flexibility for dynamic data. This catches errors at compile time rather than runtime.

```typescript
const parseJson = (json: string): unknown => {
  return JSON.parse(json);
};

const isUser = (value: unknown): value is User => {
  return typeof value === 'object' && 
         value !== null && 
         'id' in value;
};
```

### Strict Null Checks
Always enable strict null checking. Make nullable types explicit and handle them properly.

**Why:** Null and undefined errors are among the most common runtime bugs in JavaScript. Strict null checking forces developers to handle these cases explicitly, preventing "Cannot read property of undefined" errors in production.

```typescript
interface User {
  id: string;
  email: string;
  name?: string; // Explicitly optional
}

const greetUser = (user: User) => {
  // Handle optional field
  const displayName = user.name ?? user.email;
  return `Welcome, ${displayName}`;
};
```

## Data Structure Patterns

### Prefer Interfaces for Object Types
Use interfaces for object shapes and type aliases for unions, primitives, and utility types.

**Why:** Interfaces provide better error messages, support declaration merging for extensibility, and are specifically optimized by TypeScript for object types. Type aliases are better suited for non-object types where their flexibility shines.

```typescript
// Good - interface for object shape
interface UserProfile {
  id: string;
  email: string;
}

// Good - type alias for union
type Status = 'pending' | 'active' | 'archived';

// Good - type alias for utility type
type ReadonlyUser = Readonly<UserProfile>;
```

### Discriminated Unions for State
Use discriminated unions to make invalid states unrepresentable.

**Why:** Discriminated unions leverage TypeScript's control flow analysis to ensure you handle all possible states and access only the properties that exist in each state. This eliminates entire categories of bugs where code tries to access data that doesn't exist in certain states.

```typescript
type AsyncData<T> = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

const renderData = (state: AsyncData<User>) => {
  switch (state.status) {
    case 'success':
      return state.data; // TypeScript knows data exists
    case 'error':
      return state.error.message; // TypeScript knows error exists
  }
};
```

### Const Assertions for Literals
Use const assertions to create readonly, narrow types from literals.

**Why:** Const assertions prevent accidental mutation and provide the narrowest possible type inference. This is especially useful for configuration objects and ensures that literal values aren't accidentally widened to string or number types.

```typescript
const CONFIG = {
  api: {
    timeout: 5000,
    retries: 3,
  },
  features: ['auth', 'payments'] as const
} as const;

type Feature = typeof CONFIG.features[number]; // 'auth' | 'payments'
```

## Function Patterns

### Single Responsibility
Functions should do one thing well. Extract complex logic into smaller, testable functions.

**Why:** Single-responsibility functions are easier to test, debug, and reuse. They have fewer dependencies, clearer names, and when they fail, it's immediately obvious what went wrong. This also makes code reviews more focused and refactoring safer.

```typescript
// Bad - doing too much
const processUserData = async (id: string) => {
  const user = await fetchUser(id);
  const validated = validateUser(user);
  await sendEmail(validated.email);
  await updateLastActive(id);
  return formatted(validated);
};

// Good - composed single-purpose functions
const getValidatedUser = async (id: string): Promise<User> => {
  const user = await fetchUser(id);
  return validateUser(user);
};

const notifyUser = async (user: User): Promise<void> => {
  await sendEmail(user.email);
};
```

### Prefer Pure Functions
Write pure functions when possible. Side effects should be isolated and explicit.

**Why:** Pure functions are predictable, testable without mocks, and can be safely cached or parallelized. They make debugging easier because the output depends only on the input, and they can be reasoned about in isolation.

```typescript
// Pure function - predictable and testable
const calculateDiscount = (price: number, tier: CustomerTier): number => {
  const discountRate = tierDiscounts[tier];
  return price * (1 - discountRate);
};

// Side effects isolated and explicit
const applyDiscount = async (orderId: string, discount: number): Promise<void> => {
  await database.orders.update(orderId, { discount });
};
```

### Guard Clauses Over Nested Conditionals
Exit early to reduce nesting and improve readability.

**Why:** Guard clauses reduce cognitive load by handling edge cases upfront, leaving the main logic unindented and easy to follow. This pattern also makes it clear what conditions must be met for the function to execute successfully.

```typescript
// Bad - nested conditionals
const processPayment = (payment: Payment) => {
  if (payment.amount > 0) {
    if (payment.currency === 'USD') {
      if (payment.verified) {
        return charge(payment);
      }
    }
  }
};

// Good - guard clauses
const processPayment = (payment: Payment) => {
  if (payment.amount <= 0) {
    throw new Error('Invalid amount');
  }
  
  if (payment.currency !== 'USD') {
    throw new Error('Unsupported currency');
  }
  
  if (!payment.verified) {
    throw new Error('Payment not verified');
  }
  
  return charge(payment);
};
```

## Error Handling Patterns

### Result Types Over Exceptions
Use Result types for expected errors, exceptions for unexpected failures.

**Why:** Result types make error handling explicit in the type system, forcing callers to handle both success and failure cases. This prevents unhandled errors and makes error flows visible at compile time, unlike exceptions which are invisible in function signatures.

```typescript
type Result<T, E = Error> = 
  | { ok: true; value: T }
  | { ok: false; error: E };

const parseConfig = (json: string): Result<Config> => {
  try {
    const config = JSON.parse(json);
    return { ok: true, value: config };
  } catch (error) {
    return { ok: false, error: new Error('Invalid config') };
  }
};

// Usage
const result = parseConfig(configString);
if (result.ok) {
  useConfig(result.value);
} else {
  handleError(result.error);
}
```

### Custom Error Classes
Create specific error types for different failure scenarios.

**Why:** Custom error classes allow for more precise error handling, better logging, and clearer error messages. They enable catch blocks to handle different errors differently and make debugging easier by providing context-specific information.

```typescript
class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

class ApiError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
    this.name = 'ApiError';
  }
}
```

## Async Patterns

### Async/Await Over Promise Chains
Use async/await for cleaner, more readable asynchronous code.

**Why:** Async/await makes asynchronous code look and behave like synchronous code, making it easier to understand, debug, and maintain. Stack traces are clearer, and error handling with try/catch is more intuitive than promise chain callbacks.

```typescript
// Bad - promise chains
const loadUserData = (id: string) => {
  return fetchUser(id)
    .then(user => enrichUserData(user))
    .then(enriched => cacheUser(enriched))
    .catch(handleError);
};

// Good - async/await
const loadUserData = async (id: string) => {
  try {
    const user = await fetchUser(id);
    const enriched = await enrichUserData(user);
    return await cacheUser(enriched);
  } catch (error) {
    handleError(error);
  }
};
```

### Parallel Execution When Possible
Use Promise.all for independent async operations.

**Why:** Running independent operations in parallel significantly improves performance by reducing total wait time. Sequential execution when parallel is possible wastes time and creates poor user experiences, especially in network-bound operations.

```typescript
// Bad - sequential when parallel is possible
const loadDashboard = async (userId: string) => {
  const profile = await fetchProfile(userId);
  const posts = await fetchPosts(userId);
  const stats = await fetchStats(userId);
  return { profile, posts, stats };
};

// Good - parallel execution
const loadDashboard = async (userId: string) => {
  const [profile, posts, stats] = await Promise.all([
    fetchProfile(userId),
    fetchPosts(userId),
    fetchStats(userId)
  ]);
  return { profile, posts, stats };
};
```

## Component Patterns (React/Vue/Angular Agnostic)

### Props Validation
Define explicit prop types and use them consistently.

**Why:** Explicit prop types serve as documentation, enable IDE autocomplete, catch errors at compile time, and make components self-documenting. This prevents runtime errors from incorrect prop usage and makes refactoring safer.

```typescript
interface ButtonProps {
  variant: 'primary' | 'secondary';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
  onClick: () => void;
  children: React.ReactNode; // or appropriate type for your framework
}
```

### Composition Over Configuration
Prefer component composition over complex configuration objects.

**Why:** Composition creates more flexible, reusable components that are easier to understand and modify. It avoids the "props explosion" problem and allows for better tree-shaking. Each composed component has a single, clear responsibility.

```typescript
// Bad - configuration object
<Modal 
  config={{
    hasHeader: true,
    hasFooter: true,
    headerText: 'Title',
    footerButtons: ['Cancel', 'Save']
  }}
/>

// Good - composition
<Modal>
  <ModalHeader>Title</ModalHeader>
  <ModalBody>{content}</ModalBody>
  <ModalFooter>
    <Button>Cancel</Button>
    <Button variant="primary">Save</Button>
  </ModalFooter>
</Modal>
```

## Testing Patterns

### Test Behavior, Not Implementation
Focus on what the code does, not how it does it.

**Why:** Testing behavior makes tests resilient to refactoring. When you change how code works internally but maintain the same behavior, tests shouldn't break. This gives confidence to refactor and improve code without fear of breaking tests.

```typescript
// Bad - testing implementation details
test('uses forEach to sum array', () => {
  const spy = jest.spyOn(Array.prototype, 'forEach');
  sumArray([1, 2, 3]);
  expect(spy).toHaveBeenCalled();
});

// Good - testing behavior
test('sums array values correctly', () => {
  expect(sumArray([1, 2, 3])).toBe(6);
  expect(sumArray([])).toBe(0);
  expect(sumArray([-1, 1])).toBe(0);
});
```

### Arrange-Act-Assert Pattern
Structure tests clearly with setup, execution, and verification phases.

**Why:** This pattern makes tests easier to read and understand by creating a consistent structure. It clearly separates test setup from the action being tested and the verification, making it obvious what is being tested and what the expected outcome is.

```typescript
test('user can update profile', async () => {
  // Arrange
  const user = createTestUser();
  const updates = { name: 'New Name' };
  
  // Act
  const result = await updateProfile(user.id, updates);
  
  // Assert
  expect(result.name).toBe('New Name');
  expect(result.updatedAt).toBeDefined();
});
```

## Module Organization

### Barrel Exports
Use index files to create clean public APIs for modules.

**Why:** Barrel exports provide a clear contract between modules, hide internal implementation details, and simplify imports. They make refactoring easier by centralizing exports and create a clear distinction between public and private module members.

```typescript
// components/Button/Button.tsx
export const Button: FC<ButtonProps> = () => { };

// components/Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button.types';

// Usage
import { Button } from '@/components/Button';
```

### Dependency Direction
Dependencies should point inward. Domain logic shouldn't depend on infrastructure.

**Why:** This creates a stable, testable architecture where core business logic is isolated from external concerns. It makes the system more maintainable, allows for easier testing with mocks, and enables swapping out infrastructure without touching domain logic.

```
✅ Correct dependency direction:
UI Components → Application Services → Domain Logic → Domain Types

❌ Incorrect:
Domain Logic → UI Components
```

## Performance Patterns

### Memoization for Expensive Operations
Use memoization for computationally expensive pure functions.

**Why:** Memoization trades memory for speed, avoiding redundant calculations for the same inputs. This is especially valuable for expensive computations in render cycles or frequently called functions, significantly improving application responsiveness.

```typescript
const memoize = <T extends (...args: any[]) => any>(fn: T): T => {
  const cache = new Map();
  return ((...args) => {
    const key = JSON.stringify(args);
    if (!cache.has(key)) {
      cache.set(key, fn(...args));
    }
    return cache.get(key);
  }) as T;
};

const expensiveCalculation = memoize((data: LargeDataSet) => {
  // Complex computation
});
```

### Lazy Loading
Load code and data only when needed.

**Why:** Lazy loading reduces initial bundle size, improves time-to-interactive, and saves bandwidth by loading resources only when required. This is crucial for performance on slower networks and devices, creating better user experiences.

```typescript
// Lazy load components
const AdminPanel = lazy(() => import('./AdminPanel'));

// Lazy load data
const loadUserPreferences = async (userId: string) => {
  const { preferences } = await import('./preferences');
  return preferences.load(userId);
};
```

## Code Style Conventions

### Naming Conventions
- **Variables/Functions**: camelCase
- **Types/Interfaces**: PascalCase
- **Constants**: UPPER_SNAKE_CASE
- **Private fields**: prefix with underscore
- **Boolean variables**: prefix with is/has/should

**Why:** Consistent naming conventions reduce cognitive load, make code scannable, and convey meaning through convention. They eliminate debates about style, make the codebase feel cohesive, and help developers quickly understand the role of different identifiers.

```typescript
const MAX_RETRIES = 3;
const isValid = true;
const hasPermission = checkAuth();

interface UserAccount {
  id: string;
  isActive: boolean;
}

class Service {
  private _cache: Map<string, any>;
}
```

### File Organization
Structure files consistently:

**Why:** Consistent file structure makes code navigation predictable, reduces the time spent searching for definitions, and helps new team members onboard quickly. It also makes code reviews easier by establishing expected patterns.

```typescript
// 1. Imports
import { external } from 'package';
import { internal } from '@/module';
import { local } from './local';

// 2. Types
interface Props { }
type State = { };

// 3. Constants
const DEFAULTS = { };

// 4. Main implementation
export const Component = () => { };

// 5. Helper functions
const helper = () => { };
```

## Documentation Standards

### JSDoc for Public APIs
Document public functions with JSDoc including parameters, returns, and examples.

**Why:** JSDoc provides inline documentation that IDEs can display, making APIs self-documenting. It reduces the need to read implementation code, speeds up development, and serves as a contract for what functions do and how to use them.

```typescript
/**
 * Calculates compound interest
 * @param principal - Initial investment amount
 * @param rate - Annual interest rate (as decimal, e.g., 0.05 for 5%)
 * @param time - Investment period in years
 * @returns Final amount after compound interest
 * @example
 * calculateCompoundInterest(1000, 0.05, 10) // Returns 1628.89
 */
export const calculateCompoundInterest = (
  principal: number,
  rate: number,
  time: number
): number => {
  return principal * Math.pow(1 + rate, time);
};
```

### Inline Comments for Complex Logic
Use comments to explain why, not what.

**Why:** Code shows what is happening, but not always why decisions were made. "Why" comments preserve context that might otherwise be lost, explain non-obvious business rules, and help future developers (including yourself) understand the reasoning behind complex logic.

```typescript
// Bad - explains what (obvious from code)
// Increment counter by 1
counter++;

// Good - explains why
// Increment counter to account for header row in CSV
counter++;
```

## Migration and Deprecation

### Gradual Migration Strategy
Use deprecation warnings and gradual migration paths.

**Why:** Gradual migration prevents breaking changes from disrupting development, gives teams time to update their code, and maintains backward compatibility. This approach respects that different parts of a large codebase move at different speeds.

```typescript
/**
 * @deprecated Use `createUser` instead. Will be removed in v3.0
 */
export const makeUser = (data: any) => {
  console.warn('makeUser is deprecated. Use createUser instead.');
  return createUser(data);
};
```

### Feature Flags for Breaking Changes
Use feature flags to roll out breaking changes gradually.

**Why:** Feature flags allow testing in production with real data, enable quick rollback if issues arise, and let you gradually migrate users. This reduces risk and allows for A/B testing of new implementations before full commitment.

```typescript
const useNewAlgorithm = featureFlags.get('use-new-algorithm');

export const processData = (data: Data) => {
  if (useNewAlgorithm) {
    return newAlgorithm(data);
  }
  return legacyAlgorithm(data);
};
```

## Data Access Patterns

### Repository Pattern
Implement repositories as the single source of truth for data access, separating business logic from data persistence concerns.

**Why:** Repositories provide a consistent interface for data access, make testing easier through mocking, enable switching data sources without changing business logic, and centralize query optimization and caching strategies. They create a clear boundary between your domain and infrastructure layers.

```typescript
// Standard repository interface
interface Repository<T, CreateDTO, UpdateDTO> {
  create(data: CreateDTO): Promise<T>
  findById(id: string): Promise<T | null>
  findAll(filter?: QueryFilter<T>): Promise<T[]>
  update(id: string, data: UpdateDTO): Promise<T>
  delete(id: string): Promise<boolean>
  exists(id: string): Promise<boolean>
}

// Entity-specific repository implementation
export class UserRepository implements Repository<User, CreateUserDTO, UpdateUserDTO> {
  // Standard CRUD operations
  async create(data: CreateUserDTO): Promise<User> {
    // Implementation with proper error handling
    try {
      const user = await prisma.user.create({ data })
      return this.mapToEntity(user)
    } catch (error) {
      throw new RepositoryError('Failed to create user', error)
    }
  }

  async findById(id: string): Promise<User | null> {
    const user = await prisma.user.findUnique({ where: { id } })
    return user ? this.mapToEntity(user) : null
  }

  // Domain-specific operations
  async findByEmail(email: string): Promise<User | null> {
    const user = await prisma.user.findUnique({ where: { email } })
    return user ? this.mapToEntity(user) : null
  }

  async updateLastLogin(id: string): Promise<void> {
    await prisma.user.update({
      where: { id },
      data: { lastLoginAt: new Date() }
    })
  }

  // Private mapping method to ensure clean domain entities
  private mapToEntity(dbRecord: any): User {
    return {
      id: dbRecord.id,
      email: dbRecord.email,
      name: dbRecord.name,
      // Map database fields to domain model
    }
  }
}
```

### Repository Method Naming Conventions
Use consistent, intuitive names for repository methods that clearly express intent.

**Why:** Consistent naming reduces cognitive load, makes code more discoverable, prevents confusion between similar operations, and creates a predictable API that developers can use without constantly checking documentation.

```typescript
// Standard CRUD operation names
interface StandardOperations {
  create(data: CreateDTO): Promise<Entity>        // Not: add, insert, save
  findById(id: string): Promise<Entity | null>    // Not: get, fetch, load
  findAll(filter?: Filter): Promise<Entity[]>     // Not: list, getAll, fetchAll
  findOne(filter: Filter): Promise<Entity | null> // Not: getOne, fetchOne
  update(id: string, data: UpdateDTO): Promise<Entity> // Not: modify, patch
  delete(id: string): Promise<boolean>            // Not: remove, destroy
  exists(id: string): Promise<boolean>            // Not: has, contains
  count(filter?: Filter): Promise<number>         // Not: size, total
}

// Domain-specific method patterns
interface DomainOperations {
  findBy[Property](value: any): Promise<Entity | null>     // findByEmail, findBySlug
  findAllBy[Property](value: any): Promise<Entity[]>       // findAllByStatus, findAllByUserId
  update[Property](id: string, value: any): Promise<void>  // updateStatus, updateLastLogin
  [action][Entity](data: any): Promise<Result>             // activateUser, archivePost
}
```

### Repository Error Handling
Handle repository errors consistently using custom error classes and Result types.

**Why:** Consistent error handling makes debugging easier, enables proper error recovery strategies, provides meaningful error messages to users, and prevents database errors from leaking implementation details to higher layers.

```typescript
// Custom repository error classes
export class RepositoryError extends Error {
  constructor(
    message: string,
    public readonly cause?: unknown,
    public readonly code?: string
  ) {
    super(message)
    this.name = 'RepositoryError'
  }
}

export class NotFoundError extends RepositoryError {
  constructor(entity: string, id: string) {
    super(`${entity} with id ${id} not found`, undefined, 'NOT_FOUND')
  }
}

export class DuplicateError extends RepositoryError {
  constructor(entity: string, field: string, value: string) {
    super(`${entity} with ${field} '${value}' already exists`, undefined, 'DUPLICATE')
  }
}

// Repository with proper error handling
export class UserRepository {
  async create(data: CreateUserDTO): Promise<Result<User, RepositoryError>> {
    try {
      // Check for duplicates
      const existing = await this.findByEmail(data.email)
      if (existing) {
        return { ok: false, error: new DuplicateError('User', 'email', data.email) }
      }

      const user = await prisma.user.create({ data })
      return { ok: true, value: this.mapToEntity(user) }
    } catch (error) {
      return { ok: false, error: new RepositoryError('Failed to create user', error) }
    }
  }

  async findByIdOrThrow(id: string): Promise<User> {
    const user = await this.findById(id)
    if (!user) {
      throw new NotFoundError('User', id)
    }
    return user
  }
}
```

### Repository Transaction Patterns
Manage database transactions at the repository level for data consistency.

**Why:** Transactions ensure data consistency across multiple operations, prevent partial updates that could corrupt data state, enable proper rollback on failures, and centralize transaction management logic in one place rather than scattered across services.

```typescript
export class OrderRepository {
  // Transaction for complex operations
  async createWithItems(
    orderData: CreateOrderDTO,
    items: CreateOrderItemDTO[]
  ): Promise<Result<Order, RepositoryError>> {
    try {
      const order = await prisma.$transaction(async (tx) => {
        // Create order
        const order = await tx.order.create({ data: orderData })

        // Create order items
        await tx.orderItem.createMany({
          data: items.map(item => ({
            ...item,
            orderId: order.id
          }))
        })

        // Update inventory
        for (const item of items) {
          const updated = await tx.inventory.update({
            where: { productId: item.productId },
            data: { quantity: { decrement: item.quantity } }
          })

          if (updated.quantity < 0) {
            throw new Error(`Insufficient inventory for product ${item.productId}`)
          }
        }

        return order
      })

      return { ok: true, value: this.mapToEntity(order) }
    } catch (error) {
      return { ok: false, error: new RepositoryError('Failed to create order', error) }
    }
  }

  // Provide transaction context for services
  async executeInTransaction<T>(
    callback: (tx: TransactionClient) => Promise<T>
  ): Promise<T> {
    return prisma.$transaction(callback)
  }
}
```

### Testing Repositories
Test repositories using mocks and in-memory implementations.

**Why:** Testing with mocks is faster than database tests, enables testing edge cases that are hard to reproduce with real data, allows parallel test execution without conflicts, and ensures tests focus on business logic rather than database behavior.

```typescript
// Repository interface for easy mocking
export interface IUserRepository {
  create(data: CreateUserDTO): Promise<User>
  findById(id: string): Promise<User | null>
  findByEmail(email: string): Promise<User | null>
}

// In-memory implementation for testing
export class InMemoryUserRepository implements IUserRepository {
  private users: Map<string, User> = new Map()
  private emailIndex: Map<string, string> = new Map()

  async create(data: CreateUserDTO): Promise<User> {
    if (this.emailIndex.has(data.email)) {
      throw new DuplicateError('User', 'email', data.email)
    }

    const user: User = {
      id: crypto.randomUUID(),
      ...data,
      createdAt: new Date()
    }

    this.users.set(user.id, user)
    this.emailIndex.set(user.email, user.id)
    return user
  }

  async findById(id: string): Promise<User | null> {
    return this.users.get(id) ?? null
  }

  async findByEmail(email: string): Promise<User | null> {
    const id = this.emailIndex.get(email)
    return id ? this.users.get(id) ?? null : null
  }

  // Test helper methods
  clear(): void {
    this.users.clear()
    this.emailIndex.clear()
  }

  seed(users: User[]): void {
    users.forEach(user => {
      this.users.set(user.id, user)
      this.emailIndex.set(user.email, user.id)
    })
  }
}

// Testing with repository
describe('UserService', () => {
  let userService: UserService
  let userRepository: InMemoryUserRepository

  beforeEach(() => {
    userRepository = new InMemoryUserRepository()
    userService = new UserService(userRepository)
  })

  test('creates user with unique email', async () => {
    const userData = { email: 'test@example.com', name: 'Test User' }
    const user = await userService.createUser(userData)
    
    expect(user.email).toBe(userData.email)
    expect(await userRepository.findByEmail(userData.email)).toBeDefined()
  })
})
```

