# ADR-035: Service Layer Architecture

**Date**: 2026-01-09

**Status**: Accepted

**Implemented**: 2026-01-09

## Context

The application has evolved to include multiple service types:

1. **Repository services** (`*.db.ts`): Direct database access
2. **Business services** (`*Service.ts`): Business logic orchestration
3. **External services** (`*Service.server.ts`): Integration with external APIs
4. **Infrastructure services** (`logger`, `redis`, `queues`): Cross-cutting concerns

Without clear boundaries, business logic can leak into routes, repositories can contain business rules, and services can become tightly coupled.

## Decision

We will establish a clear layered architecture with defined responsibilities for each layer:

### Layer Responsibilities

```
┌─────────────────────────────────────────────────────────────────┐
│                        Routes Layer                              │
│  - Request/response handling                                     │
│  - Input validation (via middleware)                             │
│  - Authentication/authorization                                  │
│  - Rate limiting                                                 │
│  - Response formatting                                           │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                       Services Layer                             │
│  - Business logic orchestration                                  │
│  - Transaction coordination                                      │
│  - External API integration                                      │
│  - Cross-repository operations                                   │
│  - Event publishing                                              │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Repository Layer                            │
│  - Direct database access                                        │
│  - Query construction                                            │
│  - Entity mapping                                                │
│  - Error translation                                             │
└─────────────────────────────────────────────────────────────────┘
```

### Service Types and Naming

| Type                | Pattern                         | Suffix        | Example               |
| ------------------- | ------------------------------- | ------------- | --------------------- |
| Business Service    | Orchestrates business logic     | `*Service.ts` | `authService.ts`      |
| Server-Only Service | External APIs, server-side only | `*.server.ts` | `aiService.server.ts` |
| Database Repository | Direct DB access                | `*.db.ts`     | `user.db.ts`          |
| Infrastructure      | Cross-cutting concerns          | Descriptive   | `logger.server.ts`    |

### Service Dependencies

```
Routes → Business Services → Repositories
            ↓
       External Services
            ↓
      Infrastructure (logger, redis, queues)
```

**Rules**:

- Routes MUST NOT directly access repositories (except simple CRUD)
- Repositories MUST NOT call other repositories (use services)
- Services MAY call other services
- Infrastructure services MAY be called from any layer

## Implementation Details

### Business Service Pattern

```typescript
// src/services/authService.ts
import { withTransaction } from './db/connection.db';
import * as userRepo from './db/user.db';
import * as profileRepo from './db/personalization.db';
import { emailService } from './emailService.server';
import { logger } from './logger.server';

const log = logger.child({ service: 'auth' });

interface LoginResult {
  user: UserProfile;
  accessToken: string;
  refreshToken: string;
}

export const authService = {
  /**
   * Registers a new user and sends welcome email.
   * Orchestrates multiple repositories in a transaction.
   */
  async registerAndLoginUser(
    email: string,
    password: string,
    fullName?: string,
    avatarUrl?: string,
    reqLog?: Logger,
  ): Promise<LoginResult> {
    const log = reqLog || logger;

    return withTransaction(async (client) => {
      // 1. Create user (repository)
      const user = await userRepo.createUser({ email, password }, client);

      // 2. Create profile (repository)
      await profileRepo.createProfile(
        {
          userId: user.user_id,
          fullName,
          avatarUrl,
        },
        client,
      );

      // 3. Generate tokens (business logic)
      const { accessToken, refreshToken } = this.generateTokens(user);

      // 4. Send welcome email (external service, non-blocking)
      emailService.sendWelcomeEmail(email, fullName).catch((err) => {
        log.warn({ err, email }, 'Failed to send welcome email');
      });

      log.info({ userId: user.user_id }, 'User registered successfully');

      return {
        user: await this.buildUserProfile(user.user_id, client),
        accessToken,
        refreshToken,
      };
    });
  },

  // ... other methods
};
```

### Server-Only Service Pattern

```typescript
// src/services/aiService.server.ts
// This file MUST only be imported by server-side code

import { GenAI } from '@google/genai';
import { config } from '../config/env';
import { logger } from './logger.server';

const log = logger.child({ service: 'ai' });

class AiService {
  private client: GenAI;

  constructor() {
    this.client = new GenAI({ apiKey: config.ai.geminiApiKey });
  }

  async analyzeImage(imagePath: string): Promise<AnalysisResult> {
    log.info({ imagePath }, 'Starting image analysis');
    // ... implementation
  }
}

export const aiService = new AiService();
```

