Skip to content

Low-Level Design (LLD)

Version: 1.0.0 Last Updated: 2025-01-25 Status: Approved

Component Design

C4 Level 3: Component Diagram

Shared Tools Layer

The tools layer provides unified tool implementations used by both MCP and Chat:

Key components:

FilePurpose
types.tsShared types: ToolResult<T>, ContentItem, result types
core/list-types.tslistTypes() - List all distinct content types
core/list-content.tslistContent() - List content by type with filters
core/get-content.tsgetContent() - Get single item by type and slug
core/search-content.tssearchContent() - Search content by keywords
openai-adapter.tschatToolDefinitions (JSON Schema), executeToolCall()

Schema conversion:

typescript
// Zod schema in src/validation/tool.schemas.ts
export const ListContentInputSchema = z.object({
  type: z.string().regex(/^[a-z0-9-]+$/).max(100),
  status: z.enum(['draft', 'published', 'archived']).default('published'),
  limit: z.number().int().min(1).max(100).default(50),
})

// Converted to JSON Schema for OpenAI via zod-to-json-schema
import { zodToJsonSchema } from 'zod-to-json-schema'
const jsonSchema = zodToJsonSchema(ListContentInputSchema)

Class Diagrams

Repository Interfaces

Cache Provider Pattern

Error Class Hierarchy

Event System

Database Design

Entity Relationship Diagram

Sequence Diagrams

Content Bundle Request (Cache Hit)

Chat Message Flow (Full)

Content Versioning Transaction Flow

Transaction Details:

typescript
// src/repositories/content.repository.ts
async updateWithHistory(
  id: string,
  updates: Partial<ContentUpdate>,
  changedBy?: string
): Promise<Content> {
  return this.db.transaction(async (tx) => {
    // 1. Read current state
    const [current] = await tx
      .select()
      .from(content)
      .where(eq(content.id, id))

    if (!current) {
      throw new NotFoundError('Content', id)
    }

    // 2. Archive current version to history
    await tx.insert(contentHistory).values({
      id: generateId(),
      contentId: id,
      version: current.version,
      data: current.data,
      changeType: 'update',
      changedBy,
      changeSummary: this.summarizeChanges(current.data, updates.data),
      createdAt: new Date(),
    })

    // 3. Update content with new version
    const [updated] = await tx
      .update(content)
      .set({
        ...updates,
        version: current.version + 1,
        updatedAt: new Date(),
      })
      .where(eq(content.id, id))
      .returning()

    return updated
  })
}

Content Restore Flow

Circuit Breaker State Transitions

Configuration:

  • failureThreshold: 5 (failures before opening)
  • resetTimeout: 30000ms (time before trying half-open)
  • halfOpenMaxAttempts: 2 (successes needed to close)

Error Handling

Error Response Format

typescript
interface ErrorResponse {
  error: string           // Human-readable message
  code: string            // Machine-readable code
  requestId: string       // For tracing
  fields?: Record<string, string[]>  // Validation errors
  retryAfter?: number     // For rate limiting
  stack?: string          // Dev mode only
}

HTTP Status Code Mapping

StatusCodeScenario
400VALIDATION_ERRORInvalid request body/params
401UNAUTHORIZEDMissing/invalid admin key
404NOT_FOUNDResource doesn't exist
409CONFLICTDuplicate slug
429RATE_LIMITEDToken bucket empty
500INTERNAL_ERRORUnexpected server error
502LLM_ERRORLLM provider failure

Error Class Implementation

typescript
// src/errors/app-error.ts
export class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number,
    public readonly isOperational: boolean = true,
    public readonly details?: Record<string, unknown>
  ) {
    super(message)
    this.name = this.constructor.name
    Error.captureStackTrace(this, this.constructor)
  }
}

// Specialized error classes
export class ValidationError extends AppError {
  constructor(
    message: string,
    public readonly fields: Record<string, string[]>
  ) {
    super(message, 'VALIDATION_ERROR', 400, true, { fields })
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string, identifier: string) {
    super(
      `${resource} not found: ${identifier}`,
      'NOT_FOUND',
      404
    )
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Invalid or missing authentication') {
    super(message, 'UNAUTHORIZED', 401)
  }
}

