Architecture Patterns

!!! info “Guideline Metadata”

**Version**: 1.0.0
**Last Modified**: 2025-01-19T00:00:00Z
**Category**: Core Guidelines
**Priority**: Critical

Standardized architectural patterns and implementation guidelines for all projects.

Three-Layer Architecture

The fundamental architecture pattern used across all projects.

Layer Overview

graph TB
    subgraph "External World"
        HTTP[HTTP Requests]
        DB[(Database)]
        API[External APIs]
        FILES[File System]
    end

    subgraph "Application Layers"
        subgraph "IO Layer"
            CONTROLLERS[Controllers/Routes]
            VALIDATION[Input Validation]
            SERIALIZATION[Response Serialization]
        end

        subgraph "Management Layer"
            MANAGERS[Managers]
            ORCHESTRATION[Business Logic]
            COORDINATION[Provider Coordination]
        end

        subgraph "Provider Layer"
            DB_PROVIDER[Database Provider]
            API_PROVIDER[API Provider]
            FILE_PROVIDER[File Provider]
        end

        subgraph "Side Layer"
            MODELS[Models]
            MAPPERS[Mappers]
            HELPERS[Helpers]
            CONSTANTS[Constants/Enums]
        end
    end

    HTTP --> CONTROLLERS
    CONTROLLERS --> MANAGERS
    MANAGERS --> DB_PROVIDER
    MANAGERS --> API_PROVIDER
    MANAGERS --> FILE_PROVIDER
    DB_PROVIDER --> DB
    API_PROVIDER --> API
    FILE_PROVIDER --> FILES

    MANAGERS -.-> MODELS
    MANAGERS -.-> MAPPERS
    MANAGERS -.-> HELPERS

    classDef ioLayer fill:#e1f5fe
    classDef managementLayer fill:#f3e5f5
    classDef providerLayer fill:#e8f5e8
    classDef sideLayer fill:#fff3e0

    class CONTROLLERS,VALIDATION,SERIALIZATION ioLayer
    class MANAGERS,ORCHESTRATION,COORDINATION managementLayer
    class DB_PROVIDER,API_PROVIDER,FILE_PROVIDER providerLayer
    class MODELS,MAPPERS,HELPERS,CONSTANTS sideLayer

Layer Responsibilities

IO Layer

  • Purpose: Handle external communication
  • Responsibilities:
    • Receive and validate input
    • Format and send responses
    • Handle authentication/authorization
    • Rate limiting and request logging
// Example: API Controller
export class UserController {
    constructor(private userManager: UserManager) {}

    async getUser(req: Request, res: Response) {
        const { id } = this.validateGetUserRequest(req);
        const result = await this.userManager.getUserById(id);

        if (!result.success) {
            return res.status(404).json({ error: result.error });
        }

        res.json(this.formatUserResponse(result.data));
    }

    private validateGetUserRequest(req: Request): { id: string } {
        // Input validation logic
    }

    private formatUserResponse(user: User): UserResponse {
        // Response formatting logic
    }
}

Management Layer

  • Purpose: Orchestrate business logic (ONLY when adds value)
  • When to use:
    • Coordinating multiple providers
    • Complex business rules
    • Transaction management
    • Multi-step operations
  • When NOT to use:
    • Simple passthrough to single provider
    • No business logic added
    • Direct CRUD operations (IO can call provider directly)
// Example: User Manager
export class UserManager {
    constructor(
        private userProvider: UserProvider,
        private emailProvider: EmailProvider,
        private auditProvider: AuditProvider
    ) {}

    async createUser(userData: CreateUserRequest): Promise<Result<User>> {
        try {
            // Validate business rules
            const validation = this.validateUserData(userData);
            if (!validation.success) {
                return { success: false, error: validation.error };
            }

            // Coordinate multiple providers
            const user = await this.userProvider.createUser(userData);
            await this.emailProvider.sendWelcomeEmail(user.email);
            await this.auditProvider.logUserCreation(user.id);

            return { success: true, data: user };
        } catch (error) {
            return { success: false, error: error.message };
        }
    }
}

