T-CREATOR

Dify 中心のドメイン駆動設計:ユースケースとフローの境界づけ

Dify 中心のドメイン駆動設計:ユースケースとフローの境界づけ

AI アプリケーション開発プラットフォームである Dify を使うと、ノーコード・ローコードで素早く AI 機能を実装できます。しかし、実際のプロダクション環境で Dify を活用する際には、「どこまでを Dify で実装し、どこから独自実装するか」という境界の設計が重要になってきます。

この記事では、ドメイン駆動設計(DDD)の考え方を Dify 中心のアーキテクチャに適用し、ユースケースとフローの適切な境界づけについて解説します。Dify の強みを最大限活かしながら、保守性・拡張性の高いシステムを設計する方法をご紹介いたしますね。

背景

Dify とは

Dify は、LLM(大規模言語モデル)を活用した AI アプリケーションを迅速に構築できるオープンソースプラットフォームです。プロンプトエンジニアリング、RAG(検索拡張生成)、エージェント機能などを GUI ベースで設計・管理できるため、開発コストを大幅に削減できます。

typescript// Dify API を呼び出す基本的な例
import axios from 'axios';

const DIFY_API_KEY = process.env.DIFY_API_KEY;
const DIFY_API_URL = 'https://api.dify.ai/v1';
typescript// Dify のチャット完了 API を呼び出す関数
async function callDifyChatCompletion(
  query: string,
  conversationId?: string
) {
  const response = await axios.post(
    `${DIFY_API_URL}/chat-messages`,
    {
      inputs: {},
      query: query,
      response_mode: 'blocking',
      conversation_id: conversationId,
      user: 'user-123',
    },
    {
      headers: {
        Authorization: `Bearer ${DIFY_API_KEY}`,
        'Content-Type': 'application/json',
      },
    }
  );

  return response.data;
}

ドメイン駆動設計の基本

ドメイン駆動設計(DDD)は、複雑なビジネスロジックを整理し、保守性の高いソフトウェアを構築するための設計手法です。特に重要なのが以下の概念でしょう。

#概念説明
1ドメインモデルビジネスルールや概念をコードで表現したもの
2境界づけられたコンテキスト特定のドメインモデルが有効な範囲
3ユースケースシステムが提供する具体的な機能単位
4エンティティ一意の識別子を持つドメインオブジェクト
5値オブジェクト属性のみで識別されるドメインオブジェクト

以下の図は、DDD の基本構造を示しています。

mermaidflowchart TB
  subgraph presentation["プレゼンテーション層"]
    controller["コントローラー"]
  end

  subgraph application["アプリケーション層"]
    usecase["ユースケース"]
  end

  subgraph domain["ドメイン層"]
    entity["エンティティ"]
    vo["値オブジェクト"]
    service["ドメインサービス"]
  end

  subgraph infrastructure["インフラストラクチャ層"]
    repository["リポジトリ"]
    external["外部 API"]
  end

  controller -->|呼び出し| usecase
  usecase -->|利用| entity
  usecase -->|利用| service
  entity -->|含む| vo
  usecase -->|呼び出し| repository
  repository -->|永続化| entity
  usecase -->|呼び出し| external

この図から、各層が明確に分離され、依存関係が一方向であることがわかります。プレゼンテーション層からドメイン層への依存はあっても、その逆はありません。

Dify を活用した開発の課題

Dify は強力なツールですが、すべてを Dify で実装しようとすると、以下のような課題が生じます。

  • ビジネスロジックが Dify のワークフローに密結合する
  • Dify 外部のデータベースやサービスとの連携が複雑になる
  • テストやバージョン管理が困難になる
  • チーム開発での役割分担が曖昧になる

これらの課題を解決するには、適切な境界づけが必要です。

課題

Dify の責務範囲の不明確さ

多くのプロジェクトで見られる問題は、「Dify にどこまで任せるべきか」が明確でないことです。以下のようなケースで迷いが生じます。

typescript// アンチパターン:すべてのロジックを Dify API 呼び出しに依存
class OrderService {
  async createOrder(userId: string, items: OrderItem[]) {
    // ビジネスルール検証も Dify に依存
    const validationResult = await callDifyWorkflow(
      'order-validation',
      {
        userId,
        items,
      }
    );

    if (!validationResult.isValid) {
      throw new Error(validationResult.message);
    }

    // 在庫確認も Dify に依存
    const stockCheck = await callDifyWorkflow(
      'stock-check',
      { items }
    );

    // 価格計算も Dify に依存
    const pricing = await callDifyWorkflow(
      'price-calculation',
      { items }
    );

    // 注文作成も Dify に依存
    return await callDifyWorkflow('create-order', {
      userId,
      items,
      totalPrice: pricing.total,
    });
  }
}

