Type-Safe AI Integration Patterns with the Instructor Library and Clean Architecture

Daichi Kugimiya

2025.10.9

This blog post is a translation of a Japanese article posted on September 29th, 2025.

Introduction

In recent years, the integration of AI functionalities into business applications has been rapidly increasing. However, since the output of AI is inherently uncertain, maintaining the type safety and maintainability valued in traditional web application development has become a challenge.

This article introduces a method for achieving a type-safe and highly maintainable AI integration pattern by combining the Instructor library with Clean Architecture. We will focus on best practices I’ve devised for utilizing AI in applications with complex business logic.

Technology Stack and Overall Design

Technologies Used

  • @instructor-ai/instructor: For handling structured AI outputs in a type-safe manner.
  • OpenAI API: Using LLMs like GPT-4 or Gemini.
  • Zod: For schema definition and validation.
  • TypeScript: To ensure type safety.
  • Clean Architecture: For layer separation and domain-centric design.

Architecture Overview

Core Design Patterns

1. Encapsulating AI Processing within Domain Entities

By concentrating all responsibility for AI processing within domain entities, we increase the cohesion between business logic and AI processing, minimizing external dependencies.

2. Standardization through a Unified Interface

Detailed Type Definitions

// Type definition for the AI execution context
export interface AIExecutionContext {
  userInput: string;
  attachedFileUrls?: string[];
  previousResults?: Record<string, any>;
  sessionContext?: Record<string, any>;
}

// Type definition for AI configuration
export interface AIConfig {
  model: string;         // Model name to use
  temperature: number;   // Creativity of generation (0-1)
  max_retries: number;   // Maximum number of retries
  timeout: number;       // Timeout (in milliseconds)
}

// Unified interface for AI-processable entities
export interface AIProcessableEntity {
  /**
   * Gets the Zod schema that defines the structure of the AI output.
   * @returns A Zod schema for validating the AI output.
   */
  getZodSchema(): z.ZodSchema<any>;

  /**
   * Generates the prompt for AI processing.
   * @param context The AI execution context.
   * @returns The generated prompt string.
   */
  generatePrompt(context: AIExecutionContext): string;

  /**
   * Gets the configuration for AI processing.
   * @returns The configuration object for AI processing.
   */
  getAIConfig(): AIConfig;

  /**
   * Builds the context string for the AI.
   * @param context The AI execution context.
   * @returns The context string to be used for AI processing.
   */
  getContextForAI(context: AIExecutionContext): string;

  /**
   * Parses the AI output and updates the entity's state.
   * @param aiResponse The response from the AI API.
   * @param userInput The input from the user.
   * @param attachedFileUrls An array of attached file URLs.
   */
  parseFromAIResponse(
    aiResponse: any,
    userInput: string,
    attachedFileUrls?: string[]
  ): void;
}

3. Implementation Example: Business Analysis Entity

// Output type definition for business recommendations
export interface BusinessAnalysisResponse {
  recommendations: Array<{
    id: string;
    title: string;
    description: string;
    priority: number;
    reasoning: string;
  }>;
  metadata: {
    analysisApproach: string;
    keyInsights: string[];
    confidence: number;
  };
}

// Business analysis entity implementing the unified interface
export class BusinessAnalysisEntity implements AIProcessableEntity {
  private recommendations: BusinessAnalysisResponse['recommendations'] = [];
  private metadata?: BusinessAnalysisResponse['metadata'];
  private userInput = '';
  private attachedFiles: string[] = [];

  getZodSchema(): z.ZodSchema<BusinessAnalysisResponse> {
    return z.object({
      recommendations: z.array(
        z.object({
          id: z.string().describe("Recommendation ID"),
          title: z.string().min(5).max(100).describe("Recommendation title"),
          description: z.string().min(50).max(300).describe("Detailed description"),
          priority: z.number().min(1).max(5).describe("Priority"),
          reasoning: z.string().min(20).max(200).describe("Reason for recommendation")
        })
      ).min(3).max(8).describe("Business recommendations"),
      metadata: z.object({
        analysisApproach: z.string().describe("Analysis approach"),
        keyInsights: z.array(z.string()).describe("Key insights"),
        confidence: z.number().min(0).max(1).describe("Confidence score")
      }).describe("Metadata")
    });
  }