Provider Layer

  • Purpose: Perform specific tasks
  • Responsibilities:
    • Database operations
    • External API calls
    • File system operations
    • Cache management
// Example: Database Provider
export class UserProvider {
    constructor(private db: Database) {}

    async getUserById(id: string): Promise<User | null> {
        const row = await this.db.query(
            'SELECT * FROM users WHERE id = $1',
            [id]
        );

        return row ? this.mapRowToUser(row) : null;
    }

    async createUser(userData: CreateUserData): Promise<User> {
        const row = await this.db.query(
            'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *',
            [userData.email, userData.name]
        );

        return this.mapRowToUser(row);
    }

    private mapRowToUser(row: DatabaseRow): User {
        return {
            id: row.id,
            email: row.email,
            name: row.name,
            createdAt: row.created_at
        };
    }
}

Side Layer

  • Purpose: Support all layers with shared utilities
  • Components:
    • Models (data structures)
    • Mappers (data transformation)
    • Helpers (utility functions)
    • Constants and enums
// Models
export interface User {
    id: string;
    email: string;
    name: string;
    createdAt: Date;
}

// Mappers
export class UserMapper {
    static toDto(user: User): UserDto {
        return {
            id: user.id,
            email: user.email,
            displayName: user.name
        };
    }
}

// Helpers
export class DateHelper {
    static formatDate(date: Date): string {
        return date.toISOString().split('T')[0];
    }
}

// Constants
export const USER_CONSTANTS = {
    MAX_NAME_LENGTH: 100,
    MIN_PASSWORD_LENGTH: 8
} as const;

Critical Architecture Rules

Rule 1: Management Layer Only When Adds Value

// ❌ WRONG: Manager just wrapping single provider
export class UserManager {
    async getUserById(id: string): Promise<Result<User>> {
        return this.userProvider.getUserById(id); // No value added!
    }
}

// ✅ CORRECT: IO calls provider directly
export class UserController {
    async getUser(req: Request, res: Response) {
        const user = await this.userProvider.getUserById(req.params.id);
        res.json(user);
    }
}

// ✅ CORRECT: Manager when coordinating multiple providers
export class UserManager {
    async createUser(userData: CreateUserRequest): Promise<Result<User>> {
        const user = await this.userProvider.createUser(userData);
        await this.emailProvider.sendWelcomeEmail(user.email);
        await this.auditProvider.logUserCreation(user.id);
        return { success: true, data: user };
    }
}

Rule 2: No Provider-to-Provider Communication

// ❌ WRONG: Providers calling each other
export class UserProvider {
    constructor(private orderProvider: OrderProvider) {} // NO!

    async deleteUser(id: string) {
        await this.orderProvider.deleteUserOrders(id); // NO!
    }
}

// ✅ CORRECT: Manager orchestrates
export class UserManager {
    constructor(
        private userProvider: UserProvider,
        private orderProvider: OrderProvider
    ) {}

    async deleteUser(id: string) {
        await this.orderProvider.deleteUserOrders(id);
        await this.userProvider.deleteUser(id);
    }
}

Rule 3: “Let It Crash” Philosophy

Following Erlang’s philosophy - fail fast and loud:

// ✅ CORRECT: Trust input from upper layers
export class UserProvider {
    async createUser(userData: CreateUserData): Promise<User> {
        // No re-validation - trust management layer validated
        const result = await this.db.query(
            'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *',
            [userData.email, userData.name]
        );
        return result.rows[0];
    }
}

// ❌ WRONG: Re-validating already validated data
export class UserProvider {
    async createUser(userData: CreateUserData): Promise<User> {
        // Unnecessary - already validated in upper layer!
        if (!userData.email || !isValidEmail(userData.email)) {
            throw new Error('Invalid email');
        }
        // ...
    }
}

// ✅ CORRECT: Pass external data as-is
export class GithubProvider {
    async getUser(username: string) {
        const response = await fetch(`https://api.github.com/users/${username}`);
        return response.json(); // Return as-is, no unnecessary mapping
    }
}