このコードの問題点は、すべてのビジネスロジックが Dify に依存している点です。ビジネスルールの変更や、Dify 以外のシステムへの移行が非常に困難になります。

ユースケースの粒度の問題

Dify のワークフローと、アプリケーションのユースケースの粒度が合わないケースも頻繁に発生します。

typescript// 粒度が粗すぎる例:1 つの Dify ワークフローで複数のユースケースを処理
async function handleCustomerRequest(
  requestType: string,
  data: any
) {
  // すべてのリクエストを 1 つの巨大なワークフローで処理
  return await callDifyWorkflow(
    'customer-request-handler',
    {
      type: requestType,
      data: data,
    }
  );
}

このアプローチでは、各ユースケースの独立性が失われ、テストや保守が困難になってしまいます。

以下の図は、粒度の問題を視覚化したものです。

mermaidflowchart LR
  subgraph problems["課題のあるアーキテクチャ"]
    direction TB
    app1["アプリケーション"]
    mega["巨大な Dify<br/>ワークフロー"]

    app1 -->|すべてを委譲| mega
  end

  subgraph ideal["理想的なアーキテクチャ"]
    direction TB
    app2["アプリケーション"]
    uc1["ユースケース 1"]
    uc2["ユースケース 2"]
    uc3["ユースケース 3"]
    flow1["Dify フロー 1"]
    flow2["Dify フロー 2"]

    app2 --> uc1
    app2 --> uc2
    app2 --> uc3
    uc1 --> flow1
    uc2 --> flow1
    uc3 --> flow2
  end

左側の問題のあるアーキテクチャでは、すべてが 1 つの巨大なワークフローに依存していますが、右側の理想的なアーキテクチャでは、ユースケースごとに適切な Dify フローが割り当てられています。

ドメインロジックの流出

もう 1 つの大きな課題は、本来アプリケーション側で管理すべきドメインロジックが Dify 側に流出してしまうことです。

typescript// ドメインロジックが流出している例
interface Product {
  id: string;
  name: string;
  price: number;
}

class CartService {
  async addToCart(
    userId: string,
    productId: string,
    quantity: number
  ) {
    // 価格計算、割引適用などのビジネスルールを Dify に委ねている
    const result = await callDifyWorkflow('add-to-cart', {
      userId,
      productId,
      quantity,
    });

    // ドメインモデルではなく Dify のレスポンスをそのまま返す
    return result;
  }
}

この実装では、価格計算や割引適用といった重要なビジネスルールが Dify 側に隠蔽されてしまい、アプリケーション側からは制御できなくなります。

解決策

境界づけられたコンテキストの定義

Dify を中心としたアーキテクチャでは、まず「Dify が担当する領域」と「アプリケーションが担当する領域」を明確に境界づける必要があります。

以下の原則に基づいて境界を定義しましょう。

#領域担当者
1コアビジネスロジックアプリケーション価格計算、在庫管理、権限チェック
2AI による判断・生成Difyテキスト生成、意図分類、要約
3データ永続化アプリケーションデータベース操作、トランザクション管理
4AI エージェント機能Dify複数ステップの推論、ツール呼び出し
5ビジネスルール検証アプリケーション入力値検証、制約チェック

境界づけられたコンテキストを図で表現すると、以下のようになります。

mermaidflowchart TB
  subgraph app_context["アプリケーションコンテキスト"]
    direction TB
    domain_logic["ドメインロジック<br/>(価格計算・在庫管理)"]
    validation["ビジネスルール検証"]
    persistence["データ永続化"]
  end

  subgraph dify_context["Dify コンテキスト"]
    direction TB
    ai_inference["AI 推論<br/>(意図分類・生成)"]
    agent["エージェント機能"]
    prompt["プロンプト管理"]
  end

  subgraph interface_layer["インターフェース層"]
    api_call["API 呼び出し"]
    webhook["Webhook 受信"]
  end

  domain_logic -->|AI 機能が必要| interface_layer
  interface_layer -->|リクエスト| ai_inference
  ai_inference -->|レスポンス| interface_layer
  interface_layer -->|結果| domain_logic

  agent -->|ツール呼び出し| webhook
  webhook -->|データ取得| persistence

この図から、アプリケーションコンテキストと Dify コンテキストが明確に分離され、インターフェース層を介してやり取りしていることがわかります。

ユースケース層の設計

DDD のアプリケーション層に相当するユースケース層を設計し、Dify との連携を制御します。

