Interactive

アーキテクチャ

処理の複雑さや重要度に応じて設計パターンを選択することで、ClaudeCodeは適切な実装を生成できます。弊社では処理ごとに最適な設計レベルを判断し、過剰設計を避けながら必要な堅牢性を確保します。

弊社の推奨ルール

  • 処理ごとの判断 - 同一システム内でも処理により設計レベルを変える
  • 段階的な成長 - シンプルな実装から始めて必要に応じて拡張
  • 明確な判断基準 - 複雑さ・変更頻度・ビジネス重要度で判断
  • パターンの混在許可 - 同一コードベース内での異なるパターン共存を推奨
  • リファクタリング指針 - 2箇所以上で重複したら抽出、3つ以上の連携でService層

アーキテクチャレイヤーの分離

5つのレイヤー構成

弊社では処理を以下の5つのレイヤーに分離して実装します。

// 1. Interface層: HTTPリクエスト/レスポンスの処理
app.post('/users', async (c) => {
  const data = await c.req.json()
  const service = new UserService()
  const result = await service.execute({ data })
  return c.json(result)
})

// 2. Application層: ビジネスロジックの調整
export class UserService {
  execute(props: Props) {
    // 複数の処理を調整
  }
}

// 3. Domain層: ビジネスルールの実装
export class User {
  withName(name: string): User {
    // 不変条件の保証
  }
}

// 4. Infrastructure層: 外部リソースへのアクセス
export class StripeAdapter {
  constructor(apiKey: string) {
    // 外部APIとの通信
  }
}

// 5. Library層: 外部APIライブラリや複雑な計算モジュール
export class OpenAIClient {
  async generateText(prompt: string): Promise<string> {
    // 外部APIのラップ
  }
}

export class TaxCalculator {
  calculate(amount: number, rate: number): TaxResult {
    // 複雑な税金計算ロジック
  }
}

レイヤーごとの責務

Interface層

  • HTTPリクエスト/レスポンスの処理
  • 認証・認可の確認
  • バリデーション
  • エラーハンドリング

Application層

  • 複数のドメインオブジェクトの調整
  • トランザクション管理
  • 外部サービスとの連携

Domain層

  • ビジネスルールの実装
  • 不変条件の保証
  • エンティティ・値オブジェクトの定義

Infrastructure層

  • データベースアクセス(Repositoryパターン)
  • 外部APIとの通信
  • ファイルシステム操作
  • 集約ルートごとのRepository実装

Library層

  • 外部APIのラップライブラリ
  • 複雑な計算モジュール
  • 汎用的なユーティリティ
  • ビジネスロジックに依存しない共通処理

ClaudeCodeでの利用

Level 1: 直接実装の依頼

単純なCRUD処理で複雑なビジネスルールがない場合に使用

ユーザー一覧のAPIを作成して。PrismaでDBに直接アクセス

Level 2: Service層への分離依頼

複数の処理が関連する場合や、再利用が必要な場合に使用

ユーザー登録処理をUserServiceクラスに分離して。メール送信とDB登録を含む

Level 3: Domain層の導入依頼

複雑なビジネスルールや不変条件がある場合に使用

在庫管理のドメインモデルを作成して。在庫数の整合性チェックを含める

混在した実装の依頼

同一機能でも処理により設計レベルを変える依頼例

商品管理APIを作成して。参照系は直接DB、在庫更新はドメイン層経由で

Level 1ではなくLevel 2を使う

弊社では、2箇所以上で重複するロジックや、3つ以上の操作を連携させる場合はService層に分離します。シンプルな参照系はLevel 1、ビジネスロジックを含む処理はLevel 2以上を選択します。

直接実装

単純なCRUD処理でビジネスロジックがほぼない場合に使用します。

// API層で直接DBアクセス
app.get('/users', async (c) => {
  const users = await prisma.user.findMany({
    orderBy: { createdAt: 'desc' }
  })
  return c.json(users)
})

Service層への分離

複数の処理が絡む場合、2箇所以上で同じロジックが出現する場合にService層を使用します。

// Service層: 複数の操作を調整
type Props = {
  email: string
  name: string
  password: string
}

export class UserService {
  constructor(
    private readonly db = prisma,
    private readonly email = new EmailService()
  ) {}

  async execute(props: Props) {
    // メール重複チェック
    if (await this.db.user.findUnique({ where: { email: props.email }})) {
      throw new Error('メールアドレスが重複しています')
    }

    // ユーザー作成とメール送信
    const user = await this.db.user.create({ data: props })
    await this.email.sendWelcome(user.email)
    return user
  }
}