// ❌ WRONG: Creating unnecessary models
export class GithubProvider {
    async getUser(username: string) {
        const response = await fetch(`https://api.github.com/users/${username}`);
        const data = await response.json();
        // Unnecessary transformation!
        return new GithubUserModel(data);
    }
}

Rule 4: Minimal Data Transformation

// ❌ WRONG: Returning raw database rows
async getUserById(id: string): Promise<DatabaseRow> {
    return this.db.query('SELECT * FROM users WHERE id = $1', [id]);
}

// ✅ CORRECT: Returning domain models
async getUserById(id: string): Promise<User | null> {
    const row = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
    return row ? this.mapRowToUser(row) : null;
}

Rule 5: Error Handling at Layer Boundaries

// Providers: Return null/throw specific errors
export class UserProvider {
    async getUserById(id: string): Promise<User | null> {
        try {
            const row = await this.db.query(/* ... */);
            return row ? this.mapRowToUser(row) : null;
        } catch (error) {
            throw new DatabaseError('Failed to fetch user', error);
        }
    }
}

// Managers: Return Result objects
export class UserManager {
    async getUserById(id: string): Promise<Result<User>> {
        try {
            const user = await this.userProvider.getUserById(id);
            return user
                ? { success: true, data: user }
                : { success: false, error: 'User not found' };
        } catch (error) {
            return { success: false, error: error.message };
        }
    }
}

// IO Layer: Convert to HTTP responses
export class UserController {
    async getUser(req: Request, res: Response) {
        const result = await this.userManager.getUserById(req.params.id);

        if (!result.success) {
            return res.status(404).json({ error: result.error });
        }

        res.json(result.data);
    }
}

Common Patterns

Result Pattern

export interface Result<T> {
    success: boolean;
    data?: T;
    error?: string;
}

// Usage
const result = await userManager.createUser(userData);
if (result.success) {
    // Handle success
    console.log(result.data);
} else {
    // Handle error
    console.error(result.error);
}

Repository Pattern (for Providers)

export interface UserProvider {
    getUserById(id: string): Promise<User | null>;
    createUser(userData: CreateUserData): Promise<User>;
    updateUser(id: string, changes: Partial<User>): Promise<User>;
    deleteUser(id: string): Promise<void>;
}

export class DatabaseUserProvider implements UserProvider {
    // Implementation
}

export class MockUserProvider implements UserProvider {
    // Test implementation
}

Factory Pattern (for Complex Creation)

export class ServiceFactory {
    static createUserService(config: Config): UserManager {
        const userProvider = new DatabaseUserProvider(config.database);
        const emailProvider = new EmailProvider(config.email);
        const auditProvider = new AuditProvider(config.audit);

        return new UserManager(userProvider, emailProvider, auditProvider);
    }
}

Project Structure Template

Feature-Based Folder Structure (Recommended)

The primary organizational pattern groups all related files by domain/feature:

src/
├── features/                    # Domain-specific modules
│   ├── users/                  # User feature - all layers together
│   │   ├── user-controller.ts  # IO Layer
│   │   ├── user-routes.ts      # IO Layer
│   │   ├── user-manager.ts     # Management Layer (if needed)
│   │   ├── user-provider.ts    # Provider Layer
│   │   ├── user-model.ts       # Domain model (if needed)
│   │   ├── user-types.ts       # TypeScript types
│   │   └── user.test.ts        # Tests
│   │
│   ├── orders/                 # Order feature
│   │   ├── order-controller.ts
│   │   ├── order-manager.ts    # Complex logic needs manager
│   │   ├── order-provider.ts
│   │   ├── order-model.ts
│   │   └── order.test.ts
│   │
│   ├── payments/               # Payment feature
│   │   ├── payment-controller.ts
│   │   ├── payment-provider.ts # Simple CRUD, no manager needed
│   │   ├── payment-types.ts
│   │   └── payment.test.ts
│   │
│   └── notifications/          # Notification feature
│       ├── notification-controller.ts
│       ├── notification-manager.ts
│       ├── email-provider.ts
│       └── sms-provider.ts
│
├── shared/                     # Cross-feature utilities
│   ├── authentication/        # Auth middleware, JWT handling
│   ├── database/              # Database connection, base provider
│   ├── helpers/               # Date, crypto, validation helpers
│   ├── constants/             # App-wide constants
│   ├── types/                 # Shared TypeScript types
│   └── middleware/            # Common middleware
│
└── app.ts                      # Application entry point