typescript// ユースケースのインターフェース定義
interface UseCase<TRequest, TResponse> {
  execute(request: TRequest): Promise<TResponse>;
}
typescript// Dify 連携を抽象化したインターフェース
interface DifyService {
  classify(text: string): Promise<ClassificationResult>;
  generate(prompt: string, context: any): Promise<string>;
  executeAgent(
    taskDescription: string
  ): Promise<AgentResult>;
}
typescript// 具体的なユースケースの実装例
class CustomerInquiryUseCase
  implements UseCase<InquiryRequest, InquiryResponse>
{
  constructor(
    private difyService: DifyService,
    private inquiryRepository: InquiryRepository,
    private notificationService: NotificationService
  ) {}

  async execute(
    request: InquiryRequest
  ): Promise<InquiryResponse> {
    // 1. 入力値検証(アプリケーション側の責務)
    this.validateRequest(request);

    // 2. 問い合わせ内容を分類(Dify の責務)
    const classification = await this.difyService.classify(
      request.message
    );

    // 3. ドメインオブジェクトの生成(アプリケーション側の責務)
    const inquiry = new Inquiry({
      customerId: request.customerId,
      message: request.message,
      category: classification.category,
      priority: this.calculatePriority(classification),
    });

    // 4. 永続化(アプリケーション側の責務)
    await this.inquiryRepository.save(inquiry);

    // 5. 必要に応じて通知(アプリケーション側の責務)
    if (inquiry.priority === 'high') {
      await this.notificationService.notifySupport(inquiry);
    }

    return {
      inquiryId: inquiry.id,
      estimatedResponseTime: inquiry.estimatedResponseTime,
    };
  }

  private validateRequest(request: InquiryRequest): void {
    if (!request.customerId || !request.message) {
      throw new ValidationError('必須項目が不足しています');
    }
  }

  private calculatePriority(
    classification: ClassificationResult
  ): Priority {
    // ビジネスルールに基づいて優先度を計算
    if (
      classification.sentiment === 'negative' &&
      classification.category === 'complaint'
    ) {
      return 'high';
    }
    return 'normal';
  }
}

このコードでは、ユースケースが明確に定義され、Dify は「分類」という特定の機能のみを担当しています。ビジネスロジックはアプリケーション側で管理されているため、テストや変更が容易です。

Dify フローの適切な粒度

Dify のワークフローは、1 つの明確な責務を持つように設計します。

typescript// Dify フローを適切な粒度で定義
class DifyServiceImpl implements DifyService {
  // 意図分類専用のフロー
  async classify(
    text: string
  ): Promise<ClassificationResult> {
    const response = await callDifyWorkflow(
      'intent-classifier',
      {
        text: text,
      }
    );

    return {
      category: response.category,
      confidence: response.confidence,
      sentiment: response.sentiment,
    };
  }

  // テキスト生成専用のフロー
  async generate(
    prompt: string,
    context: any
  ): Promise<string> {
    const response = await callDifyWorkflow(
      'text-generator',
      {
        prompt: prompt,
        context: context,
      }
    );

    return response.generated_text;
  }

  // エージェント実行専用のフロー
  async executeAgent(
    taskDescription: string
  ): Promise<AgentResult> {
    const response = await callDifyWorkflow('task-agent', {
      task: taskDescription,
    });

    return {
      result: response.result,
      toolsUsed: response.tools_used,
      steps: response.steps,
    };
  }
}

各メソッドが 1 つの Dify ワークフローに対応し、責務が明確になっています。これにより、個別のフローをテスト・改善しやすくなります。

ドメインモデルの設計

コアとなるビジネスロジックは、ドメインモデルとして表現します。

typescript// エンティティの定義
class Inquiry {
  readonly id: string;
  readonly customerId: string;
  readonly message: string;
  readonly category: InquiryCategory;
  readonly priority: Priority;
  readonly createdAt: Date;
  private status: InquiryStatus;

  constructor(props: InquiryProps) {
    this.id = props.id || generateId();
    this.customerId = props.customerId;
    this.message = props.message;
    this.category = props.category;
    this.priority = props.priority;
    this.createdAt = props.createdAt || new Date();
    this.status = InquiryStatus.Open;

    this.validate();
  }

  private validate(): void {
    if (this.message.length < 10) {
      throw new DomainError(
        '問い合わせ内容は10文字以上必要です'
      );
    }
  }

  // ビジネスロジックをメソッドとして実装
  get estimatedResponseTime(): number {
    switch (this.priority) {
      case 'high':
        return 1; // 1時間以内
      case 'normal':
        return 24; // 24時間以内
      case 'low':
        return 72; // 72時間以内
    }
  }

  // 状態遷移もドメインモデルで管理
  assignToSupport(supportId: string): void {
    if (this.status !== InquiryStatus.Open) {
      throw new DomainError(
        'この問い合わせは既に担当者が割り当てられています'
      );
    }
    this.status = InquiryStatus.Assigned;
  }

