# ADR-039: Dependency Injection Pattern

**Date**: 2026-01-09

**Status**: Accepted

**Implemented**: 2026-01-09

## Context

As the application grows, tightly coupled components become difficult to test and maintain. Common issues include:

1. **Hard-to-Test Code**: Components that instantiate their own dependencies cannot be easily unit tested with mocks.
2. **Rigid Architecture**: Changing one implementation requires modifying all consumers.
3. **Hidden Dependencies**: It's unclear what a component needs to function.
4. **Circular Dependencies**: Tight coupling can lead to circular import issues.

Dependency Injection (DI) addresses these issues by inverting the control of dependency creation.

## Decision

We will adopt a constructor-based dependency injection pattern for all services and repositories. This approach:

1. **Explicit Dependencies**: All dependencies are declared in the constructor.
2. **Default Values**: Production dependencies have sensible defaults.
3. **Testability**: Test code can inject mocks without modifying source code.
4. **Loose Coupling**: Components depend on interfaces, not implementations.

### Design Principles

- **Constructor Injection**: Dependencies are passed through constructors, not looked up globally.
- **Default Production Dependencies**: Use default parameter values for production instances.
- **Interface Segregation**: Depend on the minimal interface needed (e.g., `Pick<Pool, 'query'>`).
- **Composition Root**: Wire dependencies at the application entry point.

## Implementation Details

### Repository Pattern with DI

Located in `src/services/db/flyer.db.ts`:

```typescript
import { Pool, PoolClient } from 'pg';
import { getPool } from './connection.db';

export class FlyerRepository {
  // Accept any object with a 'query' method - Pool or PoolClient
  private db: Pick<Pool | PoolClient, 'query'>;

  constructor(db: Pick<Pool | PoolClient, 'query'> = getPool()) {
    this.db = db;
  }

  async getFlyerById(flyerId: number, logger: Logger): Promise<Flyer> {
    const result = await this.db.query<Flyer>('SELECT * FROM flyers WHERE flyer_id = $1', [
      flyerId,
    ]);
    if (result.rows.length === 0) {
      throw new NotFoundError(`Flyer with ID ${flyerId} not found.`);
    }
    return result.rows[0];
  }

  async insertFlyer(flyer: FlyerDbInsert, logger: Logger): Promise<Flyer> {
    // Implementation
  }
}
```

**Usage in Production**:

```typescript
// Uses default pool
const flyerRepo = new FlyerRepository();
```

**Usage in Tests**:

```typescript
const mockDb = {
  query: vi.fn().mockResolvedValue({ rows: [mockFlyer] }),
};
const flyerRepo = new FlyerRepository(mockDb);
```

**Usage in Transactions**:

```typescript
import { withTransaction } from './connection.db';

await withTransaction(async (client) => {
  // Pass transactional client to repository
  const flyerRepo = new FlyerRepository(client);
  const flyer = await flyerRepo.insertFlyer(flyerData, logger);
  // ... more operations in the same transaction
});
```

### Service Layer with DI

Located in `src/services/backgroundJobService.ts`:

```typescript
export class BackgroundJobService {
  constructor(
    private personalizationRepo: PersonalizationRepository,
    private notificationRepo: NotificationRepository,
    private emailQueue: Queue<EmailJobData>,
    private logger: Logger,
  ) {}

  async runDailyDealCheck(): Promise<void> {
    this.logger.info('[BackgroundJob] Starting daily deal check...');

    const deals = await this.personalizationRepo.getBestSalePricesForAllUsers(this.logger);
    // ... process deals
  }
}

// Composition root - wire production dependencies
import { personalizationRepo, notificationRepo } from './db/index.db';
import { logger } from './logger.server';
import { emailQueue } from './queueService.server';

export const backgroundJobService = new BackgroundJobService(
  personalizationRepo,
  notificationRepo,
  emailQueue,
  logger,
);
```

**Testing with Mocks**:

