Dependency injection decouples modules and makes unit testing possible without spinning up a database. Claude Code designs complete DI patterns — interfaces, repositories, and mock implementations — when you define the rules.
CLAUDE.md for Dependency Injection
## Dependency Injection Rules
### Core approach
- Constructor injection as the default
- DI containers: optional (small projects don't need them)
- Services never instantiate dependencies internally
### Interface design
- All external dependencies abstracted behind interfaces
Examples: IEmailService, IUserRepository
- Tests swap in mock implementations
- Interfaces live in src/interfaces/
### Naming conventions
- Interfaces: I{Name} (IUserRepository)
- Implementations: {Name} (UserRepository)
- Test mocks: Mock{Name} (MockUserRepository)
### Testing
- Unit tests don't use real DBs (mock the Repository)
- DI-based code tests well with vitest + vi.fn()
### Prohibited
- new keyword to instantiate dependencies inside service classes
- Direct Prisma calls inside service classes (use Repository layer)
Generating the DI Design
Generate a DI design for UserService (no DI container needed).
Requirements:
- Define IUserRepository interface
- UserRepository implementation (Prisma)
- MockUserRepository for tests
- UserService depends on IUserRepository (not concrete class)
- IEmailService for email sending
Generate:
- src/interfaces/IUserRepository.ts
- src/interfaces/IEmailService.ts
- src/repositories/UserRepository.ts
- src/services/UserService.ts
Generated Interfaces
// src/interfaces/IUserRepository.ts
export interface IUserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
create(data: CreateUserData): Promise<User>;
update(id: string, data: Partial<UpdateUserData>): Promise<User>;
delete(id: string): Promise<void>;
}
// src/interfaces/IEmailService.ts
export interface IEmailService {
sendWelcomeEmail(user: User): Promise<void>;
sendPasswordResetEmail(user: User, token: string): Promise<void>;
}
Generated Repository
// src/repositories/UserRepository.ts
import { IUserRepository } from '../interfaces/IUserRepository';
import { prisma } from '../lib/prisma';
export class UserRepository implements IUserRepository {
async findById(id: string): Promise<User | null> {
return prisma.user.findUnique({ where: { id } });
}
async findByEmail(email: string): Promise<User | null> {
return prisma.user.findUnique({ where: { email } });
}
async create(data: CreateUserData): Promise<User> {
return prisma.user.create({ data });
}
async update(id: string, data: Partial<UpdateUserData>): Promise<User> {
return prisma.user.update({ where: { id }, data });
}
async delete(id: string): Promise<void> {
await prisma.user.delete({ where: { id } });
}
}
Generated Service with DI
// src/services/UserService.ts — depends on interfaces, not concrete classes
import { IUserRepository } from '../interfaces/IUserRepository';
import { IEmailService } from '../interfaces/IEmailService';
export class UserService {
constructor(
private readonly userRepository: IUserRepository, // injected
private readonly emailService: IEmailService // injected
) {}
async createUser(data: CreateUserData): Promise<User> {
const existing = await this.userRepository.findByEmail(data.email);
if (existing) throw new ConflictError('Email already in use');
const user = await this.userRepository.create(data);
await this.emailService.sendWelcomeEmail(user);
return user;
}
async getUserById(id: string): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) throw new NotFoundError(`User not found: ${id}`);
return user;
}
}
Unit Tests Using DI
Generate unit tests for UserService.
Requirements:
- Mock IUserRepository (no DB)
- Mock IEmailService (no actual emails)
- Use vitest vi.fn() for mock functions
- Test cases: successful creation, duplicate email error
Save to: src/services/__tests__/UserService.test.ts
Generated:
// src/services/__tests__/UserService.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from '../UserService';
import { IUserRepository } from '../../interfaces/IUserRepository';
import { IEmailService } from '../../interfaces/IEmailService';
describe('UserService', () => {
let userService: UserService;
let mockUserRepository: IUserRepository;
let mockEmailService: IEmailService;
beforeEach(() => {
mockUserRepository = {
findById: vi.fn(),
findByEmail: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
};
mockEmailService = {
sendWelcomeEmail: vi.fn(),
sendPasswordResetEmail: vi.fn(),
};
// Inject mock dependencies — no DB, no real email
userService = new UserService(mockUserRepository, mockEmailService);
});
it('creates user and sends welcome email', async () => {
const data = { email: 'new@example.com', name: 'Test User' };
const created = { id: '123', ...data, createdAt: new Date() };
vi.mocked(mockUserRepository.findByEmail).mockResolvedValue(null);
vi.mocked(mockUserRepository.create).mockResolvedValue(created);
vi.mocked(mockEmailService.sendWelcomeEmail).mockResolvedValue(undefined);
const result = await userService.createUser(data);
expect(result).toEqual(created);
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(created);
expect(mockUserRepository.create).toHaveBeenCalledWith(data);
});
it('throws ConflictError when email already exists', async () => {
vi.mocked(mockUserRepository.findByEmail).mockResolvedValue({
id: 'existing', email: 'existing@example.com', name: 'Existing',
});
await expect(
userService.createUser({ email: 'existing@example.com', name: 'New' })
).rejects.toThrow('Email already in use');
// No create or email should happen
expect(mockUserRepository.create).not.toHaveBeenCalled();
expect(mockEmailService.sendWelcomeEmail).not.toHaveBeenCalled();
});
});
Summary
Design dependency injection with Claude Code:
-
CLAUDE.md — Interface-first, constructor injection, no internal
new - Interfaces first — Define the contract before implementing
- Services depend on interfaces — Not concrete Prisma/Redis classes
- Unit tests use mocks — Fast, no DB, no external services
Code Review Pack (¥980) includes /code-review for DI pattern review — tight coupling, direct DB access in services, untestable designs.
Myouga (@myougatheaxo) — Claude Code engineer focused on clean architecture.
Top comments (0)