  close(): void {
    if (this.status !== InquiryStatus.Assigned) {
      throw new DomainError(
        '未割り当ての問い合わせは完了できません'
      );
    }
    this.status = InquiryStatus.Closed;
  }
}
typescript// 値オブジェクトの定義
class ClassificationResult {
  readonly category: InquiryCategory;
  readonly confidence: number;
  readonly sentiment: 'positive' | 'neutral' | 'negative';

  constructor(
    category: InquiryCategory,
    confidence: number,
    sentiment: string
  ) {
    if (confidence < 0 || confidence > 1) {
      throw new Error(
        '信頼度は0から1の範囲である必要があります'
      );
    }

    this.category = category;
    this.confidence = confidence;
    this.sentiment = sentiment as any;
  }

  isHighConfidence(): boolean {
    return this.confidence >= 0.8;
  }
}

ドメインモデルには、ビジネスルールや制約が含まれており、Dify の結果はあくまで入力値として扱われます。

レイヤードアーキテクチャの実装

最終的に、以下のようなレイヤードアーキテクチャを構築します。

typescript// プレゼンテーション層(API ハンドラー)
import { Request, Response } from 'express';

class InquiryController {
  constructor(
    private inquiryUseCase: CustomerInquiryUseCase
  ) {}

  async createInquiry(
    req: Request,
    res: Response
  ): Promise<void> {
    try {
      const result = await this.inquiryUseCase.execute({
        customerId: req.body.customerId,
        message: req.body.message,
      });

      res.status(201).json(result);
    } catch (error) {
      this.handleError(error, res);
    }
  }

  private handleError(error: any, res: Response): void {
    if (error instanceof ValidationError) {
      res.status(400).json({ error: error.message });
    } else {
      res
        .status(500)
        .json({ error: '内部エラーが発生しました' });
    }
  }
}
typescript// インフラストラクチャ層(リポジトリ実装)
class InquiryRepositoryImpl implements InquiryRepository {
  constructor(private database: Database) {}

  async save(inquiry: Inquiry): Promise<void> {
    await this.database.query(
      'INSERT INTO inquiries (id, customer_id, message, category, priority, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
      [
        inquiry.id,
        inquiry.customerId,
        inquiry.message,
        inquiry.category,
        inquiry.priority,
        'open',
        inquiry.createdAt,
      ]
    );
  }

  async findById(id: string): Promise<Inquiry | null> {
    const row = await this.database.queryOne(
      'SELECT * FROM inquiries WHERE id = ?',
      [id]
    );

    if (!row) return null;

    return new Inquiry({
      id: row.id,
      customerId: row.customer_id,
      message: row.message,
      category: row.category,
      priority: row.priority,
      createdAt: new Date(row.created_at),
    });
  }
}
typescript// 依存性の注入(DI コンテナ)
class DIContainer {
  private static instance: DIContainer;
  private services: Map<string, any> = new Map();

  static getInstance(): DIContainer {
    if (!this.instance) {
      this.instance = new DIContainer();
    }
    return this.instance;
  }

  register(): void {
    // インフラストラクチャ層
    const database = new Database(process.env.DATABASE_URL);
    const difyService = new DifyServiceImpl();
    const inquiryRepository = new InquiryRepositoryImpl(
      database
    );
    const notificationService =
      new NotificationServiceImpl();

    // ユースケース層
    const inquiryUseCase = new CustomerInquiryUseCase(
      difyService,
      inquiryRepository,
      notificationService
    );

    // プレゼンテーション層
    const inquiryController = new InquiryController(
      inquiryUseCase
    );

    this.services.set(
      'inquiryController',
      inquiryController
    );
  }

  get<T>(serviceName: string): T {
    return this.services.get(serviceName);
  }
}

この構造により、各層の責務が明確になり、テストや保守が容易になります。

具体例

ケーススタディ:カスタマーサポートシステム

実際のプロジェクトを例に、Dify 中心の DDD アーキテクチャを見ていきましょう。

システム要件

  • 顧客からの問い合わせを受け付ける
  • AI で問い合わせ内容を自動分類する
  • 緊急度に応じて適切な担当者にルーティングする
  • 自動返信で初期対応を行う
  • 問い合わせ履歴を管理する

アーキテクチャ設計

以下の図は、システム全体のアーキテクチャを示しています。