Benefits of Feature-Based Structure:

  • Cohesion: All related code in one place
  • Discoverability: Easy to find all code for a feature
  • Isolation: Clear boundaries between features
  • Deletability: Remove entire feature by deleting folder
  • Scalability: Easy to add new features
  • Team work: Different teams can own different features

Technology-Specific Adaptations

SvelteKit Project

src/
├── routes/               # SvelteKit routes (IO Layer)
│   ├── api/             # API routes
│   └── (app)/           # App pages
├── lib/
│   ├── features/        # Domain features
│   │   ├── users/
│   │   └── orders/
│   └── shared/          # Shared utilities
│       ├── auth/
│       └── helpers/
└── app.html

Node.js/Express API

src/
├── features/            # Domain features
│   ├── users/
│   ├── orders/
│   └── payments/
├── shared/              # Shared code
│   ├── auth/
│   ├── database/
│   └── middleware/
└── app.ts

C# Web API

ProjectName/
├── Features/           # Domain features
│   ├── Users/
│   │   ├── UsersController.cs
│   │   ├── UserManager.cs
│   │   └── UserProvider.cs
│   └── Orders/
├── Shared/             # Shared code
│   ├── Authentication/
│   └── Database/
└── Program.cs

Testing Architecture

Test Structure Mirrors Code Structure

src/
├── features/
│   ├── users/
│   │   ├── user-controller.ts
│   │   ├── user-provider.ts
│   │   └── user.test.ts        # Tests with feature
│   └── orders/
│       ├── order-controller.ts
│       ├── order-manager.ts
│       └── order.test.ts       # Tests with feature
tests/                          # Only for E2E/integration
├── integration/
└── e2e/

Testing Each Layer

// Provider tests (unit)
describe('UserProvider', () => {
    it('should return user when exists', async () => {
        const provider = new UserProvider(mockDatabase);
        const user = await provider.getUserById('123');
        expect(user).toEqual(expectedUser);
    });
});

// Manager tests (unit with mocked providers)
describe('UserManager', () => {
    it('should create user and send email', async () => {
        const manager = new UserManager(mockUserProvider, mockEmailProvider);
        const result = await manager.createUser(userData);
        expect(result.success).toBe(true);
        expect(mockEmailProvider.sendWelcomeEmail).toHaveBeenCalled();
    });
});

// Controller tests (integration)
describe('UserController', () => {
    it('should return 200 for valid user', async () => {
        const response = await request(app).get('/api/users/123');
        expect(response.status).toBe(200);
        expect(response.body).toEqual(expectedUserResponse);
    });
});

Performance Considerations

Database Layer Optimization

  • Use connection pooling
  • Implement query batching
  • Add proper indexes
  • Use transactions for multi-operation flows

Caching Strategy

  • Provider-level caching for external APIs
  • Manager-level caching for computed results
  • HTTP-level caching for static responses

Async/Await Best Practices

// ✅ Good: Parallel execution when possible
async function getUserWithOrders(userId: string) {
    const [user, orders] = await Promise.all([
        userProvider.getUserById(userId),
        orderProvider.getOrdersByUserId(userId)
    ]);

    return { user, orders };
}

// ❌ Bad: Sequential when parallel is possible
async function getUserWithOrders(userId: string) {
    const user = await userProvider.getUserById(userId);
    const orders = await orderProvider.getOrdersByUserId(userId);

    return { user, orders };
}