export class RateLimitError extends AppError {
  constructor(public readonly retryAfter: number) {
    super(
      'Rate limit exceeded',
      'RATE_LIMITED',
      429,
      true,
      { retryAfter }
    )
  }
}

export class ConflictError extends AppError {
  constructor(message: string, field?: string) {
    super(message, 'CONFLICT', 409, true, field ? { field } : undefined)
  }
}

export class LLMError extends AppError {
  constructor(
    message: string,
    public readonly provider: string,
    public readonly originalError?: Error
  ) {
    super(message, 'LLM_ERROR', 502, true, { provider })
  }
}

Throwing Errors in Services

typescript
// src/services/content.service.ts
export class ContentService {
  async getBySlug(type: string, slug: string): Promise<Content> {
    const content = await this.repository.findBySlug(type, slug)

    if (!content) {
      throw new NotFoundError('Content', `${type}/${slug}`)
    }

    if (content.status !== 'published') {
      throw new NotFoundError('Content', `${type}/${slug}`)
    }

    return content
  }

  async create(data: CreateContentInput): Promise<Content> {
    // Check for duplicate slug
    if (data.slug) {
      const existing = await this.repository.findBySlug(data.type, data.slug)
      if (existing) {
        throw new ConflictError(
          `Content with slug '${data.slug}' already exists`,
          'slug'
        )
      }
    }

    return this.repository.create(data)
  }
}

// src/services/chat.service.ts
export class ChatService {
  async processMessage(input: ChatInput): Promise<ChatResponse> {
    // Rate limit check happens in middleware, but we double-check here
    const allowed = await this.rateLimiter.consume(input.ipHash, 1)
    if (!allowed.allowed) {
      throw new RateLimitError(allowed.retryAfter)
    }

    try {
      const response = await this.circuitBreaker.execute(() =>
        this.llmProvider.chat(messages, options)
      )
      return response
    } catch (error) {
      if (error instanceof CircuitBreakerOpenError) {
        throw new LLMError(
          'AI service temporarily unavailable',
          this.llmProvider.name
        )
      }
      throw error
    }
  }
}

See Schemas & Errors - Error Codes for error response examples and the full status code mapping.

Testing Strategy

Test Pyramid

Coverage Targets

MetricTargetRationale
Line Coverage80%Balance between confidence and maintenance
Branch Coverage75%Ensure error paths are tested
Critical Paths100%Auth, rate limiting, data mutations
Infrastructure60%Harder to test, rely on integration tests

Test Categories

CategoryDirectoryFocusTools
Unittests/unit/Functions, classes in isolationJest (ts-jest ESM), mocks
Integrationtests/integration/API endpoints, DB operationsSupertest, in-memory SQLite
E2Etests/e2e/Full HTTP and MCP flowsSupertest, in-memory SQLite

Database Testing Strategy

See tests/ directory for implementation examples including mocks, fixtures, and test utilities.

Appendix

File Naming Conventions

Files use dot-separated naming to indicate their layer:

TypePatternExample
Routes{resource}.routes.tscontent.routes.ts, chat.routes.ts
Services{name}.service.tschat.service.ts, content.service.ts
Repositories{entity}.repository.tscontent.repository.ts, chat.repository.ts
Middleware{name}.middleware.tsadmin-auth.middleware.ts, idempotency.middleware.ts
Types{name}.types.tschat.types.ts, content.types.ts
Schemas{name}.schemas.tscontent.schemas.ts, chat.schemas.ts
Tests{name}.test.tsrate-limiter.test.ts, chat.service.test.ts

Code Style

  • Use async/await over Promise chains
  • Prefer const over let
  • Use explicit return types on public functions
  • Use unknown over any where possible
  • Document public APIs with JSDoc comments

Released under the MIT License.