Domain層の導入

複雑なビジネスルール、不変条件の保証、整合性の維持が必要な場合にDomain層を導入します。

// Domain層: ビジネスルールをカプセル化
export class Stock {
  constructor(
    private readonly productId: string,
    private readonly quantity: number
  ) {
    if (quantity < 0) {
      throw new Error('在庫数は負の値にできません')
    }
    Object.freeze(this)
  }

  // 在庫減算ロジック
  withDecrease(amount: number): Stock {
    if (amount > this.quantity) {
      throw new Error('在庫不足です')
    }
    return new Stock(this.productId, this.quantity - amount)
  }
}

設計レベルの判断基準

複雑さによる判断

弊社では処理の複雑さを基準に設計レベルを選択します。

// Level 1: 単純な参照 - 直接DBアクセス
const users = await prisma.user.findMany()

// Level 2: 複数の操作 - Service層
const service = new UserService()
await service.createUserWithNotification(data)

// Level 3: 複雑なルール - Domain層
const stock = new Stock(productId, 100)
const updated = stock.withDecrease(orderQuantity)

変更頻度による判断

頻繁に変更されるビジネスルールはDomain層に集約します。

// 変更が少ない: Level 1または2で十分
app.post('/users', async (c) => {
  const data = await c.req.json()
  const user = await prisma.user.create({ data })
  return c.json(user)
})

// 頻繁に変更: Domain層に集約
export class PricingRule {
  calculateDiscount(order: Order): number {
    // キャンペーンルールをここに集約
    return this.applySeasonalDiscount(order.total)
  }
}

ビジネス重要度による判断

金銭や権限に関わる重要な処理は、必ずDomain層を経由します。

// 一般的な処理: Level 1または2
const posts = await prisma.post.findMany()

// 決済処理: 必ずDomain層
export class Payment {
  constructor(
    private readonly amount: number,
    private readonly currency: string
  ) {
    if (amount <= 0) {
      throw new Error('金額は正の値である必要があります')
    }
    Object.freeze(this)
  }

  canRefund(): boolean {
    // 返金ポリシーをDomain層で管理
    return this.amount < 10000
  }
}

各レイヤーの実装例

Interface層の実装

HTTPリクエストの処理とApplication層への委譲を担当します。

import { Hono } from 'hono'
import { getContext } from '@/lib/context'

const app = new Hono<{ Variables: Env }>()

// Interface層: リクエスト処理
app.post('/payments', async (c) => {
  const data = await c.req.json()
  const ctx = getContext<Env>()

  // Infrastructure層のインスタンス化
  const stripeAdapter = new StripeAdapter(ctx.var.stripeApiKey)

  // Application層の呼び出し
  const service = new PaymentService({ stripe: stripeAdapter })
  const result = await service.execute(data)

  return c.json(result)
})

Application層の実装

複数のドメインオブジェクトやInfrastructure層を調整します。

type Props = {
  amount: number
  currency: string
  customerId: string
}

type Deps = {
  stripe: StripeAdapter
  db: PrismaClient
}

export class PaymentService {
  constructor(
    private readonly deps: Deps = {
      stripe: new StripeAdapter(getContext<Env>().var.stripeApiKey),
      db: prisma
    }
  ) {}

  async execute(props: Props) {
    // Domain層の利用
    const payment = new Payment(props.amount, props.currency)

    if (!payment.isValid()) {
      throw new Error('無効な決済情報です')
    }

    // Infrastructure層の利用
    const intent = await this.deps.stripe.createPaymentIntent({
      amount: props.amount,
      currency: props.currency,
      customer: props.customerId
    })

    // DB保存
    await this.deps.db.payment.create({
      data: {
        intentId: intent.id,
        amount: props.amount,
        currency: props.currency,
        customerId: props.customerId
      }
    })

    return { paymentIntentId: intent.id }
  }
}

Domain層の実装

ビジネスルールと不変条件をカプセル化します。

export class Payment {
  constructor(
    private readonly amount: number,
    private readonly currency: string
  ) {
    if (amount <= 0) {
      throw new Error('金額は正の値である必要があります')
    }
    if (!this.isSupportedCurrency(currency)) {
      throw new Error('サポートされていない通貨です')
    }
    Object.freeze(this)
  }

  isValid(): boolean {
    return this.amount > 0 && this.amount <= 1000000
  }

  private isSupportedCurrency(currency: string): boolean {
    return ['JPY', 'USD', 'EUR'].includes(currency)
  }

