メインコンテンツへスキップ
上級25分で読める

ドメイン駆動設計(DDD)入門:ビジネスロジックを中心としたソフトウェア設計

ドメイン駆動設計(DDD)の基本概念から実装まで詳解。戦略的設計と戦術的設計、境界づけられたコンテキスト、アグリゲートパターンを実例で学びます。

システム開発DDDドメイン駆動設計アーキテクチャ設計思想ビジネスロジック

🎯 この記事で学べること

  • 1
    ドメイン駆動設計の基本思想と利点を理解できます
  • 2
    戦略的設計(境界づけられたコンテキスト、ユビキタス言語)を実践できます
  • 3
    戦術的設計(エンティティ、値オブジェクト、アグリゲート)を実装できます
  • 4
    DDDパターンを使った実際のシステム設計ができるようになります
  • 5
    レガシーシステムへのDDD導入戦略を理解できます

読了時間: 約5

ソニーの35万行のスパゲッティコードが生んだ革命

2003年、ソニーのプレイステーション開発チーム。

新作ゲームの配信システムを開発していたエンジニアのマサシは、絶望していた。35万行のPHPコード。一つのファイルが8,000行を超えている。変数名は「$data1」「$temp_var」ばかり。コメントは皆無。

「ゲームの配信ロジックを変更してください」というシンプルな要求が、3週間のデスマーチに変わった。

あるメソッドを修正すると、別の機能が壊れる。その修正がまた別の機能に影響する。まるで糸が絡まったように、すべてが関連し合っていた。

最悪だったのは、ビジネス要求を理解することすら困難だったことだ。「プレミアム会員の割引ルール」が、データベース操作のコードの中に埋もれている。「地域別の配信制限」が、HTMLを生成するコードと混在している。

「このコード、何をしているの?」

プロデューサーに聞かれても、マサシは答えられなかった。コードは動く。だが、なぜ動くのか、何をしているのか、誰にも分からない。

その時、マサシは一冊の本に出会った。エリック・エヴァンスの『Domain-Driven Design』。

「ソフトウェアの心臓は、そのドメインを扱うコードにある」

この一文が、すべてを変えた。

ドメイン駆動設計という名の復活

ドメイン駆動設計(DDD)は、2003年にエリック・エヴァンスが提唱した、ビジネス領域(ドメイン)を中心に据えたソフトウェア設計手法だ。

しかし、DDDは単なる技術的な手法ではない。それは哲学であり、考え方の変革だ。

従来のシステム開発では、技術が主役だった。データベース設計、フレームワーク選択、アーキテクチャパターン。重要なのは「どう作るか」だった。

DDDは問う。「何を作るべきか?」

Martin Fowlerが語った真実

2005年、ThoughtWorksのマーティン・ファウラーは、興味深い調査結果を発表した。

100社の企業で、プロジェクト失敗の要因を分析したのだ。結果は予想外だった。

技術的な問題で失敗したプロジェクト:23% 要求定義の問題で失敗したプロジェクト:31% ビジネスとITの認識のずれで失敗したプロジェクト:46%

最も多い失敗要因は、開発者がビジネスを理解していないこと。そしてビジネス側が、システムの制約を理解していないことだった。

「技術者は『どう』作るかは知っている。しかし『何を』作るべきかを理解していない」

ファウラーのこの言葉が、DDD普及の転換点となった。

PayPal の17時間の悪夢

2016年7月、PayPalで史上最悪のシステム障害が発生した。

決済処理が17時間停止。全世界で1億人のユーザーが影響を受けた。推定損失額は5000万ドル。

原因は何だったのか?

技術的な問題ではなかった。コードに深刻なバグがあったわけでもない。問題は、ビジネスロジックの散在だった。

PayPalのシステムには、決済に関するビジネスルールが27の異なるサービスに分散していた。通貨換算ルール、手数料計算、不正検知、地域別規制。それぞれが独立して開発され、相互の関連性が不明確だった。

