stock-bot/libs/LIBRARY_STANDARDS.md

4.9 KiB

Library Standards and Patterns

This document defines the standardized patterns for all libraries in the @stock-bot ecosystem.

Export Patterns

Standard: Named Exports Only

All libraries should use named exports only. Default exports have been removed for consistency and better tree-shaking.

Example:

// ✅ Good - Named exports
export { createCache } from './cache';
export type { CacheOptions } from './types';

// ❌ Bad - Default export
export default createCache;

Initialization Patterns

Libraries follow different initialization patterns based on their purpose:

1. Singleton with Global State

Use for: Global services that should have only one instance (config, logger)

Example: Config library

let configInstance: ConfigManager | null = null;

export function initializeConfig(): AppConfig {
  if (!configInstance) {
    configInstance = new ConfigManager();
  }
  return configInstance.initialize();
}

export function getConfig(): AppConfig {
  if (!configInstance) {
    throw new Error('Config not initialized');
  }
  return configInstance.get();
}

2. Factory with Registry

Use for: Services that need instance reuse based on configuration (cache, logger instances)

Example: Cache library

const cacheInstances = new Map<string, CacheProvider>();

export function createCache(options: CacheOptions): CacheProvider {
  if (options.shared) {
    const key = generateKey(options);
    if (cacheInstances.has(key)) {
      return cacheInstances.get(key)!;
    }
    const cache = new RedisCache(options);
    cacheInstances.set(key, cache);
    return cache;
  }
  return new RedisCache(options);
}

3. Pure Factory Functions

Use for: Services that need creation logic beyond simple instantiation

Example: Event bus with configuration processing

export function createEventBus(config: EventBusConfig): EventBus {
  // Process config, set defaults, etc.
  const processedConfig = { ...defaultConfig, ...config };
  return new EventBus(processedConfig);
}

Note: Simple instantiation doesn't need factories - use direct class instantiation or DI container.

4. Direct Class Exports

Use for: Simple utilities or services managed by DI container

Example: MongoDB library

export { MongoDBClient } from './client';
// No factory function - let DI container handle instantiation

5. Singleton Classes

Use for: Manager classes that coordinate multiple instances

Example: QueueManager

export class QueueManager {
  private static instance: QueueManager | null = null;
  
  static initialize(config: QueueConfig): QueueManager {
    if (!QueueManager.instance) {
      QueueManager.instance = new QueueManager(config);
    }
    return QueueManager.instance;
  }
  
  static getInstance(): QueueManager {
    if (!QueueManager.instance) {
      throw new Error('QueueManager not initialized');
    }
    return QueueManager.instance;
  }
}

Pattern Selection Guide

Choose the initialization pattern based on these criteria:

Pattern When to Use Examples
Singleton with Global State - One instance per process
- Stateful configuration
- Process-wide settings
config, logger setup
Factory with Registry - Multiple instances with same config should share
- Connection pooling
- Resource optimization
cache, logger instances
Pure Factory - Complex initialization logic
- Configuration processing needed
- Defaults to apply
event bus (if needed)
Direct Class Export - DI container manages lifecycle
- Simple initialization
- No special setup needed
database clients (MongoDB, PostgreSQL, QuestDB), utilities
Singleton Class - Coordinates multiple resources
- Central management point
- Graceful shutdown needed
QueueManager, ConnectionManager

Additional Standards

Error Handling

  • All libraries should throw descriptive errors
  • Consider creating custom error classes for domain-specific errors
  • Always include context in error messages

Configuration

  • Accept configuration through constructor/factory parameters
  • Validate configuration using Zod schemas
  • Provide sensible defaults where appropriate

Testing

  • All libraries must have unit tests
  • Use consistent test file naming: *.test.ts
  • Mock external dependencies

Documentation

  • Every library must have a README.md
  • Include usage examples
  • Document all public APIs with JSDoc

TypeScript

  • Export all public types
  • Use strict TypeScript settings
  • Avoid any types

Dependencies

  • Minimize external dependencies
  • Use exact versions for critical dependencies
  • Document peer dependencies clearly