軽量DDDで業務システムを設計する——重厚なDDDとの違い

Alcogyでは業務WEBシステムのOEM開発において、ドメイン駆動設計(DDD)の考え方を取り入れています。ただし、書籍で語られるようなフルスケールの戦術的DDD——集約ルートの厳密な分離、リポジトリインターフェースの抽象化、ドメインイベントによる非同期連携——をそのまま適用することはほとんどありません。中小規模の業務システムにそこまでの重厚さを持ち込むと、開発コストが跳ね上がり、変更のたびに複数レイヤーを触る羽目になるからです。Alcogyではこれを「軽量DDD」と呼び、必要な部分だけを取り入れる方針を取っています。

なぜフルスケールのDDDを避けるのか

戦術的DDDの代表的なパターンであるリポジトリインターフェース(OrderRepositoryのような抽象を定義し、実装をDIで差し替える設計)は、DBを差し替える可能性がある大規模システムでは価値があります。しかし、AlcogyのOEM開発の多くはCloudflare D1を前提に一貫して設計しており、DBエンジンを途中で切り替える想定がありません。抽象化のためのコストが、得られるメリットを上回ってしまうケースがほとんどです。同様に、集約間の整合性をドメインイベントで保証する設計も、リクエスト単位で完結する業務システムでは過剰設計になりがちです。

軽量DDDで残しているもの

一方で、以下の3点は業務システムの複雑さを抑えるために必ず取り入れています。

  • ドメインモデルとDBスキーマの分離: DBの行データをそのままUIに渡さず、ビジネスルールを持つドメインオブジェクトに変換してから扱う
  • 値オブジェクトによるバリデーションの集約: 金額・ステータス・期間といった値の妥当性チェックを、呼び出し側に散らばらせず1箇所にまとめる
  • ユースケース単位でのアプリケーション層の分離: 「受注を確定する」「見積を承認する」といった業務操作を、Reactive UIのイベントハンドラや+page.server.tsのactionに直接書かず、ユースケース関数として切り出す
// domain/order.ts
export type OrderStatus = 'draft' | 'confirmed' | 'shipped';

export class Order {
  private constructor(
    public readonly id: string,
    public readonly customerId: string,
    public readonly amount: number,
    public readonly status: OrderStatus
  ) {}

  static create(customerId: string, amount: number): Order {
    if (amount <= 0) {
      throw new Error('amount must be positive');
    }
    return new Order(crypto.randomUUID(), customerId, amount, 'draft');
  }

  confirm(): Order {
    if (this.status !== 'draft') {
      throw new Error(`cannot confirm order in status: ${this.status}`);
    }
    return new Order(this.id, this.customerId, this.amount, 'confirmed');
  }
}
// application/confirmOrder.ts
import { db } from '../db/client';
import { orders } from '../db/schema';
import { eq } from 'drizzle-orm';
import { Order } from '../domain/order';

export async function confirmOrder(orderId: string) {
  const row = await db.query.orders.findFirst({ where: eq(orders.id, orderId) });
  if (!row) throw new Error('order not found');

  const order = new Order(row.id, row.customerId, row.amount, row.status);
  const confirmed = order.confirm();

  await db.update(orders).set({ status: confirmed.status }).where(eq(orders.id, orderId));
  return confirmed;
}

リポジトリの抽象化はせず、アプリケーション層から直接Drizzleのクエリを呼んでいますが、ドメインロジック(confirm()のような状態遷移の正当性チェック)はドメインオブジェクト側に閉じ込めています。この境界だけは崩さないようにしています。

レイヤーを増やしすぎない

Alcogyのアーキテクチャは基本的に「ルート(+page.server.ts)→アプリケーション層(ユースケース関数)→ドメイン層(値オブジェクト・エンティティ)→Drizzleスキーマ」という4層に収めています。インフラ層を独立させたクリーンアーキテクチャのような構成にすることもできますが、SvelteKitとCloudflareという技術スタックが固定されているOEM開発では、そこまでの抽象化は必要ないと判断しています。

軽量DDDの目的は、DDDの用語を正しく使うことではなく、ビジネスルールがUIやDBのコードに漏れ出さないようにすることです。プロジェクトの規模と将来の変更可能性を見極めながら、どこまで層を分けるかを都度判断しています。