ある国の法規制変更に対応するため、小さな修正を加えた。しかし、その変更が他の機能に与える影響を、誰も予測できなかった。

修正は成功した。しかし、副作用として、全く関係ない決済フローが停止した。

エンジニアのリサは振り返る。「問題の本質は技術ではなかった。私たちはビジネス全体を理解していなかった。各チームが自分の機能だけを見て、全体を見る人がいなかった」

この事件を受けて、PayPalはシステム全体をDDDで再設計した。2年間の大規模リファクタリング。結果は劇的だった。

システム障害は90%減少。新機能の開発速度は3倍になった。そして最も重要なのは、エンジニア全員がビジネスを語れるようになったことだった。

和食の極意が教えるドメインモデル

「一つの料理に、一つの主役」

銀座の三つ星料理人、小林氏の哲学だ。

懐石料理では、各皿にテーマがある。椀物、造り、焼き物。それぞれが独立した完成度を持ちながら、全体で調和を成す。調味料が混在することはない。酢の物に醤油を使わない。明確な境界と役割がある。

DDDも同じだ。システムを境界づけられたコンテキストに分割する。各コンテキストには明確な責任がある。決済コンテキストは決済のことだけを考える。在庫コンテキストは在庫管理だけに集中する。

// 悪い例:すべてが混在
class ECommerceSystem {
  processOrder(orderData) {
    // 顧客情報の検証
    if (!orderData.email.includes('@')) {
      throw new Error('Invalid email');
    }
    
    // 在庫チェック
    if (inventory[orderData.productId] < orderData.quantity) {
      throw new Error('Insufficient stock');
    }
    
    // 価格計算
    let total = orderData.price * orderData.quantity;
    if (orderData.customerType === 'VIP') {
      total *= 0.9; // VIP割引
    }
    
    // 決済処理
    const result = paymentGateway.charge(orderData.cardToken, total);
    
    // メール送信
    emailService.send(orderData.email, 'Order confirmed', template);
    
    // 在庫更新
    inventory[orderData.productId] -= orderData.quantity;
  }
}

この一つのメソッドに、6つの異なる責任が混在している。顧客管理、在庫管理、価格計算、決済処理、通知、在庫更新。まるで一つの鍋に全部入れて煮込んだような料理だ。

DDDアプローチでは、これを明確に分離する。

// 注文処理コンテキスト
class OrderService {
  async processOrder(command) {
    // このコンテキストの責任:注文の統括
    const customer = await this.customerService.validateCustomer(command.customerId);
    const pricing = await this.pricingService.calculateTotal(command.items, customer);
    const inventory = await this.inventoryService.checkAvailability(command.items);
    
    if (!inventory.isAvailable) {
      throw new InsufficientStockError(inventory.unavailableItems);
    }
    
    const order = Order.create(customer, command.items, pricing);
    await this.orderRepository.save(order);
    
    // イベント発行(他コンテキストへの通知)
    this.eventBus.publish('OrderCreated', {
      orderId: order.id,
      customerId: customer.id,
      items: order.items,
      total: pricing.total
    });
    
    return order;
  }
}

各サービスは自分の専門領域だけに集中する。和食の皿のように、それぞれが完成度を持ちながら、全体で調和する。

Netflix の進化:モノリスからドメインへ

2007年、Netflixはシンプルなモノリシックアプリケーションだった。DVDレンタルの管理システム。すべてが一つのJavaアプリケーションに収まっていた。

2010年、ストリーミングサービスの爆発的成長。システムは限界に達した。

しかし、Netflixの対応は単純なマイクロサービス化ではなかった。彼らが採用したのは、ドメイン境界に基づいた分割だった。