  generatePrompt(context: AIExecutionContext): string {
    return `
You are a business analysis expert.

## Context
User Input: ${context.userInput}
Attached Files: ${context.attachedFileUrls?.join(', ') || 'None'}
Previous Results: ${JSON.stringify(context.previousResults) || 'None'}

## Instructions
Provide structured recommendations based on the following requirements.

## Output Requirements
- Provide actionable and specific recommendations.
- Show clear reasoning for each recommendation.
- Prioritize the recommendations to clarify implementation order.
    `;
  }

  getAIConfig(): AIConfig {
    return {
      model: "gpt-4",
      temperature: 0.1,
      max_retries: 3,
      timeout: 30000
    };
  }

  getContextForAI(context: AIExecutionContext): string {
    return `
User Input: ${context.userInput}
Attached Files: ${context.attachedFileUrls?.join(', ') || 'None'}
Previous Results: ${JSON.stringify(context.previousResults) || 'None'}
    `;
  }

  parseFromAIResponse(
    aiResponse: BusinessAnalysisResponse,
    userInput: string,
    attachedFileUrls?: string[]
  ): void {
    // Validate the AI response
    const schema = this.getZodSchema();
    const validated = schema.parse(aiResponse);

    // Update the entity's state
    this.recommendations = validated.recommendations;
    this.metadata = validated.metadata;
    this.userInput = userInput;
    this.attachedFiles = attachedFileUrls || [];
  }

  // Additional domain methods
  getPublicResult() {
    return {
      recommendations: this.recommendations,
      metadata: this.metadata
    };
  }
}

4. Type-Safe AI Service Layer

Let’s Implement an integration service layer that acts as a bridge between the domain entities and the Instructor client

export class UnifiedAIService {
  constructor(
    private instructorClient: InstructorClient,
    private logger: Logger
  ) {}

  async processWithEntity<T>(
    entity: AIProcessableEntity,
    context: AIExecutionContext
  ): Promise<T> {
    try {
      const prompt = entity.generatePrompt(context);
      const schema = entity.getZodSchema();
      const config = entity.getAIConfig();

      const result = await this.instructorClient.chat.completions.create({
        messages: [{ role: "user", content: prompt }],
        model: config.model,
        temperature: config.temperature,
        max_retries: config.max_retries,
        response_model: {
          schema: schema,
          name: `${entity.constructor.name}Response`
        }
      });

      // Update the entity's state
      entity.parseFromAIResponse(
        result,
        context.userInput,
        context.attachedFileUrls
      );

      return result as T;
    } catch (error) {
      this.logger.error('AI processing failed', {
        error,
        entity: entity.constructor.name
      });
      throw new AIProcessingError('Failed to process AI request', error);
    }
  }
}

Benefits of Implementation

1. Achieving Type Safety

The combination of Zod schemas and TypeScript type definitions enables consistent type safety from the AI output to its use within the application.

// The result of AI processing is completely type-safe
const result: BusinessAnalysisResponse = await aiService.processWithEntity(
  entity,
  context
);

// IDE autocompletion works
console.log(result.recommendations[0].title); // ✅ Type-safe
console.log(result.invalidProperty); // ❌ Compile error

2. Flexible Processing Based on Context

Prompts can be dynamically adjusted based on the information in the execution context (user input, attached files, previous results, etc.).

const context: AIExecutionContext = {
  userInput: "How can we improve sales?",
  attachedFileUrls: ["<https://example.com/sales-data.xlsx>"],
  previousResults: { lastAnalysis: "marketing focus needed" }
};

// Context information is automatically reflected in the prompt
const prompt = entity.generatePrompt(context);

3. Improved Testability

Since each domain entity manages its AI processing in a self-contained manner, unit testing with mocks becomes easier.

describe('BusinessAnalysisEntity', () => {
  it('should generate appropriate schema', () => {
    const entity = new BusinessAnalysisEntity();
    const schema = entity.getZodSchema();

    expect(schema.shape.recommendations).toBeDefined();
    expect(schema.shape.metadata).toBeDefined();
  });

  it('should parse AI response correctly', () => {
    const entity = new BusinessAnalysisEntity();
    const mockResponse: BusinessAnalysisResponse = {
      recommendations: [
        {
          id: "1",
          title: "Test Recommendation",
          description: "This is a test recommendation with a detailed description.",
          priority: 3,
          reasoning: "For testing purposes"
        }
      ],
      metadata: {
        analysisApproach: "Test Analysis",
        keyInsights: ["Insight 1"],
        confidence: 0.8
      }
    };

    entity.parseFromAIResponse(mockResponse, "Test input");
    const result = entity.getPublicResult();

    expect(result.recommendations).toHaveLength(1);
    expect(result.metadata?.confidence).toBe(0.8);
  });
});

