# Dependency Construction Architecture ## The Decision: Manual Construction (Not DI Container) ### Why Not Dependency Injection in RSC? **Problem**: Next.js RSC pages run on every request, but DI containers are singletons. **Risks with DI:** 1. **Data Leakage**: Singleton container could share auth tokens between users 2. **Request Context**: Can't inject request-specific data (user ID, auth token) 3. **Scoping Complexity**: Would need request-scoped containers 4. **Overhead**: DI adds complexity without benefit for RSC **Example of the Problem:** ```typescript // ❌ DON'T DO THIS - DI in RSC export default async function DashboardPage() { const container = ContainerManager.getInstance().getContainer(); const service = container.get(DASHBOARD_SERVICE_TOKEN); // Problem: What if DashboardService needs the user's auth token? // The singleton container doesn't know which user this request is for! } ``` ### The Solution: Manual Construction **Pattern:** ``` RSC Page ↓ PageQuery (constructs Service) ↓ Service (constructs API Client, Logger, ErrorReporter) ↓ API Client (makes HTTP calls) ``` **Example:** ```typescript // ✅ CORRECT: Manual construction in RSC export default async function DashboardPage() { const query = new DashboardPageQuery(); const result = await query.execute(); // ... } // In DashboardPageQuery export class DashboardPageQuery { async execute() { const service = new DashboardService(); // Manual construction return await service.getDashboardOverview(); } } // In DashboardService export class DashboardService { private apiClient: DashboardApiClient; constructor() { // Service creates its own dependencies this.apiClient = new DashboardApiClient( process.env.NEXT_PUBLIC_API_URL || '', new ConsoleErrorReporter(), new ConsoleLogger() ); } async getDashboardOverview() { return await this.apiClient.getOverview(); } } ``` ### Benefits of Manual Construction 1. **Explicit**: Dependencies are clear and visible 2. **Simple**: No magic, no container, no configuration 3. **Safe**: No singleton issues, no data leakage 4. **Testable**: Easy to pass mocks in constructor 5. **Flexible**: Can change dependencies without affecting callers ### What About the Existing `lib/di/`? The project has an Inversify DI system, but it's designed for: **Client Components:** ```typescript // ✅ OK in client components export function UserDashboard() { const service = useInject(DASHBOARD_SERVICE_TOKEN); // ... } ``` **Testing:** ```typescript // ✅ OK in tests const mockApiClient = new MockDashboardApiClient(); const service = new DashboardService(mockApiClient); ``` **RSC (PageQueries/Mutations):** ```typescript // ❌ DON'T use DI container in RSC // ✅ DO use manual construction ``` ### ESLint Enforcement The `services-no-instantiation` rule is **removed** because it was wrong. **Correct Rules:** - ✅ `clean-error-handling`: PageQueries must use Services, not API Clients - ✅ `services-implement-contract`: Services must return Result types - ✅ `lib-no-next-imports`: No Next.js imports in lib/ directory **What the Rules Allow:** ```typescript // In PageQuery - ALLOWED const service = new DashboardService(); // In Service - ALLOWED this.apiClient = new DashboardApiClient(...); this.logger = new ConsoleLogger(); // In PageQuery - FORBIDDEN const apiClient = new DashboardApiClient(...); // Use Service instead! ``` ### Complete Example: Read Flow ```typescript // apps/website/app/dashboard/page.tsx export default async function DashboardPage() { const query = new DashboardPageQuery(); const result = await query.execute(); if (result.isErr()) { // Handle presentation errors return ; } return ; } // apps/website/lib/page-queries/page-queries/DashboardPageQuery.ts export class DashboardPageQuery implements PageQuery { async execute(): Promise> { const service = new DashboardService(); const result = await service.getDashboardOverview(); if (result.isErr()) { // Map domain error to presentation error return Result.err(mapToPresentationError(result.error)); } const viewData = DashboardViewDataBuilder.build(result.value); return Result.ok(viewData); } } // apps/website/lib/services/analytics/DashboardService.ts export class DashboardService { private apiClient: DashboardApiClient; constructor() { this.apiClient = new DashboardApiClient( process.env.NEXT_PUBLIC_API_URL || '', new ConsoleErrorReporter(), new ConsoleLogger() ); } async getDashboardOverview(): Promise> { try { const data = await this.apiClient.getOverview(); return Result.ok(data); } catch (error) { // Convert HTTP errors to domain errors if (error instanceof HttpNotFoundError) { return Result.err(new NotFoundError('Dashboard not found')); } return Result.err(new UnknownError('Failed to fetch dashboard')); } } } // apps/website/lib/api/dashboard/DashboardApiClient.ts export class DashboardApiClient { constructor( private baseUrl: string, private errorReporter: ErrorReporter, private logger: Logger ) {} async getOverview(): Promise { const response = await fetch(`${this.baseUrl}/dashboard/overview`); if (!response.ok) { if (response.status === 404) { throw new HttpNotFoundError('Dashboard not found'); } throw new HttpError(`HTTP ${response.status}`); } return response.json(); } } ``` ### Complete Example: Write Flow ```typescript // apps/website/app/actions/userActions.ts 'use server'; export async function updateUserStatus(input: UpdateUserStatusInput) { const mutation = new UpdateUserStatusMutation(); const result = await mutation.execute(input); if (result.isErr()) { return { success: false, error: result.error }; } revalidatePath('/admin/users'); return { success: true }; } // apps/website/lib/mutations/UpdateUserStatusMutation.ts export class UpdateUserStatusMutation implements Mutation { async execute(input: UpdateUserStatusInput): Promise> { const service = new UserService(); const result = await service.updateUserStatus(input.userId, input.status); if (result.isErr()) { return Result.err(mapToMutationError(result.error)); } return Result.ok(undefined); } } // apps/website/lib/services/user/UserService.ts export class UserService { private apiClient: UserApiClient; constructor() { this.apiClient = new UserApiClient( process.env.NEXT_PUBLIC_API_URL || '', new ConsoleErrorReporter(), new ConsoleLogger() ); } async updateUserStatus(userId: string, status: string): Promise> { try { await this.apiClient.updateUserStatus(userId, status); return Result.ok(undefined); } catch (error) { if (error instanceof HttpForbiddenError) { return Result.err(new ForbiddenError('Insufficient permissions')); } return Result.err(new UnknownError('Failed to update user')); } } } ``` ### Summary | Aspect | RSC (PageQueries/Mutations) | Client Components | |--------|----------------------------|-------------------| | **Construction** | Manual (`new Service()`) | Manual or DI hooks | | **DI Container** | ❌ Never use | ✅ Can use | | **Dependencies** | Service creates its own | Injected or manual | | **Testability** | Pass mocks to constructor | Use DI mocks | | **Complexity** | Low | Medium | **Golden Rule**: In RSC, always use manual construction. It's simpler, safer, and more explicit.