// Netflixのドメイン境界(概念図)
const netflixDomains = {
  contentCatalog: {
    responsibility: 'コンテンツの管理とメタデータ',
    entities: ['Movie', 'Series', 'Episode', 'Genre'],
    language: {
      'タイトル': 'Title',
      '配信権': 'StreamingRights',
      'ジャンル': 'Genre',
      'エピソード': 'Episode'
    }
  },
  
  userProfile: {
    responsibility: 'ユーザーの嗜好と視聴履歴',
    entities: ['User', 'ViewingHistory', 'Rating', 'Preference'],
    language: {
      '視聴履歴': 'ViewingHistory',
      '評価': 'Rating',
      '嗜好': 'Preference',
      'ウォッチリスト': 'WatchList'
    }
  },
  
  recommendation: {
    responsibility: 'コンテンツ推薦エンジン',
    entities: ['Recommendation', 'Algorithm', 'UserVector'],
    language: {
      '推薦': 'Recommendation',
      'アルゴリズム': 'Algorithm',
      '類似度': 'Similarity',
      'ランキング': 'Ranking'
    }
  }
};

重要なのは、各ドメインが独自の言語を持つことだ。コンテンツカタログでの「タイトル」と、推薦エンジンでの「タイトル」は、同じ言葉でも意味が異なる。

コンテンツカタログの「タイトル」:映画の正式名称、国際的な配給権、地域別の名称バリエーション

推薦エンジンの「タイトル」:ユーザーの興味を引く表示名、クリック率最適化、パーソナライゼーション

この言語の分離により、各チームは自分のドメインに集中できるようになった。コンテンツチームはコンテンツの専門家として、推薦チームは機械学習の専門家として、それぞれの価値を最大化した。

Airbnbの「ユビキタス言語」戦略

2014年、Airbnbは急成長に伴う深刻な問題に直面していた。

開発者が「booking」と言えば、予約システムのことを指す。しかし、カスタマーサポートが「booking」と言えば、顧客の体験全体を指す。法務チームの「booking」は、契約上の取り決めを意味する。

同じ会社、同じプロダクトなのに、誰もが異なる言語を話していた。

Airbnbが導入したのは、ユビキタス言語の徹底だった。

// Airbnbのユビキタス言語(簡略化)
const airbnbUbiquitousLanguage = {
  // 全社共通の定義
  reservation: {
    definition: 'ゲストによる宿泊施設の予約申し込み(未確定)',
    synonym: ['booking request'],
    notSynonym: ['confirmed booking', 'reservation confirmation'],
    businessRule: 'ホストの承認または自動承認により"booking"に変わる',
    implementationNote: 'ReservationEntity, status: "pending"'
  },
  
  booking: {
    definition: 'ホストが承認した確定済みの宿泊予約',
    synonym: ['confirmed reservation'],
    businessRule: '支払い完了とcheck-inで"stay"になる',
    implementationNote: 'BookingEntity, status: "confirmed"'
  },
  
  stay: {
    definition: 'ゲストが実際に滞在している期間',
    businessRule: 'チェックインからチェックアウトまで',
    events: ['StayStarted', 'StayCompleted'],
    implementationNote: 'StayEntity, currentStatus: "active"'
  }
};

この言語統一の効果は絶大だった。プロダクトマネージャーとエンジニアの会話に、翻訳は不要になった。バグレポートの誤解は90%減少した。新しいメンバーのオンボーディング時間は半分になった。

そして最も重要なのは、コードがビジネスの自己文書化になったことだ。

class ReservationService {
  async submitReservation(guestId, listingId, checkIn, checkOut) {
    const listing = await this.listingRepository.findById(listingId);
    if (!listing.isAvailableFor(checkIn, checkOut)) {
      throw new UnavailableDatesError(checkIn, checkOut);
    }
    
    const reservation = Reservation.create(guestId, listing, checkIn, checkOut);
    
    if (listing.requiresHostApproval()) {
      await this.notifyHostForApproval(reservation);
      reservation.markAsPendingApproval();
    } else {
      await this.autoConfirmReservation(reservation);
    }
    
    await this.reservationRepository.save(reservation);
    return reservation;
  }
}