  withFee(feeRate: number): Payment {
    const feeAmount = Math.floor(this.amount * feeRate)
    return new Payment(this.amount + feeAmount, this.currency)
  }
}

Infrastructure層の実装

外部サービスやデータベースとの通信を担当します。

import Stripe from 'stripe'

export class StripeAdapter {
  private readonly stripe: Stripe

  constructor(apiKey: string) {
    this.stripe = new Stripe(apiKey, {
      apiVersion: '2023-10-16'
    })
  }

  async createPaymentIntent(params: {
    amount: number
    currency: string
    customer: string
  }) {
    return await this.stripe.paymentIntents.create({
      amount: params.amount,
      currency: params.currency.toLowerCase(),
      customer: params.customer,
      automatic_payment_methods: {
        enabled: true
      }
    })
  }

  async refundPayment(paymentIntentId: string) {
    return await this.stripe.refunds.create({
      payment_intent: paymentIntentId
    })
  }
}

Library層の実装

外部APIのラップライブラリや複雑な計算モジュールを実装します。

// 外部APIライブラリ
import OpenAI from 'openai'

export class OpenAIClient {
  private readonly client: OpenAI

  constructor(apiKey: string) {
    this.client = new OpenAI({ apiKey })
  }

  async generateText(prompt: string): Promise<string> {
    const response = await this.client.chat.completions.create({
      model: 'gpt-4',
      messages: [{ role: 'user', content: prompt }],
      temperature: 0.7
    })
    return response.choices[0].message.content ?? ''
  }

  async analyzeImage(imageUrl: string): Promise<string> {
    const response = await this.client.chat.completions.create({
      model: 'gpt-4-vision-preview',
      messages: [{
        role: 'user',
        content: [{
          type: 'image_url',
          image_url: { url: imageUrl }
        }]
      }]
    })
    return response.choices[0].message.content ?? ''
  }
}

// 複雑な計算モジュール
type TaxResult = {
  subtotal: number
  taxAmount: number
  total: number
  breakdown: TaxBreakdown[]
}

type TaxBreakdown = {
  name: string
  rate: number
  amount: number
}

export class TaxCalculator {
  calculate(amount: number, prefecture: string): TaxResult {
    const nationalTax = amount * 0.1 // 消費税
    const localTax = this.getLocalTaxRate(prefecture) * amount

    // 軽減税率の適用
    const reducedRate = this.applyReducedRate(amount)

    return {
      subtotal: amount,
      taxAmount: nationalTax + localTax - reducedRate,
      total: amount + nationalTax + localTax - reducedRate,
      breakdown: [
        { name: '消費税', rate: 0.1, amount: nationalTax },
        { name: '地方税', rate: this.getLocalTaxRate(prefecture), amount: localTax },
        { name: '軽減税率', rate: 0, amount: -reducedRate }
      ]
    }
  }

  private getLocalTaxRate(prefecture: string): number {
    // 都道府県別の税率ロジック
    const rates: Record<string, number> = {
      '東京都': 0.004,
      '大阪府': 0.0037,
      // ...
    }
    return rates[prefecture] ?? 0.003
  }

  private applyReducedRate(amount: number): number {
    // 軽減税率の複雑な計算
    if (amount < 10000) return amount * 0.02
    if (amount < 50000) return amount * 0.015
    return amount * 0.01
  }
}

// 暗号化モジュール
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'

export class EncryptionModule {
  constructor(private readonly secretKey: string) {}

  encrypt(text: string): { encrypted: string; iv: string } {
    const iv = randomBytes(16)
    const cipher = createCipheriv(
      'aes-256-cbc',
      Buffer.from(this.secretKey),
      iv
    )

    let encrypted = cipher.update(text, 'utf8', 'hex')
    encrypted += cipher.final('hex')

    return {
      encrypted,
      iv: iv.toString('hex')
    }
  }

  decrypt(encrypted: string, iv: string): string {
    const decipher = createDecipheriv(
      'aes-256-cbc',
      Buffer.from(this.secretKey),
      Buffer.from(iv, 'hex')
    )

    let decrypted = decipher.update(encrypted, 'hex', 'utf8')
    decrypted += decipher.final('utf8')

    return decrypted
  }
}

同一システム内でのパターン混在

機能ごとの設計レベル分離

弊社では同一システム内でも機能ごとに異なる設計レベルを適用します。

// 同一システム内の異なる設計レベル

// Level 1: マスタメンテナンス
app.get('/categories', async (c) => {
  const categories = await prisma.category.findMany()
  return c.json(categories)
})