mermaidflowchart TB
  subgraph presentation["プレゼンテーション層"]
    api["REST API"]
    webhook_receiver["Webhook 受信"]
  end

  subgraph application["アプリケーション層"]
    create_inquiry["問い合わせ作成<br/>ユースケース"]
    auto_reply["自動返信<br/>ユースケース"]
    assign_support["担当者割当<br/>ユースケース"]
  end

  subgraph domain["ドメイン層"]
    inquiry_entity["Inquiry<br/>エンティティ"]
    support_entity["Support<br/>エンティティ"]
    routing_service["ルーティング<br/>サービス"]
  end

  subgraph infrastructure["インフラストラクチャ層"]
    inquiry_repo["Inquiry<br/>リポジトリ"]
    support_repo["Support<br/>リポジトリ"]
    dify_service["Dify<br/>サービス"]
    email_service["メール<br/>サービス"]
  end

  subgraph dify_flows["Dify ワークフロー"]
    classifier["意図分類<br/>フロー"]
    generator["返信生成<br/>フロー"]
  end

  api -->|呼び出し| create_inquiry
  create_inquiry -->|利用| inquiry_entity
  create_inquiry -->|呼び出し| dify_service
  create_inquiry -->|呼び出し| inquiry_repo

  dify_service -->|リクエスト| classifier
  classifier -->|分類結果| dify_service

  create_inquiry -->|トリガー| auto_reply
  auto_reply -->|呼び出し| dify_service
  dify_service -->|リクエスト| generator
  auto_reply -->|送信| email_service

  create_inquiry -->|トリガー| assign_support
  assign_support -->|利用| routing_service
  assign_support -->|呼び出し| support_repo

この図から、各層が明確に分離され、Dify は特定の AI 機能のみを担当していることがわかります。ビジネスロジックはドメイン層で管理され、永続化はインフラストラクチャ層が担当しています。

実装例:問い合わせ作成フロー

ステップ 1 から順に実装を見ていきましょう。

ステップ 1:ドメインモデルの定義

typescript// 問い合わせカテゴリの列挙型
enum InquiryCategory {
  TechnicalSupport = 'technical_support',
  Billing = 'billing',
  FeatureRequest = 'feature_request',
  Complaint = 'complaint',
  General = 'general',
}

// 優先度の列挙型
enum Priority {
  High = 'high',
  Normal = 'normal',
  Low = 'low',
}

// 問い合わせの状態
enum InquiryStatus {
  Open = 'open',
  Assigned = 'assigned',
  InProgress = 'in_progress',
  Resolved = 'resolved',
  Closed = 'closed',
}

ステップ 2:エンティティとビジネスロジック

typescript// 問い合わせエンティティ
class Inquiry {
  readonly id: string;
  readonly customerId: string;
  readonly message: string;
  readonly category: InquiryCategory;
  readonly priority: Priority;
  readonly createdAt: Date;
  private _status: InquiryStatus;
  private _assignedTo?: string;
  private _resolvedAt?: Date;

  constructor(props: InquiryProps) {
    this.id = props.id || generateId();
    this.customerId = props.customerId;
    this.message = props.message;
    this.category = props.category;
    this.priority = props.priority;
    this.createdAt = props.createdAt || new Date();
    this._status = props.status || InquiryStatus.Open;
    this._assignedTo = props.assignedTo;
    this._resolvedAt = props.resolvedAt;

    this.validate();
  }

  private validate(): void {
    if (!this.customerId) {
      throw new DomainError('顧客 ID は必須です');
    }
    if (this.message.length < 10) {
      throw new DomainError(
        '問い合わせ内容は 10 文字以上必要です'
      );
    }
  }

  // ビジネスルール:推定応答時間の計算
  get estimatedResponseTime(): number {
    const baseTime = this.getBaseResponseTime();
    const categoryMultiplier = this.getCategoryMultiplier();
    return baseTime * categoryMultiplier;
  }

  private getBaseResponseTime(): number {
    switch (this.priority) {
      case Priority.High:
        return 1;
      case Priority.Normal:
        return 24;
      case Priority.Low:
        return 72;
    }
  }

  private getCategoryMultiplier(): number {
    // 苦情は 50% 早く対応
    if (this.category === InquiryCategory.Complaint) {
      return 0.5;
    }
    return 1.0;
  }

  // ビジネスルール:担当者割当
  assignTo(supportId: string): void {
    if (this._status !== InquiryStatus.Open) {
      throw new DomainError(
        'オープン状態の問い合わせのみ割り当て可能です'
      );
    }
    this._assignedTo = supportId;
    this._status = InquiryStatus.Assigned;
  }

  // ビジネスルール:解決
  resolve(): void {
    if (!this._assignedTo) {
      throw new DomainError(
        '担当者が割り当てられていません'
      );
    }
    if (this._status === InquiryStatus.Closed) {
      throw new DomainError('既に完了済みです');
    }
    this._status = InquiryStatus.Resolved;
    this._resolvedAt = new Date();
  }

  get status(): InquiryStatus {
    return this._status;
  }

  get assignedTo(): string | undefined {
    return this._assignedTo;
  }
}

ステップ 3:Dify サービスの実装

typescript// Dify 連携のインターフェース
interface DifyService {
  classifyInquiry(
    message: string
  ): Promise<InquiryClassification>;
  generateAutoReply(inquiry: Inquiry): Promise<string>;
}