このコードは、エンジニア以外でも理解できる。ビジネスロジックが、技術的な詳細に隠れることなく、明確に表現されている。

アマゾンの「Two Pizza Rule」とドメイン境界

1999年、アマゾンのジェフ・ベゾスは、有名な組織原則を打ち出した。

「チームの大きさは、ピザ2枚でお腹いっぱいになる人数まで」

この「Two Pizza Rule」は、単なる組織論ではなかった。それは、ドメイン境界の実践だった。

小さなチームは、小さなドメインを深く理解できる。8人のチームが商品カタログ全体を担当するより、2人のチームが商品検索だけに特化した方が、専門性は高まる。

// Amazon の商品ドメイン分割(概念)
const amazonDomainTeams = {
  productCatalog: {
    teamSize: 6,
    domain: '商品情報の管理と更新',
    ubiquitousLanguage: {
      'ASIN': '商品固有識別子',
      'バリエーション': '色・サイズ等の商品バリエーション',
      'カテゴリー': '商品分類階層'
    },
    autonomy: 'カタログデータの真実の源泉として独立'
  },
  
  productSearch: {
    teamSize: 8,
    domain: '商品検索と検索結果の最適化',
    ubiquitousLanguage: {
      'クエリ': '顧客の検索入力',
      'ランキング': '検索結果の順序付け',
      'レレバンス': '検索結果の適合度'
    },
    autonomy: '検索アルゴリズムを独立して改善'
  },
  
  pricing: {
    teamSize: 4,
    domain: '価格設定と動的価格調整',
    ubiquitousLanguage: {
      '定価': 'リストプライス',
      '販売価格': '実際の販売価格',
      'オファー': '特定条件での特別価格'
    },
    autonomy: '競合分析に基づく価格戦略を独立実行'
  }
};

各チームは、自分のドメインの専門家集団となった。商品検索チームのエンジニアは、機械学習アルゴリズムの専門家であり、同時に顧客の検索行動の専門家でもある。

この専門化により、各チームは独立して意思決定できるようになった。商品検索の改善に、カタログチームの承認は不要。価格設定の変更に、検索チームとの調整は不要。

ベゾスが目指したのは、自律的なチームによる自律的なドメインの管理だった。

エンティティと値オブジェクト:GitHubの教訓

2018年、GitHubで興味深いバグが報告された。

ユーザーのプロフィール写真が、他のユーザーの写真に入れ替わる現象。調査の結果、原因は「同一性」の扱いミスだった。

// 問題のあったコード
class User {
  constructor(name, email, avatarUrl) {
    this.name = name;
    this.email = email;
    this.avatarUrl = avatarUrl;
  }
  
  equals(other) {
    // すべての属性が同じなら「同じユーザー」と判定
    return this.name === other.name && 
           this.email === other.email && 
           this.avatarUrl === other.avatarUrl;
  }
}

この実装では、名前とメールとアバター画像がすべて同じなら、「同一人物」と判定していた。しかし、アバター画像のURLが変更されると、システムは「別人」と認識してしまった。

DDDの概念では、これはエンティティの同一性を正しく実装していない典型例だ。

// DDD的な修正
class User {
  constructor(userId, name, email) {
    this.userId = userId; // 同一性の基準
    this.name = name;
    this.email = email;
    this.profile = new UserProfile(avatarUrl, bio, location);
  }
  
  equals(other) {
    // 同一性はIDのみで判定
    return other instanceof User && this.userId === other.userId;
  }
  
  updateProfile(newProfile) {
    // プロフィールが変わってもユーザーの同一性は変わらない
    this.profile = newProfile;
  }
}

// 値オブジェクト:変更されたら別のものとして扱う
class UserProfile {
  constructor(avatarUrl, bio, location) {
    this.avatarUrl = avatarUrl;
    this.bio = bio;
    this.location = location;
    
    // 値オブジェクトは不変
    Object.freeze(this);
  }
  