// Level 2: ユーザー管理
const userService = new UserService()
app.post('/users', async (c) => {
  const data = await c.req.json()
  const user = await userService.execute(data)
  return c.json(user)
})

// Level 3: 在庫管理
const stock = new Stock(productId, currentQuantity)
const updated = stock.withDecrease(orderQuantity)
await stockRepo.save(updated)

リファクタリングのタイミング

設計レベルの引き上げは必要に応じて段階的に行います。

// 初期実装: Level 1
app.post('/orders', async (c) => {
  const order = await prisma.order.create({ data })
  // 在庫更新ロジックを直接記述
  await prisma.product.update({
    where: { id: productId },
    data: { stock: { decrement: quantity }}
  })
  return c.json(order)
})

// 2箇所で重複 → Service層に抽出
type Props = {
  productId: string
  quantity: number
  userId: string
}

class OrderService {
  async execute(props: Props) {
    const order = await this.db.order.create({ data: props })
    await this.updateStock(props.productId, props.quantity)
    return order
  }
}

// 複雑なルールが追加 → Domain層を導入
const stock = await stockRepo.findByProductId(productId)
const updated = stock.withDecrease(quantity) // ビジネスルールをカプセル化
await stockRepo.save(updated)

依存性注入とテスト

コンストラクタデフォルト引数でのDI

弊社ではDIコンテナを使用せず、コンストラクタのデフォルト引数で依存性を注入します。

type Props = {
  email: string
  name: string
}

export class UserService {
  constructor(
    // テスト時にモックを注入可能
    private readonly deps = {
      db: prisma,
      email: new EmailService(),
      logger: new Logger()
    }
  ) {}

  async execute(props: Props) {
    const user = await this.deps.db.user.create({ data: props })
    await this.deps.email.sendWelcome(user.email)
    this.deps.logger.info(`User created: ${user.id}`)
    return user
  }
}

// テスト時
const mockDeps = {
  db: prismaMock,
  email: { sendWelcome: jest.fn() },
  logger: { info: jest.fn() }
}
const service = new UserService(mockDeps)
await service.execute({ email: 'test@example.com', name: 'Test' })

async hooksを使用したコンテキスト管理

リクエストコンテキスト(認証情報、DB接続等)をasync hooksで管理します。

import { AsyncLocalStorage } from 'async_hooks'

const requestContext = new AsyncLocalStorage<RequestContext>()

type RequestContext = {
  user: AuthUser
  db: PrismaClient
  requestId: string
}

// ミドルウェアでコンテキストを設定
app.use(async (c, next) => {
  const context = {
    user: await verifyAuth(c),
    db: new PrismaClient(),
    requestId: crypto.randomUUID()
  }
  await requestContext.run(context, next)
})

// どこからでもアクセス可能
export function getContext() {
  const ctx = requestContext.getStore()
  if (!ctx) throw new Error('Context not initialized')
  return ctx
}

Repositoryパターンと集約ルート

弊社では、Repositoryは常にEntityを受け取りEntityを返します。複雑な検索メソッドは定義せず、集約ルートの保護に必要なデータをJOINして取得します。

集約ルートの設計原則

同じデータベーステーブルに対しても、目的に応じて複数の集約ルートが存在します。各集約ルートは特定のビジネス不変条件を守るために必要なデータをJOINして保持します。

// 同じusersテービルに対する複数の集約ルート

// 認証用集約ルート
export class UserAuthEntity {
  constructor(props: {
    id: string
    email: string
    hashedPassword: string
    isActive: boolean
  }) {
    Object.assign(this, props)
    Object.freeze(this)
  }

  canLogin(): boolean {
    return this.isActive
  }
}

// プロフィール用集約ルート(フラットな構造)
export class UserProfileEntity {
  constructor(props: {
    id: string
    name: string
    avatar: string
    bio: string
    followersCount: number  // JOINしたデータをフラットに
  }) {
    Object.assign(this, props)
    Object.freeze(this)
  }
}

// 課金用集約ルート(フラットな構造)
export class UserBillingEntity {
  constructor(props: {
    id: string
    email: string
    stripeCustomerId: string
    subscriptionPlan: string      // JOINしたデータをフラットに
    subscriptionExpiresAt: Date
  }) {
    Object.assign(this, props)
    Object.freeze(this)
  }

  canUpgrade(): boolean {
    return this.subscriptionPlan === 'free'
  }
}

Repositoryの実装原則

Repositoryは集約ルートごとに作成し、必要なデータをJOINしてEntityを構築します。

// 認証用Repository
export class UserAuthRepository {
  constructor(private readonly prisma: PrismaClient) {}

