Skip to content

Architecture Patterns

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
  • Responsibilities:
  • Coordinate between providers
  • Implement business rules
  • Handle complex operations
  • Manage transactions
// 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: 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 2: Providers Return Domain Models

// ❌ 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 3: 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

Standard Folder Structure

src/
├── io/                     # IO Layer
│   ├── controllers/        # HTTP controllers
│   ├── routes/            # Route definitions
│   ├── middleware/        # Request middleware
│   └── validators/        # Input validation
├── managers/              # Management Layer
│   ├── user-manager.ts
│   ├── order-manager.ts
│   └── payment-manager.ts
├── providers/             # Provider Layer
│   ├── database/
│   │   ├── user-provider.ts
│   │   └── order-provider.ts
│   ├── external/
│   │   ├── email-provider.ts
│   │   └── payment-provider.ts
│   └── file/
│       └── storage-provider.ts
├── models/               # Side Layer - Data structures
│   ├── user.ts
│   ├── order.ts
│   └── payment.ts
├── mappers/              # Side Layer - Data transformation
│   ├── user-mapper.ts
│   └── order-mapper.ts
├── helpers/              # Side Layer - Utilities
│   ├── date-helper.ts
│   ├── validation-helper.ts
│   └── crypto-helper.ts
├── constants/            # Side Layer - Configuration
│   ├── app-constants.ts
│   └── error-messages.ts
└── types/               # Side Layer - Type definitions
    ├── api-types.ts
    └── database-types.ts

Technology-Specific Adaptations

SvelteKit Project

src/
├── routes/               # SvelteKit routes (IO Layer)
├── lib/
│   ├── managers/
│   ├── providers/
│   ├── models/
│   ├── mappers/
│   ├── helpers/
│   └── constants/
└── app.html

Node.js/Express API

src/
├── controllers/          # Express controllers (IO Layer)
├── routes/              # Express routes (IO Layer)
├── middleware/          # Express middleware (IO Layer)
├── managers/            # Management Layer
├── providers/           # Provider Layer
├── models/              # Domain models
├── mappers/             # Data transformation
├── helpers/             # Utilities
└── constants/           # Configuration

C# Web API

ProjectName/
├── Controllers/         # ASP.NET controllers (IO Layer)
├── Managers/           # Management Layer
├── Providers/          # Provider Layer
├── Models/             # Domain models
├── Mappers/            # Data transformation
├── Helpers/            # Utilities
└── Constants/          # Configuration

Testing Architecture

Test Structure Mirrors Code Structure

tests/
├── unit/
│   ├── managers/
│   ├── providers/
│   ├── mappers/
│   └── helpers/
├── integration/
│   ├── api/
│   └── database/
└── e2e/
    └── user-flows/

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 };
}