```typescript
describe('BackgroundJobService', () => {
  it('should process deals for all users', async () => {
    const mockPersonalizationRepo = {
      getBestSalePricesForAllUsers: vi.fn().mockResolvedValue([mockDeal]),
    };
    const mockNotificationRepo = {
      createBulkNotifications: vi.fn().mockResolvedValue([]),
    };
    const mockEmailQueue = {
      add: vi.fn().mockResolvedValue({ id: 'job-1' }),
    };
    const mockLogger = {
      info: vi.fn(),
      error: vi.fn(),
    };

    const service = new BackgroundJobService(
      mockPersonalizationRepo as any,
      mockNotificationRepo as any,
      mockEmailQueue as any,
      mockLogger as any,
    );

    await service.runDailyDealCheck();

    expect(mockPersonalizationRepo.getBestSalePricesForAllUsers).toHaveBeenCalled();
    expect(mockEmailQueue.add).toHaveBeenCalled();
  });
});
```

### Processing Service with DI

Located in `src/services/flyer/flyerProcessingService.ts`:

```typescript
export class FlyerProcessingService {
  constructor(
    private fileHandler: FlyerFileHandler,
    private aiProcessor: FlyerAiProcessor,
    private fsAdapter: FileSystemAdapter,
    private cleanupQueue: Queue<CleanupJobData>,
    private dataTransformer: FlyerDataTransformer,
    private persistenceService: FlyerPersistenceService,
  ) {}

  async processFlyer(filePath: string, logger: Logger): Promise<ProcessedFlyer> {
    // Use injected dependencies
    const fileInfo = await this.fileHandler.extractMetadata(filePath);
    const aiResult = await this.aiProcessor.analyze(filePath, logger);
    const transformed = this.dataTransformer.transform(aiResult);
    const saved = await this.persistenceService.save(transformed, logger);

    // Queue cleanup
    await this.cleanupQueue.add('cleanup', { filePath });

    return saved;
  }
}

// Composition root
const flyerProcessingService = new FlyerProcessingService(
  new FlyerFileHandler(fsAdapter, execAsync),
  new FlyerAiProcessor(aiService, db.personalizationRepo),
  fsAdapter,
  cleanupQueue,
  new FlyerDataTransformer(),
  new FlyerPersistenceService(),
);
```

### Interface Segregation

Use the minimum interface required:

```typescript
// Bad - depends on full Pool
constructor(pool: Pool) {}

// Good - depends only on what's needed
constructor(db: Pick<Pool | PoolClient, 'query'>) {}
```

This allows injecting either a `Pool`, `PoolClient` (for transactions), or a mock object with just a `query` method.

### Composition Root Pattern

Wire all dependencies at application startup:

```typescript
// src/services/db/index.db.ts - Composition root for repositories
import { getPool } from './connection.db';

export const userRepo = new UserRepository(getPool());
export const flyerRepo = new FlyerRepository(getPool());
export const adminRepo = new AdminRepository(getPool());
export const personalizationRepo = new PersonalizationRepository(getPool());
export const notificationRepo = new NotificationRepository(getPool());

export const db = {
  userRepo,
  flyerRepo,
  adminRepo,
  personalizationRepo,
  notificationRepo,
};
```

## Consequences

### Positive

- **Testability**: Unit tests can inject mocks without modifying production code.
- **Flexibility**: Swap implementations (e.g., different database adapters) easily.
- **Explicit Dependencies**: Clear contract of what a component needs.
- **Transaction Support**: Repositories can participate in transactions by accepting a client.

### Negative

- **More Boilerplate**: Constructors become longer with many dependencies.
- **Composition Complexity**: Must wire dependencies somewhere (composition root).
- **No Runtime Type Checking**: TypeScript types are erased at runtime.

### Mitigation

For complex services with many dependencies, consider:

1. **Factory Functions**: Encapsulate construction logic.
2. **Dependency Groups**: Pass related dependencies as a single object.
3. **DI Containers**: For very large applications, consider a DI library like `tsyringe` or `inversify`.

## Key Files

- `src/services/db/*.db.ts` - Repository classes with constructor DI
- `src/services/db/index.db.ts` - Composition root for repositories
- `src/services/backgroundJobService.ts` - Service class with constructor DI
- `src/services/flyer/flyerProcessingService.ts` - Complex service with multiple dependencies

## Related ADRs

- [ADR-002](./0002-standardized-transaction-management.md) - Transaction Management
- [ADR-034](./0034-repository-pattern-standards.md) - Repository Pattern Standards
- [ADR-035](./0035-service-layer-architecture.md) - Service Layer Architecture
