Application層
複数のドメインオブジェクトや外部サービスを調整することで、ClaudeCodeはビジネスユースケースを実現する処理フローを生成できます。弊社ではトランザクション境界とエラーハンドリングをApplication層に集約し、処理の流れを明確にします。
弊社の推奨ルール
- executeメソッドで統一 - すべてのServiceクラスはexecuteメソッドを持つ
- Props型で引数定義 - 複数の引数はPropsオブジェクトにまとめる
- 依存性注入の活用 - コンストラクタのデフォルト引数で依存関係を管理
- 単一責任の原則 - 1つのServiceは1つのユースケースのみを扱う
- エラーのユーザーフレンドリー化 - Infrastructure層のエラーをユーザー向けメッセージに変換
ClaudeCodeでの利用
基本的なServiceの作成依頼
ビジネスロジックの調整が必要な場合
ユーザー登録のServiceクラスを作成して。メール送信とDB登録を含む
複数ドメインの連携依頼
複数のドメインオブジェクトを調整する場合
注文処理のServiceを実装して。在庫確認、決済、配送手配を順番に実行
外部サービス連携の依頼
外部APIとドメインロジックを組み合わせる場合
決済ServiceでStripeと連携。決済後にDBとメール通知を更新
バッチ処理の実装依頼
定期的な処理や大量データ処理が必要な場合
月次請求のバッチServiceを作成。未払いユーザーに請求書を送信
単純な処理ではなくServiceパターンを使う
弊社では、2つ以上の処理を組み合わせる場合や、3つ以上のオブジェクトを調整する場合にService層を導入します。単一の処理は直接実装で十分です。
直接実装で十分な場合
単純なCRUD操作や単一の外部API呼び出しは、Service層を経由せず直接実装します。
// Interface層で直接処理
app.get('/users/:id', async (c) => {
const user = await prisma.user.findUnique({
where: { id: c.req.param('id') }
})
return c.json(user)
})
// 単純な外部API呼び出し
app.get('/weather', async (c) => {
const response = await fetch('https://api.weather.com/current')
return c.json(await response.json())
})
Service層が必要な場合
複数の処理を調整する必要がある場合、Service層でユースケースをカプセル化します。
// Application層: 複数の処理を調整
type Props = {
email: string
name: string
planId: string
}
export class UserRegistrationService {
constructor(
private readonly c: Context,
private readonly deps = {
db: prisma,
email: new EmailService(),
stripe: new StripeAdapter({ apiKey: c.env.stripeApiKey })
}
) {}
async execute(props: Props) {
// 1. メール重複チェック
const existing = await this.deps.db.user.findUnique({
where: { email: props.email }
})
if (existing) {
throw new BusinessError(
'このメールアドレスは既に使用されています',
'EMAIL_ALREADY_EXISTS',
400
)
}
try {
// 2. Stripeで顧客作成
const customerId = await this.deps.stripe.createCustomer({
email: props.email,
name: props.name
})
// 3. DBにユーザー作成
const user = await this.deps.db.user.create({
data: {
email: props.email,
name: props.name,
stripeCustomerId: customerId,
planId: props.planId
}
})
// 4. ウェルカムメール送信
await this.deps.email.sendWelcome(user.email)
return user
} catch (error) {
// Infrastructure層のエラーをユーザーフレンドリーに変換
if (error.code === 'card_declined') {
throw new BusinessError(
'カードが承認されませんでした',
'PAYMENT_DECLINED',
400
)
}
if (error.message?.includes('email')) {
throw new BusinessError(
'メール送信に失敗しました',
'EMAIL_SEND_FAILED',
500
)
}
// 予期しないエラーはそのまま投げる
throw error
}
}
}
エラーハンドリング
カスタムエラーの定義
ビジネスエラーを定義し、Infrastructure層の技術的エラーをユーザーフレンドリーなメッセージに変換します。
// ビジネスエラー
export class BusinessError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 400
) {
super(message)
this.name = 'BusinessError'
}
}
// 使用例
export class OrderService {
async execute(props: Props) {
const product = await this.deps.db.product.findUnique({
where: { id: props.productId }
})
if (!product) {
throw new BusinessError(
'商品が見つかりません',
'PRODUCT_NOT_FOUND',
404
)
}
if (product.stock < props.quantity) {
throw new BusinessError(
'在庫が不足しています',
'INSUFFICIENT_STOCK',
400
)
}
try {
// 決済処理
await this.deps.payment.processPayment({
amount: product.price * props.quantity,
customerId: props.customerId
})
} catch (error) {
// Infrastructure層のエラーをユーザー向けに変換
if (error.type === 'StripeCardError') {
throw new BusinessError(
'決済処理に失敗しました。カード情報を確認してください',
'PAYMENT_FAILED',
400
)
}
throw error
}
}
}
よくある問題
Serviceクラスが肥大化する
単一責任の原則に従い、1つのServiceは1つのユースケースのみを扱います。複雑な処理は複数のServiceに分割します。
依存関係の管理が複雑
コンストラクタのデフォルト引数パターンで依存性注入を実現し、テスト時はモックを注入します。
Infrastructure層のエラー処理
Infrastructure層から返された技術的エラーをApplication層でキャッチし、ユーザーフレンドリーなメッセージに変換してから再スローします。