4. Scalability and Maintainability

When new AI processing is required, you can respond simply by adding a new domain entity without impacting existing code.

// Output type definition for new business requirements
export interface RiskAnalysisResponse {
  analysis: {
    summary: string;
    riskLevel: 'low' | 'medium' | 'high';
    recommendations: string[];
  };
  actionItems: Array<{
    id: string;
    action: string;
    deadline: string;
    assignee?: string;
  }>;
}

// Entity for the new business requirement
export class RiskAnalysisEntity implements AIProcessableEntity {
  getZodSchema(): z.ZodSchema<RiskAnalysisResponse> {
    return z.object({
      analysis: z.object({
        summary: z.string().min(50).max(500).describe("Analysis summary"),
        riskLevel: z.enum(['low', 'medium', 'high']).describe("Risk level"),
        recommendations: z.array(z.string()).min(1).max(10).describe("Recommendations")
      }).describe("Analysis results"),
      actionItems: z.array(
        z.object({
          id: z.string().describe("Action item ID"),
          action: z.string().min(10).max(200).describe("Action to be taken"),
          deadline: z.string().describe("Deadline"),
          assignee: z.string().optional().describe("Assignee")
        })
      ).min(1).max(20).describe("Action items")
    });
  }

  generatePrompt(context: AIExecutionContext): string {
    return `
You are a business strategy advisor.

## Subject of Analysis
${context.userInput}

## Output Requirements
- Appropriately assess the risk level.
- Propose specific and actionable items.
- Set realistic deadlines.
    `;
  }

  getAIConfig(): AIConfig {
    return {
      model: "gpt-4-turbo",
      temperature: 0.2,
      max_retries: 5,
      timeout: 45000
    };
  }

  getContextForAI(context: AIExecutionContext): string {
    return `
User Input: ${context.userInput}
Attached Files: ${context.attachedFileUrls?.join(', ') || 'None'}
Session Info: ${JSON.stringify(context.sessionContext) || 'None'}
    `;
  }

  parseFromAIResponse(
    aiResponse: RiskAnalysisResponse,
    userInput: string,
    attachedFileUrls?: string[]
  ): void {
    const schema = this.getZodSchema();
    const validated = schema.parse(aiResponse);

    // Entity-specific state update logic
    // ... implementation details
  }
}

Error Handling and Retry Functionality

Let’s Implement robust error handling by combining it with the self-correction feature of the Instructor library.

export class AIErrorHandler {
  async handleAIProcessing<T>(
    processingFn: () => Promise<T>,
    entity: AIProcessableEntity,
    context: AIExecutionContext
  ): Promise<T> {
    try {
      return await processingFn();
    } catch (error) {
      if (error instanceof ValidationError) {
        // For schema validation errors
        this.logger.warn('Schema validation failed, retrying with adjusted prompt', {
          entity: entity.constructor.name,
          error: error.message
        });

        throw new AIValidationError('Invalid AI output format', error);
      } else if (error instanceof RateLimitError) {
        // For rate limit errors
        await this.waitForRateLimit();
        throw new AIRateLimitError('API rate limit reached', error);
      } else {
        // For other errors
        throw new AIProcessingError('An error occurred during AI processing', error);
      }
    }
  }

  private async waitForRateLimit(): Promise<void> {
    const waitTime = Math.random() * 5000 + 1000; // Random wait between 1-6 seconds
    await new Promise(resolve => setTimeout(resolve, waitTime));
  }
}

Conclusion

Adopting the architecture introduced in this article rewards you with:

  1. Type Safety: Consistent type safety from AI output throughout the application.
  2. Maintainability: Clear separation of responsibilities through domain entities.
  3. Scalability: Ease of adding new features via a unified interface.
  4. Testability: Simplified testing due to component independence.
  5. Reusability: Application of standardized patterns to other projects.
  6. Flexibility: Dynamic prompt generation based on context.

When integrating AI into complex business applications, it is crucial to carefully consider the architecture-level design, rather than simply calling an API. The combination of the Instructor library and appropriate architectural patterns enables an AI integration that balances maintainability and scalability.

Reference Links

採用情報

Blog一覧へ戻る

Feel free to contact us

We provide support
for all aspects of SRE,
from design and technical support
to deployment of tools for SRE operations.

Brochure request and inquiries