  async findOne(where: { id?: string; email?: string }): Promise<UserAuthEntity | null> {
    const data = await this.prisma.user.findFirst({ where })
    if (!data) return null

    return new UserAuthEntity({
      id: data.id,
      email: data.email,
      hashedPassword: data.hashedPassword,
      isActive: data.isActive
    })
  }

  async findMany(where: {}): Promise<UserAuthEntity[]> {
    const dataList = await this.prisma.user.findMany({ where })

    return dataList.map(data => new UserAuthEntity({
      id: data.id,
      email: data.email,
      hashedPassword: data.hashedPassword,
      isActive: data.isActive
    }))
  }

  async write(entity: UserAuthEntity): Promise<void> {
    await this.prisma.user.upsert({
      where: { id: entity.id },
      create: entity,
      update: entity
    })
  }

  async delete(id: string): Promise<void> {
    await this.prisma.user.delete({ where: { id } })
  }
}

// プロフィール用Repository
export class UserProfileRepository {
  constructor(private readonly prisma: PrismaClient) {}

  async findOne(where: { id?: string }): Promise<UserProfileEntity | null> {
    const data = await this.prisma.user.findFirst({
      where,
      include: {
        profile: true,
        _count: { select: { followers: true } }
      }
    })
    if (!data) return null

    return new UserProfileEntity({
      id: data.id,
      name: data.name,
      avatar: data.profile?.avatar || '',
      bio: data.profile?.bio || '',
      followersCount: data._count.followers
    })
  }

  async write(entity: UserProfileEntity): Promise<void> {
    await this.prisma.user.upsert({
      where: { id: entity.id },
      create: entity,
      update: entity
    })
  }

  async delete(id: string): Promise<void> {
    await this.prisma.user.delete({ where: { id } })
  }
}

// 課金用Repository
export class UserBillingRepository {
  constructor(private readonly prisma: PrismaClient) {}

  async findOne(where: { id?: string }): Promise<UserBillingEntity | null> {
    const data = await this.prisma.user.findFirst({
      where,
      include: { subscription: true }
    })
    if (!data) return null

    return new UserBillingEntity({
      id: data.id,
      email: data.email,
      stripeCustomerId: data.stripeCustomerId,
      subscriptionPlan: data.subscription.plan,
      subscriptionExpiresAt: data.subscription.expiresAt
    })
  }

  async write(entity: UserBillingEntity): Promise<void> {
    await this.prisma.user.upsert({
      where: { id: entity.id },
      create: entity,
      update: entity
    })
  }

  async delete(id: string): Promise<void> {
    await this.prisma.user.delete({ where: { id } })
  }
}

Repositoryの禁止事項

// ❌ 禁止: 複雑な検索メソッド
export class UserRepository {
  // このようなメソッドは作らない
  async findActiveUsersWithSubscription() { /* ... */ }
  async findByEmailAndStatus(email: string, status: string) { /* ... */ }
  async searchByName(keyword: string) { /* ... */ }
}

// ✅ 正しい: 基本的なメソッドのみ
export class UserAuthRepository {
  async findOne(where: { id?: string; email?: string }): Promise<UserAuthEntity | null> { /* ... */ }
  async findMany(where: { isActive?: boolean }): Promise<UserAuthEntity[]> { /* ... */ }
  async write(entity: UserAuthEntity): Promise<void> { /* ... */ }
  async delete(id: string): Promise<void> { /* ... */ }
}

Application層での利用

export class UserAuthService {
  constructor(
    private readonly userAuthRepo: UserAuthRepository,
    private readonly userProfileRepo: UserProfileRepository
  ) {}

  async login(email: string, password: string) {
    // 認証用集約ルートを使用
    const authEntity = await this.userAuthRepo.findOne({ email })
    if (!authEntity || !authEntity.canLogin()) {
      throw new Error('ログインできません')
    }
    // ...
  }

  async getProfile(userId: string) {
    // プロフィール用集約ルートを使用
    const profileEntity = await this.userProfileRepo.findOne({ id: userId })
    if (!profileEntity) {
      throw new Error('ユーザーが見つかりません')
    }
    return profileEntity
  }
}

よくある問題

設計レベルの判断に迷う

複雑さ・変更頻度・重要度の3つの軸で判断します。迷ったらLevel 2から始めて必要に応じて調整します。

パターンが混在して統一性がない

同一システム内でのパターン混在は問題ありません。処理に適した設計を選ぶことが重要です。

過剰設計になる

必要なときに必要なだけの設計を適用します。将来の拡張性より現在のシンプルさを優先します。