// 分類結果の型定義
interface InquiryClassification {
  category: InquiryCategory;
  priority: Priority;
  confidence: number;
  keywords: string[];
}
typescript// Dify サービスの実装
class DifyServiceImpl implements DifyService {
  private readonly apiKey: string;
  private readonly baseUrl: string;

  constructor(
    apiKey: string,
    baseUrl: string = 'https://api.dify.ai/v1'
  ) {
    this.apiKey = apiKey;
    this.baseUrl = baseUrl;
  }

  async classifyInquiry(
    message: string
  ): Promise<InquiryClassification> {
    // Dify の分類ワークフローを呼び出し
    const response = await axios.post(
      `${this.baseUrl}/workflows/run`,
      {
        inputs: { message },
        response_mode: 'blocking',
        user: 'system',
      },
      {
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
      }
    );

    // Dify のレスポンスをドメインモデルに変換
    return this.mapToDomainModel(response.data);
  }

  private mapToDomainModel(
    difyResponse: any
  ): InquiryClassification {
    return {
      category: this.mapCategory(
        difyResponse.data.outputs.category
      ),
      priority: this.mapPriority(
        difyResponse.data.outputs.priority
      ),
      confidence: difyResponse.data.outputs.confidence,
      keywords: difyResponse.data.outputs.keywords || [],
    };
  }

  private mapCategory(category: string): InquiryCategory {
    const mapping: Record<string, InquiryCategory> = {
      technical: InquiryCategory.TechnicalSupport,
      billing: InquiryCategory.Billing,
      feature: InquiryCategory.FeatureRequest,
      complaint: InquiryCategory.Complaint,
      general: InquiryCategory.General,
    };
    return mapping[category] || InquiryCategory.General;
  }

  private mapPriority(priority: string): Priority {
    const mapping: Record<string, Priority> = {
      high: Priority.High,
      normal: Priority.Normal,
      low: Priority.Low,
    };
    return mapping[priority] || Priority.Normal;
  }

  async generateAutoReply(
    inquiry: Inquiry
  ): Promise<string> {
    // 自動返信生成ワークフローを呼び出し
    const response = await axios.post(
      `${this.baseUrl}/workflows/run`,
      {
        inputs: {
          message: inquiry.message,
          category: inquiry.category,
          estimatedTime: inquiry.estimatedResponseTime,
        },
        response_mode: 'blocking',
        user: 'system',
      },
      {
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
      }
    );

    return response.data.data.outputs.reply;
  }
}

ステップ 4:ユースケースの実装

typescript// 問い合わせ作成ユースケースのリクエスト
interface CreateInquiryRequest {
  customerId: string;
  message: string;
}

// 問い合わせ作成ユースケースのレスポンス
interface CreateInquiryResponse {
  inquiryId: string;
  category: InquiryCategory;
  priority: Priority;
  estimatedResponseTime: number;
  autoReply: string;
}
typescript// 問い合わせ作成ユースケースの実装
class CreateInquiryUseCase {
  constructor(
    private difyService: DifyService,
    private inquiryRepository: InquiryRepository,
    private emailService: EmailService,
    private eventBus: EventBus
  ) {}

  async execute(
    request: CreateInquiryRequest
  ): Promise<CreateInquiryResponse> {
    // 1. 入力値検証(アプリケーション層の責務)
    this.validateRequest(request);

    // 2. AI による分類(Dify の責務)
    const classification =
      await this.difyService.classifyInquiry(
        request.message
      );

    // 3. 信頼度チェック(アプリケーション層の責務)
    if (classification.confidence < 0.7) {
      // 信頼度が低い場合はデフォルト値を使用
      classification.category = InquiryCategory.General;
      classification.priority = Priority.Normal;
    }

    // 4. ドメインオブジェクトの生成(ドメイン層の責務)
    const inquiry = new Inquiry({
      customerId: request.customerId,
      message: request.message,
      category: classification.category,
      priority: classification.priority,
    });

    // 5. 永続化(インフラストラクチャ層の責務)
    await this.inquiryRepository.save(inquiry);

    // 6. 自動返信の生成と送信(Dify + アプリケーション層)
    const autoReply =
      await this.difyService.generateAutoReply(inquiry);
    await this.emailService.sendAutoReply(
      request.customerId,
      autoReply,
      inquiry.id
    );

    // 7. イベント発行(イベント駆動アーキテクチャ)
    await this.eventBus.publish(
      new InquiryCreatedEvent(inquiry)
    );

    // 8. レスポンスの構築
    return {
      inquiryId: inquiry.id,
      category: inquiry.category,
      priority: inquiry.priority,
      estimatedResponseTime: inquiry.estimatedResponseTime,
      autoReply: autoReply,
    };
  }

