ドメイン駆動設計(DDD)入門:ビジネスロジックを中心としたソフトウェア設計
ドメイン駆動設計(DDD)の基本概念から実装まで詳解。戦略的設計と戦術的設計、境界づけられたコンテキスト、アグリゲートパターンを実例で学びます。
🎯 この記事で学べること
- 1ドメイン駆動設計の基本思想と利点を理解できます
- 2戦略的設計(境界づけられたコンテキスト、ユビキタス言語)を実践できます
- 3戦術的設計(エンティティ、値オブジェクト、アグリゲート)を実装できます
- 4DDDパターンを使った実際のシステム設計ができるようになります
- 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は「ビジネス理解」から始まります。まず現在の業務プロセスを深く理解し、ドメインエキスパートとの対話を重視することが成功の鍵です。
おすすめコース
関連記事
イベント駆動アーキテクチャ:非同期処理で実現する柔軟なシステム設計
イベント駆動アーキテクチャ(EDA)の基本概念から実装パターンまでを詳解。イベントソーシング、CQRSとの組み合わせ、実践的な設計指針を学びます。
システムアーキテクチャの基本パターン
システム設計における基本的なアーキテクチャパターンを分かりやすく解説。レイヤードアーキテクチャ、マイクロサービス、MVC、イベント駆動など、実践的なパターンとその適用場面を学びます。
マイクロサービスvsモノリシック:どちらを選ぶべきか
システムアーキテクチャの重要な選択肢であるマイクロサービスとモノリシックを比較。それぞれの特徴、メリット・デメリット、適用場面を実例を交えて解説します。