### Route Handler Pattern

```typescript
// src/routes/auth.routes.ts
import { Router } from 'express';
import { validateRequest } from '../middleware/validation.middleware';
import { loginLimiter } from '../config/rateLimiters';
import { authService } from '../services/authService';

const router = Router();

// Route is thin - delegates to service
router.post(
  '/register',
  registerLimiter,
  validateRequest(registerSchema),
  async (req, res, next) => {
    try {
      const { email, password, full_name } = req.body;

      // Delegate to service
      const result = await authService.registerAndLoginUser(
        email,
        password,
        full_name,
        undefined,
        req.log, // Pass request-scoped logger
      );

      // Format response
      res.status(201).json({
        message: 'Registration successful',
        user: result.user,
        accessToken: result.accessToken,
      });
    } catch (error) {
      next(error); // Let error handler deal with it
    }
  },
);
```

### Service File Organization

```
src/services/
├── db/                           # Repository layer
│   ├── connection.db.ts          # Pool, transactions
│   ├── errors.db.ts              # DB error types
│   ├── user.db.ts                # User repository
│   ├── flyer.db.ts               # Flyer repository
│   └── index.db.ts               # Barrel exports
├── authService.ts                # Authentication business logic
├── userService.ts                # User management business logic
├── gamificationService.ts        # Gamification business logic
├── aiService.server.ts           # AI API integration (server-only)
├── emailService.server.ts        # Email sending (server-only)
├── geocodingService.server.ts    # Geocoding API (server-only)
├── cacheService.server.ts        # Redis caching (server-only)
├── queueService.server.ts        # BullMQ queues (server-only)
├── logger.server.ts              # Pino logger (server-only)
└── logger.client.ts              # Client-side logger
```

### Dependency Injection for Testing

Services should support dependency injection for easier testing:

```typescript
// Production: use singleton
export const authService = createAuthService();

// Testing: inject mocks
export function createAuthService(deps?: Partial<AuthServiceDeps>) {
  const userRepo = deps?.userRepo || defaultUserRepo;
  const emailService = deps?.emailService || defaultEmailService;

  return {
    async registerAndLoginUser(...) { /* ... */ },
  };
}
```

## Key Files

### Infrastructure Services

- `src/services/logger.server.ts` - Server-side structured logging
- `src/services/logger.client.ts` - Client-side logging
- `src/services/redis.server.ts` - Redis connection management
- `src/services/queueService.server.ts` - BullMQ queue management
- `src/services/cacheService.server.ts` - Caching abstraction

### Business Services

- `src/services/authService.ts` - Authentication flows
- `src/services/userService.ts` - User management
- `src/services/gamificationService.ts` - Achievements, leaderboards
- `src/services/flyerProcessingService.server.ts` - Flyer pipeline

### External Integration Services

- `src/services/aiService.server.ts` - Gemini AI integration
- `src/services/emailService.server.ts` - Email sending
- `src/services/geocodingService.server.ts` - Address geocoding

## Consequences

### Positive

- **Separation of Concerns**: Clear boundaries between layers
- **Testability**: Services can be tested in isolation with mocked dependencies
- **Reusability**: Business logic in services can be used by multiple routes
- **Maintainability**: Changes to one layer don't ripple through others
- **Transaction Safety**: Services coordinate transactions across repositories

### Negative

- **Indirection**: More layers mean more code to navigate
- **Potential Over-Engineering**: Simple CRUD operations don't need full service layer
- **Coordination Overhead**: Team must agree on layer boundaries

## Guidelines

### When to Create a Service

Create a business service when:

- Logic spans multiple repositories
- External APIs need to be called
- Complex business rules exist
- The same logic is needed by multiple routes
- Transaction coordination is required

### When Direct Repository Access is OK

Routes can directly use repositories for:

- Simple single-entity CRUD operations
- Read-only queries with no business logic
- Operations that don't need transaction coordination

### Service Method Guidelines

- Accept a request-scoped logger as an optional parameter
- Return domain objects, not HTTP-specific responses
- Throw domain errors, let routes handle HTTP status codes
- Use `withTransaction` for multi-repository operations
- Log business events (user registered, order placed, etc.)