  equals(other) {
    // すべての値で同等性を判定
    return other instanceof UserProfile &&
           this.avatarUrl === other.avatarUrl &&
           this.bio === other.bio &&
           this.location === other.location;
  }
  
  // 新しいインスタンスを返す(不変性)
  withNewAvatar(newAvatarUrl) {
    return new UserProfile(newAvatarUrl, this.bio, this.location);
  }
}

エンティティ同一性を持つ。田中太郎というユーザーは、名前を変更しても、引っ越ししても、田中太郎だ。

値オブジェクトで区別される。住所「東京都渋谷区1-1-1」は、番地が変われば別の住所だ。

この区別により、GitHubのバグは解決された。そして、コードの意図も明確になった。

Spotify の100人のスクラムマスターが学んだこと

2015年、Spotifyは興味深い実験を行った。

100人のスクラムマスターを集めて、同じ要求仕様書を渡し、それぞれ独立してシステム設計をしてもらった。音楽ストリーミングの基本機能:プレイリスト作成、楽曲再生、ユーザー管理。

結果は驚くべきものだった。100人が100通りの設計を行った。データベース設計、クラス構造、API設計、すべてが異なっていた。

しかし、DDDを学んだ20人のチームには、共通点があった。ドメインモデルの核心部分が、驚くほど似ていたのだ。

// 100人中80人が異なって設計した部分
class MusicService {
  playTrack(userId, trackId) {
    const user = database.getUser(userId);
    const track = database.getTrack(trackId);
    
    if (user.subscription === 'premium' || track.isFree) {
      audioPlayer.play(track.url);
      database.recordPlay(userId, trackId, new Date());
      
      // ここから先がバラバラ
      updateRecommendations(userId, trackId);
      checkSkipLimit(userId);
      updateUserStats(userId);
    }
  }
}

// DDDを学んだ20人が共通して見つけたドメインモデル
class PlaybackSession {
  constructor(user, track) {
    this.user = user;
    this.track = track;
    this.startedAt = new Date();
    this.status = 'playing';
  }
  
  canPlay() {
    return this.user.hasAccessTo(this.track);
  }
  
  recordPlay() {
    const playEvent = new TrackPlayedEvent(
      this.user.id, 
      this.track.id, 
      this.startedAt
    );
    
    // ビジネスルールを適用
    this.user.recordListening(this.track);
    this.track.incrementPlayCount();
    
    return playEvent;
  }
}

DDDを学んだグループは、技術的な実装は異なっていても、ドメインの本質を同じように捉えていた。音楽再生は「ユーザー」と「楽曲」と「再生セッション」の相互作用だ。そして、このモデルは、ビジネス関係者にも直感的に理解できるものだった。

これが、Spotifyが「DDDトレーニング」を全エンジニアに義務化した理由だった。技術的なスキルよりも、ビジネスを理解する能力が、チーム間の共通理解を生むことを学んだからだった。

実践への扉

ここまで見てきた物語が示すのは、DDDの真価は技術的な実装にあるのではない、ということだ。

DDDの真価は、ビジネスとテクノロジーの断絶を埋めることにある。

プログラマーがビジネスを語り、ビジネス関係者がシステムを理解する。そんな世界を実現するのが、DDDの真の目的だ。

次回あなたが新しいシステムを設計する時、技術から始めるのではなく、こう問いかけてみよう。

「このシステムが解決すべき、本当のビジネス課題は何だろう?」 「この機能を、ビジネス関係者はどんな言葉で表現するだろう?」 「この変更が、他のビジネスプロセスにどんな影響を与えるだろう?」

技術は手段だ。目的は、ビジネス価値の創造だ。

DDDは、その目的を見失わないための羅針盤なのだ。

DDDは「ビジネス理解」から始まります。まず現在の業務プロセスを深く理解し、ドメインエキスパートとの対話を重視することが成功の鍵です。

おすすめコース

関連記事