  private validateRequest(
    request: CreateInquiryRequest
  ): void {
    if (!request.customerId) {
      throw new ValidationError('顧客 ID は必須です');
    }
    if (
      !request.message ||
      request.message.trim().length < 10
    ) {
      throw new ValidationError(
        '問い合わせ内容は 10 文字以上必要です'
      );
    }
  }
}

ステップ 5:API エンドポイントの実装

typescript// Express での API エンドポイント実装
import express, {
  Request,
  Response,
  NextFunction,
} from 'express';

const router = express.Router();

// 問い合わせ作成エンドポイント
router.post(
  '/inquiries',
  async (
    req: Request,
    res: Response,
    next: NextFunction
  ) => {
    try {
      const useCase =
        DIContainer.getInstance().get<CreateInquiryUseCase>(
          'createInquiryUseCase'
        );

      const result = await useCase.execute({
        customerId: req.body.customerId,
        message: req.body.message,
      });

      res.status(201).json({
        success: true,
        data: result,
      });
    } catch (error) {
      next(error);
    }
  }
);

// エラーハンドリングミドルウェア
router.use(
  (
    error: Error,
    req: Request,
    res: Response,
    next: NextFunction
  ) => {
    if (error instanceof ValidationError) {
      res.status(400).json({
        success: false,
        error: {
          type: 'validation_error',
          message: error.message,
        },
      });
    } else if (error instanceof DomainError) {
      res.status(422).json({
        success: false,
        error: {
          type: 'domain_error',
          message: error.message,
        },
      });
    } else {
      console.error('Unexpected error:', error);
      res.status(500).json({
        success: false,
        error: {
          type: 'internal_error',
          message: 'システムエラーが発生しました',
        },
      });
    }
  }
);

export default router;

テスト戦略

DDD アーキテクチャでは、各層を独立してテストできます。

typescript// ドメインモデルの単体テスト
describe('Inquiry Entity', () => {
  describe('constructor', () => {
    it('正常な入力で問い合わせを作成できる', () => {
      const inquiry = new Inquiry({
        customerId: 'customer-123',
        message: 'この商品について質問があります',
        category: InquiryCategory.General,
        priority: Priority.Normal,
      });

      expect(inquiry.customerId).toBe('customer-123');
      expect(inquiry.status).toBe(InquiryStatus.Open);
    });

    it('メッセージが短すぎる場合はエラーを投げる', () => {
      expect(() => {
        new Inquiry({
          customerId: 'customer-123',
          message: '短い',
          category: InquiryCategory.General,
          priority: Priority.Normal,
        });
      }).toThrow(DomainError);
    });
  });

  describe('estimatedResponseTime', () => {
    it('高優先度の場合は 1 時間を返す', () => {
      const inquiry = new Inquiry({
        customerId: 'customer-123',
        message: '緊急の問い合わせです',
        category: InquiryCategory.General,
        priority: Priority.High,
      });

      expect(inquiry.estimatedResponseTime).toBe(1);
    });

    it('苦情の場合は応答時間が半分になる', () => {
      const inquiry = new Inquiry({
        customerId: 'customer-123',
        message: 'サービスに不満があります',
        category: InquiryCategory.Complaint,
        priority: Priority.Normal,
      });

      expect(inquiry.estimatedResponseTime).toBe(12); // 24 * 0.5
    });
  });
});
typescript// ユースケースのテスト(モックを使用)
describe('CreateInquiryUseCase', () => {
  let useCase: CreateInquiryUseCase;
  let mockDifyService: jest.Mocked<DifyService>;
  let mockRepository: jest.Mocked<InquiryRepository>;
  let mockEmailService: jest.Mocked<EmailService>;
  let mockEventBus: jest.Mocked<EventBus>;

  beforeEach(() => {
    mockDifyService = {
      classifyInquiry: jest.fn(),
      generateAutoReply: jest.fn(),
    } as any;

    mockRepository = {
      save: jest.fn(),
      findById: jest.fn(),
    } as any;

    mockEmailService = {
      sendAutoReply: jest.fn(),
    } as any;

    mockEventBus = {
      publish: jest.fn(),
    } as any;

    useCase = new CreateInquiryUseCase(
      mockDifyService,
      mockRepository,
      mockEmailService,
      mockEventBus
    );
  });

  it('正常に問い合わせを作成できる', async () => {
    // Dify サービスのモック設定
    mockDifyService.classifyInquiry.mockResolvedValue({
      category: InquiryCategory.TechnicalSupport,
      priority: Priority.High,
      confidence: 0.95,
      keywords: ['エラー', 'ログイン'],
    });

    mockDifyService.generateAutoReply.mockResolvedValue(
      'お問い合わせありがとうございます。担当者が確認いたします。'
    );

    // ユースケースの実行
    const result = await useCase.execute({
      customerId: 'customer-123',
      message: 'ログインできません。エラーが表示されます。',
    });

    // アサーション
    expect(result.category).toBe(
      InquiryCategory.TechnicalSupport
    );
    expect(result.priority).toBe(Priority.High);
    expect(mockRepository.save).toHaveBeenCalledTimes(1);
    expect(
      mockEmailService.sendAutoReply
    ).toHaveBeenCalledTimes(1);
    expect(mockEventBus.publish).toHaveBeenCalledTimes(1);
  });

  it('信頼度が低い場合はデフォルト値を使用する', async () => {
    // 低信頼度の分類結果
    mockDifyService.classifyInquiry.mockResolvedValue({
      category: InquiryCategory.TechnicalSupport,
      priority: Priority.High,
      confidence: 0.5, // 低信頼度
      keywords: [],
    });

    mockDifyService.generateAutoReply.mockResolvedValue(
      '自動返信'
    );

    const result = await useCase.execute({
      customerId: 'customer-123',
      message: '曖昧な問い合わせ内容',
    });

    // デフォルト値が使用されることを確認
    expect(result.category).toBe(InquiryCategory.General);
    expect(result.priority).toBe(Priority.Normal);
  });
});

