AI駆動開発で使っているバックエンドクラス設計
はじめに
AI 主導開発が主流となった現在、多くの開発者が Claude Code や GitHub Copilot などのAI ツールを活用したコーディング(バイブコーディング) を行っています。
しかし、「AI が強力だから設計力は不要」と考えていませんか?実際は逆です。AI に正しいガードレールを提供し、効率的な開発を進めるためには、適切なバックエンド設計パターンの理解がより重要になっています。
この記事で学べること:
- AI 時代に効果的な 3 つの設計パターン(実用的に厳選)
- 実際の TypeScript コード例による具体的な実装方法
- AI ツールに明確な指示を出すための設計技法
- 保守性と開発効率を両立するアーキテクチャ構築法
想定読者
- AI 主導開発を行っている中級バックエンドエンジニア
- バックエンド設計力を向上させたい開発者
- AI ツールの活用に課題を感じている方
なぜ AI 時代にバックエンド設計力が重要なのか
AI 主導開発の課題
AI ツールは確かに強力ですが、現状の AI では以下のような課題があります:
- 技術負債の蓄積: 短期的な解決策を提案しがちで、長期的な保守性を考慮しない
- アーキテクチャの理解不足: 局所的な最適化に集中し、全体設計への影響を考慮できない
- ドメイン知識の不足: ビジネスロジックの複雑さを理解せず、表面的な実装に留まる
このような課題を解決するために適切なガードレール(設計パターン)を引いてあげることが AI 駆動開発で最も重要なことだと著者は考えています。
設計パターンが AI に与える価値
適切な設計パターンを適用することで:
- AI への明確な指針: パターンに従った実装を AI に指示できる
- 品質の担保: 実証済みのパターンにより、バグや設計ミスを防げる
- チーム間の共通理解: 統一されたパターンで開発効率が向上
- 保守性の向上: 将来の変更要求に柔軟に対応できる
私は DDD でよく用いられる CQRS パターンと SOLID 原則の考えをいい感じに使った設計をしています。(やりすぎは返って毒になるので)
SOLID 原則とは?
SOLID 原則は、オブジェクト指向設計における 5 つの基本原則の頭文字を取ったものです。この記事では特に重要な 2 つの原則に焦点を当てます:
Single Responsibility Principle(単一責任原則)
「一つのクラスは一つの責任のみを持つべき」
❌ 悪い例:責任が混在
// UserManagerクラスが複数の責任を持っている
class UserManager {
async createUser(userData: UserData): Promise<void> {
// ❌ バリデーション責任
if (!userData.email.includes("@")) {
throw new Error("Invalid email");
}
// ❌ データベース操作責任
await this.database.save(userData);
// ❌ 通知責任
await this.emailService.sendWelcome(userData.email);
// ❌ ログ記録責任
console.log(`User created: ${userData.id}`);
}
}
✅ 良い例:責任を分離
// 各クラスが単一の責任を持つ
class UserService {
constructor(
private validator: UserValidator,
private repository: UserRepository,
private notificationService: NotificationService
) {}
async createUser(userData: UserData): Promise<void> {
this.validator.validate(userData);
const user = new User(userData);
await this.repository.save(user);
await this.notificationService.sendWelcomeEmail(userData.email);
}
}
Open/Closed Principle(開放閉鎖原則)
「拡張に対して開いており、変更に対して閉じている」
❌ 悪い例:変更が必要な設計
class OrderProcessor {
async processOrder(order: Order, paymentType: string): Promise<void> {
// ❌ 新しい支払い方法を追加するたびに既存コードを変更する必要がある
if (paymentType === "credit_card") {
await this.processCreditCard(order);
} else if (paymentType === "paypal") {
await this.processPayPal(order);
} else if (paymentType === "bank_transfer") {
await this.processBankTransfer(order);
}
// 新しい支払い方法が追加されるたびにelse ifが増える...
}
}
✅ 良い例:拡張可能な設計
// 支払い処理のインターフェース
interface PaymentProcessor {
process(order: Order): Promise<void>;
}
// 各支払い方法の実装(既存コードを変更せずに追加可能)
class CreditCardProcessor implements PaymentProcessor {
async process(order: Order): Promise<void> {
// クレジットカード処理
}
}
class PayPalProcessor implements PaymentProcessor {
async process(order: Order): Promise<void> {
// PayPal処理
}
}
// 新しい支払い方法(既存コードを変更せずに追加)
class CryptoPaymentProcessor implements PaymentProcessor {
async process(order: Order): Promise<void> {
// 暗号通貨処理
}
}
class OrderProcessor {
constructor(private paymentProcessor: PaymentProcessor) {}
async processOrder(order: Order): Promise<void> {
// 支払い方法に関係なく同じコード
await this.paymentProcessor.process(order);
}
}
AI 時代における SOLID 原則の価値
- AI への明確な指示: 「UserValidator クラスを実装してください」のように具体的な責任を指示可能
- 段階的な拡張: 既存コードを変更せずに新機能を AI に追加させられる
- テストの容易さ: 単一責任により、AI がテストコードを書きやすい
- デバッグの効率化: 問題の箇所を AI が特定しやすい
CQRS とは?
CQRS(Command Query Responsibility Segregation)は、データの書き込み処理(Command)と読み取り処理(Query)を分離する設計パターンです。
CQRS の基本概念
AI 駆動開発で CQRS を活用する際の実践的な考え方:
Command(書き込み処理)
// 「ユーザー作成のオーケストレーション」をAIに指示
class CreateUserCommand {
async execute(input) {
// 1. バリデーション → 2. 保存 → 3. 通知 の順序を管理
}
}
Query(読み取り処理)
// 「ユーザー情報の取得」をAIに指示
class GetUserQuery {
async execute(userId) {
// データ取得のみに専念
}
}
分離のメリット: AI に「書き込み用のクラス」「読み取り用のクラス」として明確に指示できる
Command の責任範囲
重要な原則: Command はビジネスロジックを実装せず、どのサービスをどの順序で呼び出すかという調整役(オーケストレーション)に徹する。
具体的な責任:
- 処理順序の管理: 「バリデーション → 保存 → 通知」のような流れを制御
- トランザクション境界: データ整合性を保つ範囲を明確化
- サービス呼び出し: 実際の処理は各 Service クラスに委譲
CQRS が AI 開発に与えるメリット
- 明確な責任分離: AI に「これはオーケストレーション」「これは読み取り処理」と明確に指示できる
- トランザクション境界の明確化: データ整合性を保つ範囲を AI が理解しやすい
- 複雑さの軽減: オーケストレーションと実際のロジックを分離することで、個々の処理が単純になる
- 段階的な実装: 各サービスを独立して開発・テスト可能
従来の CRUD vs CQRS
// ❌ 従来のCRUD(すべてが混在)
class UserService {
async updateUserProfile(userId: string, data: any) {
// トランザクション、ビジネスロジック、副作用が混在
const transaction = await this.db.beginTransaction();
try {
const user = await this.userRepo.findById(userId);
if (!user) throw new Error("User not found");
// 複雑なビジネスロジック
if (data.email && data.email !== user.email) {
await this.validateEmailUniqueness(data.email);
await this.emailVerificationService.sendVerification(data.email);
}
const updated = await this.userRepo.update(userId, data);
await this.auditService.log("user_updated", updated);
await this.cacheService.invalidate(`user:${userId}`);
await transaction.commit();
// 非同期処理
await this.emailService.sendUpdateNotification(updated);
return this.formatUserForResponse(updated);
} catch (error) {
await transaction.rollback();
throw error;
}
}
}
// ✅ CQRS(責任が明確に分離)
class UpdateUserProfileCommand {
constructor(
public readonly userId: string,
public readonly updates: Partial<UserProfile>,
private userService: UserService,
private auditService: AuditService,
private eventPublisher: EventPublisher
) {}
// オーケストレーション
async execute(): Promise<void> {
// 1. ビジネスロジックの実行(サービスに委譲)
const updateResult = await this.userService.updateProfile(
this.userId,
this.updates
);
// 2. 監査ログの記録
await this.auditService.log("user_profile_updated", {
userId: this.userId,
changes: updateResult.changes,
});
// 3. イベント発行(副作用処理)
await this.eventPublisher.publish(
new UserProfileUpdatedEvent(this.userId, updateResult.user)
);
}
}
class GetUserProfileQuery {
constructor(
public readonly userId: string,
private userRepository: UserRepository
) {}
// 読み取り専用、最適化に特化
async execute(): Promise<UserProfileDto | null> {
return await this.userRepository.findUserProfileById(this.userId);
}
}
実装方針:実用的なパターンの組み合わせ
ここまで紹介した設計パターンの中から、AI 駆動開発で特に効果的な組み合わせを選択します:
採用するパターン:
- 単一責任原則 + 開放閉鎖原則 + CQRS パターン
なぜこの 3 つなのか?
SOLID 原則全てを適用するのはオーバーエンジニアリングになる可能性があります。設計パターンはあくまで手段であり、目的はAI 駆動開発で開発効率を上げることです。
選択基準:
- 学習コストが低い: チームメンバーが理解しやすい
- AI との相性が良い: 明確で具体的な指示ができる
- 実装が簡単: 複雑すぎず、実際のプロジェクトで適用しやすい
- 実用的: 開発効率向上の効果が実感できる
この 3 つの組み合わせで、実用的かつ保守性の高いアーキテクチャを構築できます。
具体的な実装
ここまでで、AI 時代に有効な設計パターンの理論を学びました。ここからは実際のコード例を通して、これらのパターンをどう実装するかを見ていきましょう。
実装例として扱う機能: EC サイトのユーザー管理機能
- ユーザー作成(Command)
- ユーザー情報取得(Query)
- 名前重複チェック(Service)
- データベースアクセス(Repository)
各コンポーネントが単一責任原則に従い、CQRS パターンで書き込みと読み取りを分離し、将来の拡張(開放閉鎖原則)を考慮した設計になっています。
ディレクトリ構造
src/
├── application/ # アプリケーション層
│ ├── commands/ # Commands
│ │ ├── user/
│ │ │ ├── CreateUserCommand.ts
│ │ │ ├── UpdateUserCommand.ts
│ │ │ └── DeleteUserCommand.ts
│ │ └── order/
│ │ ├── CreateOrderCommand.ts
│ │ └── UpdateOrderStatusCommand.ts
│ ├── queries/ # Queries
│ │ ├── user/
│ │ │ ├── GetUserQuery.ts
│ │ │ └── GetUsersQuery.ts
│ │ └── order/
│ │ ├── GetOrderQuery.ts
│ │ └── GetOrderHistoryQuery.ts
│ └── services/ # Domain services
│ ├── UserService.ts
│ └── OrderService.ts
├── domain/ # ドメイン層
│ └── entities/ # エンティティ
│ ├── User.ts
│ └── Order.ts
├── infrastructure/ # インフラ層
│ ├── repositories/ # Repository implementations
│ │ ├── UserRepository.ts
│ │ └── OrderRepository.ts
│ └── database/
│ └── prisma.ts
└── presentation/ # プレゼンテーション層
└── controllers/
├── UserController.ts
└── OrderController.ts
Command(書き込み処理)
Command はオーケストレーションのみを担当し、ドメインロジックは Service クラスに委譲します。
✅ 良い例:オーケストレーションに専念
export class CreateUserCommand {
constructor(
private userService: UserService,
private transactionManager: TransactionManager
) {}
async execute(input: {
name: string;
email: string;
password: string;
}): Promise<string> {
// 1. 名前重複チェック
await this.userService.checkNameDuplicate(input.name);
// 2. ユーザー作成・保存
return await this.userService.createUser(input);
}
}
❌ 悪い例:ドメインロジックが混在
export class CreateUserCommand {
constructor(
public readonly name: string,
public readonly email: string,
public readonly password: string,
private userRepository: UserRepository,
private emailService: EmailService
) // 多数の依存関係...
{}
async execute(): Promise<string> {
// ❌ バリデーションロジックがCommandに混在
if (!this.email.includes("@")) {
throw new Error("Invalid email");
}
// ❌ ビジネスロジックがCommandに混在
const existingUser = await this.userRepository.findByEmail(this.email);
if (existingUser) {
throw new Error("Email already exists");
}
// ❌ 詳細な実装ロジックがCommandに混在
// const hashedPassword = await hashPassword(this.password);
// const user = new User(...);
// await this.userRepository.save(user);
// await this.emailService.sendWelcome(this.email);
return "user-id"; // 簡略化
}
}
オーケストレーション専念の利点:
- AI が理解しやすいシンプルな構造
- ドメインロジックの変更が Command に影響しない
- テストが簡単(Service 層のモックのみ)
- トランザクション境界も含めて Service で管理
Query(読み取り処理)
Query はデータ取得のみを担当し、ビジネスロジックは含めません。
✅ 良い例:データ取得に専念
export class GetUserQuery {
constructor(
public readonly userId: string,
private userRepository: UserRepository
) {}
async execute(): Promise<UserDto | null> {
// シンプルなデータ取得のみ
const user = await this.userRepository.findById(this.userId);
return user ? UserDto.fromEntity(user) : null;
}
}
export class SearchUsersQuery {
constructor(
public readonly keyword: string,
public readonly page: number,
private userRepository: UserRepository
) {}
async execute(): Promise<UserDto[]> {
// 検索ロジックはRepositoryに委譲
const users = await this.userRepository.searchByKeyword(
this.keyword,
this.page
);
return users.map((user) => UserDto.fromEntity(user));
// Repositoryが担当する処理:
// - 検索クエリの構築、ページネーション、結果の取得
}
}
❌ 悪い例:ビジネスロジックが混在
export class GetUsersQuery {
constructor(
public readonly filters: UserFilters,
private userRepository: UserRepository,
private cacheService: CacheService,
private analyticsService: AnalyticsService
) {}
async execute(): Promise<UserSearchResult> {
// ❌ キャッシュロジックがQueryに混在
const cacheKey = this.buildCacheKey();
let result = await this.cacheService.get(cacheKey);
if (!result) {
// ❌ 複雑な検索ロジックがQueryに混在
// const users = await this.userRepository.findMany(complexFilters);
// const processed = this.processResults(users);
// result = this.buildResponse(processed);
// await this.cacheService.set(cacheKey, result);
}
// ❌ 分析ロジックまで混在
await this.analyticsService.trackSearch(this.filters);
return result; // 簡略化
}
}
データ取得専念の利点:
- 高速(余計な処理がない)
- AI が理解しやすいシンプルな構造
- キャッシュ戦略はインフラ層で分離
- 副作用がないため安全
DTO(データ転送オブジェクト)
export class UserDto {
constructor(
public readonly id: string,
public readonly name: string,
public readonly email: string,
public readonly createdAt: Date
) {}
static fromEntity(user: User): UserDto {
return new UserDto(user.id, user.name, user.email, user.createdAt);
}
}
Repository(データアクセス層)
Repository はデータアクセスのみに特化し、Prisma クライアントを直接使用します。
export class UserRepository {
constructor(private prisma: PrismaClient) {}
async findById(id: string): Promise<User | null> {
const userData = await this.prisma.user.findUnique({ where: { id } });
return userData ? User.fromPrisma(userData) : null;
}
async findByName(name: string): Promise<User | null> {
const userData = await this.prisma.user.findFirst({ where: { name } });
return userData ? User.fromPrisma(userData) : null;
}
async findByEmail(email: string): Promise<User | null> {
const userData = await this.prisma.user.findUnique({ where: { email } });
return userData ? User.fromPrisma(userData) : null;
}
async save(user: User): Promise<void> {
await this.prisma.user.upsert({
where: { id: user.id },
create: user.toPrisma(),
update: user.toPrisma(),
});
}
}
Service(ドメインサービス)
Service はビジネスロジックを担当し、Command/Query から呼び出される実際の処理を実装します。
export class UserService {
constructor(
private userRepository: UserRepository,
private emailService: EmailService
) {}
async checkNameDuplicate(name: string): Promise<void> {
const existingUser = await this.userRepository.findByName(name);
if (existingUser) throw new Error("Name already exists");
}
async createUser(userData: {
name: string;
email: string;
password: string;
}): Promise<string> {
// ビジネスルール検証(メール重複チェック)
const existingUser = await this.userRepository.findByEmail(userData.email);
if (existingUser) throw new Error("Email already exists");
// ユーザー作成・保存
const hashedPassword = await hashPassword(userData.password);
const user = new User(
generateId(),
userData.name,
userData.email,
hashedPassword
);
await this.userRepository.save(user);
// 副作用処理
await this.emailService.sendWelcome(userData.email);
return user.id;
}
}
コントローラーでの統合
export class UserController {
async createUser(req: Request, res: Response) {
const command = new CreateUserCommand(
this.userService,
this.transactionManager
);
const userId = await command.execute({
name: req.body.name,
email: req.body.email,
password: req.body.password,
});
res.status(201).json({ userId });
}
}
まとめ
AI 主導開発時代において、バックエンド設計力は不要になるどころか、より重要性を増しています。
この記事で紹介したSingle Responsibility Principle、Open/Closed Principle、CQRS パターンの組み合わせにより、以下の利益を得ることができます:
実践的な効果
1. 開発効率の向上
- AI に明確な指示を出せるため、期待通りの実装を得やすい
- 既存コードを変更せずに機能追加できるため、バグのリスクが低い
- 単純な処理の組み合わせで複雑な機能を実現できる
2. 保守性の向上
- 責任が明確に分離されているため、問題の箇所を特定しやすい
- 各クラスを独立してテストできる
- 将来の要求変更に柔軟に対応できる
3. チーム開発の効率化
- パターンが統一されているため、チームメンバーが理解しやすい
- AI ツールの活用方法を標準化できる
- コードレビューが効率的に行える
AI 時代の開発における重要なポイント
- ガードレールとしての設計: AI に正しい方向性を与える適切な制約
- 実用主義の採用: オーバーエンジニアリングを避け、必要十分なパターンの選択
- 段階的な拡張: 小さな単位での機能追加による安全な成長
-
明確な責任分離:
- Command: オーケストレーション + トランザクション管理
- Service: 純粋なビジネスロジック
- Query: 読み取り最適化
- Repository: データアクセスのみ
- トランザクション境界の明確化: データ整合性を保つ範囲を AI が理解しやすく
適切な設計パターンを身につけることで、AI ツールをより効果的に活用し、保守性の高いバックエンドシステムを継続的に構築できるようになります。
技術は手段であり、目的は価値のあるソフトウェアを効率的に作ることです。AI 時代だからこそ、基本的な設計原則の重要性を再認識し、実践していきましょう。
Discussion