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 | コアビジネスロジック | アプリケーション | 価格計算、在庫管理、権限チェック |
2 | AI による判断・生成 | Dify | テキスト生成、意図分類、要約 |
3 | データ永続化 | アプリケーション | データベース操作、トランザクション管理 |
4 | AI エージェント機能 | 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 アーキテクチャを導入する際は、以下のステップで進めるとスムーズです。
-
ドメインの理解と境界づけ:ビジネス要件を分析し、Dify が担当する領域とアプリケーションが担当する領域を明確に区分する
-
ドメインモデルの設計:コアとなるビジネスルールをエンティティや値オブジェクトとして表現する
-
ユースケースの定義:システムが提供する機能を明確なユースケースとして定義し、それぞれの責務を明確化する
-
Dify ワークフローの設計:各ワークフローが 1 つの明確な AI 機能を提供するように設計する
-
インターフェースの定義:Dify との連携部分を抽象化し、テスト可能な構造にする
-
段階的な実装とテスト:各層を独立して実装・テストし、統合していく
今後の展望
Dify のようなノーコード・ローコード AI プラットフォームは今後も進化を続けるでしょう。適切なアーキテクチャ設計により、これらの進化を柔軟に取り込みながら、ビジネス価値を継続的に提供できるシステムを構築することができます。
ドメイン駆動設計の原則を守りつつ、Dify の強力な AI 機能を活用することで、開発速度と品質の両立が可能になりますね。
関連リンク
- article
Dify 中心のドメイン駆動設計:ユースケースとフローの境界づけ
- article
Dify プロンプト設計チートシート:役割宣言・制約・出力フォーマットの定石
- article
Dify を macOS でローカル検証:Docker Compose で最短起動する手順
- article
Dify と LangGraph/LangChain を比較:表現力・保守性・学習コストのリアル
- article
Dify でジョブが止まる/再実行される問題の原因切り分けガイド
- article
Dify の内部アーキテクチャ超図解:エージェント・ワークフロー・データストアの関係
- article
ESLint シェアラブル設定の設計術:単一ソースで Web/Node/React をカバー
- article
Dify 中心のドメイン駆動設計:ユースケースとフローの境界づけ
- article
Python クリーンアーキテクチャ実践:依存逆転と境界インタフェースの具体化
- article
Cursor 前提の開発プロセス設計:要求 → 設計 → 実装 → 検証の短サイクル化
- article
Cline 中心の開発プロセス設計:要求 → 設計 → 実装 → 検証の最短動線
- article
Prisma Driver Adapters 導入手順:libSQL/Turso・Neon の最短セットアップ
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来