実装のポイント整理

以下の表で、各層の責務と実装のポイントをまとめます。

#責務Dify との関係テスト方法
1ドメイン層ビジネスルール、制約、状態管理無関係(純粋なビジネスロジック)単体テスト
2アプリケーション層ユースケースの調整、Dify 呼び出しDify を利用するが依存しないモックを使った単体テスト
3インフラストラクチャ層Dify API 呼び出し、データ永続化Dify API を直接呼び出す統合テスト
4プレゼンテーション層HTTP リクエスト処理、レスポンス整形無関係E2E テスト

この設計により、Dify の機能を最大限活用しながら、ビジネスロジックの独立性を保つことができます。

まとめ

Dify を中心としたアーキテクチャにドメイン駆動設計を適用することで、以下のメリットが得られます。

主要なメリット

明確な責務分離

  • Dify は AI 機能に特化し、ビジネスロジックはアプリケーション側で管理する
  • 各層の責務が明確になり、コードの理解と保守が容易になる

高いテスタビリティ

  • ドメインモデルは Dify に依存しないため、単体テストが容易
  • モックを使ってユースケースを独立してテストできる

柔軟な変更対応

  • Dify のワークフローを変更してもドメインロジックへの影響を最小化
  • 将来的に Dify 以外の AI サービスへの移行も容易

チーム開発の効率化

  • フロントエンド、バックエンド、AI エンジニアの役割が明確
  • 並行開発がしやすくなる

設計のポイント

以下の原則を守ることで、Dify 中心の DDD アーキテクチャを成功させることができます。

mermaidflowchart LR
  subgraph principles["設計原則"]
    direction TB
    p1["1. 境界づけられた<br/>コンテキストの定義"]
    p2["2. ユースケースの<br/>適切な粒度"]
    p3["3. ドメインモデルの<br/>独立性"]
    p4["4. Dify フローの<br/>単一責任"]

    p1 --> p2
    p2 --> p3
    p3 --> p4
  end

  subgraph benefits["得られる効果"]
    direction TB
    b1["保守性向上"]
    b2["テスト容易性"]
    b3["拡張性"]
    b4["チーム開発効率化"]
  end

  p1 -.->|実現| b1
  p2 -.->|実現| b2
  p3 -.->|実現| b3
  p4 -.->|実現| b4

実践のステップ

Dify 中心の DDD アーキテクチャを導入する際は、以下のステップで進めるとスムーズです。

  1. ドメインの理解と境界づけ:ビジネス要件を分析し、Dify が担当する領域とアプリケーションが担当する領域を明確に区分する

  2. ドメインモデルの設計:コアとなるビジネスルールをエンティティや値オブジェクトとして表現する

  3. ユースケースの定義:システムが提供する機能を明確なユースケースとして定義し、それぞれの責務を明確化する

  4. Dify ワークフローの設計:各ワークフローが 1 つの明確な AI 機能を提供するように設計する

  5. インターフェースの定義:Dify との連携部分を抽象化し、テスト可能な構造にする

  6. 段階的な実装とテスト:各層を独立して実装・テストし、統合していく

今後の展望

Dify のようなノーコード・ローコード AI プラットフォームは今後も進化を続けるでしょう。適切なアーキテクチャ設計により、これらの進化を柔軟に取り込みながら、ビジネス価値を継続的に提供できるシステムを構築することができます。

ドメイン駆動設計の原則を守りつつ、Dify の強力な AI 機能を活用することで、開発速度と品質の両立が可能になりますね。

関連リンク