私たちは日常の開発の中で、よく DDD という言葉を耳にしますが、DDD とは一体何なのでしょうか?
以前インターネット上にもいくつかの記事がありましたが、文章が長くて分かりづらいことが多いです。この記事では DDD について皆さんに分かりやすく紹介し、その正体をはっきりと理解してもらいたいと思います。
DDD とは何か
DDD(Domain-Driven Design、ドメイン駆動設計)とは、ビジネスドメインに焦点を当てて複雑なシステムを構築するソフトウェア開発手法です。中心となる考え方は、コード構造をビジネスの実際のニーズと深く結びつけることです。
一言で言えば、「コードでビジネスの本質を再現すること」、つまり単なる機能実現ではありません。
- 従来の開発:PRD(要件文書)を見ながら if-else を書いていく(データベース設計通りにコードを書く)
- DDD:ビジネス側と一緒にドメインモデルを構築し、コードはビジネスの鏡となる(ビジネスが変われば、コードも変わる)
従来の開発モデル:シンプルな登録の例
実際、こうした概念は読んだ直後でもすぐに忘れてしまいますよね?それでは、コードの例を見てみましょう。
たとえば、ユーザー登録機能を作るとします。ビジネスルールは以下の通りです:
- ユーザー名は一意でなければならない
- パスワードは複雑さの要件を満たす必要がある
- 登録後にログを記録する
従来の方法では、次のようなコードを素早く書くことができます:
@Controller
public class UserController {
public void register(String username, String password) {
// パスワードを検証
// ユーザー名を確認
// データベースに保存
// ログを記録
// 全てのロジックが一つに混在している
}
}
「すべてのコードがコントローラに集まってるなんてあり得ない、ちゃんとレイヤーに分けてるよ」と言う人もいるでしょう。例えば、controller、service、dao レイヤーに分けるなど。すると次のようなコードになります:
// サービス層:フロー制御のみ、ビジネスルールは各所に分散
public class UserService {
public void register(User user) {
// ルール1:ユーティリティクラスに記述
ValidationUtil.checkPassword(user.getPassword());
// ルール2:アノテーションで実装
if (userRepository.exists(user)) { ... }
// DAOへ直接渡す
userDao.save(user);
}
}
これでも実は、処理の流れは比較的はっきりしています。多くの人は「すでにレイヤー分けされていて、コードもきれいで分かりやすい。これが DDD だよね」と思うかもしれません。
レイヤー分け= DDD?
答えは「NO!」です。
上記のコードは確かにレイヤー分けされていて、構造も整っていますが、それだけでは DDD とは言えません。
実際、従来のレイヤー分けされたコードでは、User オブジェクトは単なるデータコンテナ(貧血モデル)であり、ビジネスロジックは外部に分解されています。DDD においては、一部のロジックを User ドメインオブジェクトに内包する(凝集する)ことができます。例えば、パスワードのルールチェックなどです。
この登録例での DDD の正しい姿(充血モデル)は以下のようになります:
// ドメインエンティティ:ビジネスロジックを内包
public class User {
public User(String username, String password) {
// パスワードのルールをコンストラクタに内包
if (!isValidPassword(password)) {
throw new InvalidPasswordException();
}
this.username = username;
this.password = encrypt(password);
}
// パスワード複雑性チェックはエンティティの責任
private boolean isValidPassword(String password) { ... }
}
このように、パスワードのチェック処理を User ドメインエンティティ内部に取り込んでいます。専門用語で言えば、ビジネスルールがドメインオブジェクト内部にカプセル化され、オブジェクトが単なる「データ袋」ではなくなった、ということです。
DDD の重要な設計
つまり DDD とは、一部のロジックをドメインオブジェクトに下ろす(内包する)ことなのでしょうか?
それだけではありません。
レイヤー分けに加えて、DDD のキーデザインは以下のようなパターンを通じて、ビジネスの表現を深化させます:
- 集約ルート(Aggregate Root)
- ドメインサービス vs アプリケーションサービス
- ドメインイベント(Domain Events)
集約ルート(Aggregate Root)
シナリオ:ユーザー(User)と配送先住所(Address)が関連付けられている場合
- 従来方式:User と Address を Service で個別に管理
- DDD 方式:User を集約ルートとし、Address の追加・削除を管理する
public class User {
private List<Address> addresses;
// 住所追加のロジックは集約ルートであるUserが制御
public void addAddress(Address address) {
if (addresses.size() >= 5) {
throw new AddressLimitExceededException();
}
addresses.add(address);
}
}
ドメインサービス vs アプリケーションサービス
- ドメインサービス:複数のエンティティを跨るビジネスロジックを処理(例:振込処理は 2 つの口座が関与)
- アプリケーションサービス:処理の流れを調整(例:ドメインサービス呼び出し+メッセージ送信)
// ドメインサービス:コアビジネスロジックを担当
public class TransferService {
public void transfer(Account from, Account to, Money amount) {
from.debit(amount); // 口座から引き落とし(Accountエンティティにロジックを内包)
to.credit(amount);
}
}
// アプリケーションサービス:フローを調整し、ビジネスルールは含まない
public class BankingAppService {
public void executeTransfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId);
Account to = accountRepository.findById(toId);
transferService.transfer(from, to, new Money(amount));
messageQueue.send(new TransferEvent(...)); // インフラ操作
ドメインイベント(Domain Events)
ビジネスの変化をイベントとして明示的に表現
例:ユーザー登録成功後に UserRegisteredEvent を発行
public class User {
public void register() {
// ...登録処理
this.events.add(new UserRegisteredEvent(this.id)); // ドメインイベントを記録
}
}
従来開発と DDD の違い
従来開発と DDD の違いを簡単にまとめましょう。
従来開発:
- ビジネスロジックの所在:Service、Util、Controller に分散
- モデルの役割:データコンテナ(貧血モデル)
- 技術実装の影響:データベース設計主導
DDD:
- ビジネスロジックの所在:ドメインエンティティ/ドメインサービスに内包
- モデルの役割:振る舞いを持つビジネスモデル(充血モデル)
- 技術実装の影響:ビジネス要求主導のテーブル設計
EC 注文処理における DDD の例
理解しやすくするために、DDD の実例を紹介します。少し喉を潤しましょう。
要件:
ユーザーが注文する際に、以下の処理が必要:
- 在庫確認
- クーポンの適用
- 実際の支払金額を計算
- 注文を生成
従来の書き方(貧血モデル)
// サービス層:ごちゃ混ぜ注文処理
public class OrderService {
@Autowired private InventoryDAO inventoryDAO;
@Autowired private CouponDAO couponDAO;
public Order createOrder(Long userId, List<ItemDTO> items, Long couponId) {
// 1. 在庫確認(Serviceに散在)
for (ItemDTO item : items) {
Integer stock = inventoryDAO.getStock(item.getSkuId());
if (item.getQuantity() > stock) {
throw new RuntimeException("在庫不足");
}
}
// 2. 合計金額を計算
BigDecimal total = items.stream()
.map(i -> i.getPrice().multiply(i.getQuantity()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 3. クーポンを適用(ユーティリティクラスにロジック)
if (couponId != null) {
Coupon coupon = couponDAO.getById(couponId);
total = CouponUtil.applyCoupon(coupon, total); // ロジックはUtilに隠れている
}
// 4. 注文を保存(純粋なデータ操作)
Order order = new Order();
order.setUserId(userId);
order.setTotalAmount(total);
orderDAO.save(order);
return order;
}
}
従来の問題点:
- 在庫チェックやクーポン処理が Service、Util、DAO に分散
- Order オブジェクトがデータコンテナに過ぎない
- 要件変更時、Service 層の「考古学」が必要
DDD の書き方(充血モデル):ロジックをドメインに集約
// 集約ルート:Order(コアロジックを担う)
public class Order {
private List<OrderItem> items;
private Coupon coupon;
private Money totalAmount;
// コンストラクタにビジネスロジックを内包
public Order(User user, List<OrderItem> items, Coupon coupon) {
// 1. 在庫チェック(ドメイン内のルール)
items.forEach(item -> item.checkStock());
// 2. 合計金額計算(値オブジェクトにロジック)
this.totalAmount = items.stream()
.map(OrderItem::subtotal)
.reduce(Money.ZERO, Money::add);
// 3. クーポン適用(ルールを内部に保持)
if (coupon != null) {
validateCoupon(coupon, user); // クーポン使用ルールを内包
this.totalAmount = coupon.applyDiscount(this.totalAmount);
}
}
// クーポン検証ロジック(責任の所在が明確)
private void validateCoupon(Coupon coupon, User user) {
if (!coupon.isValid() || !coupon.isApplicable(user)) {
throw new InvalidCouponException();
}
}
}
// ドメインサービス:注文処理フローを調整
public class OrderService {
public Order createOrder(User user, List<Item> items, Coupon coupon) {
Order order = new Order(user, convertItems(items), coupon);
orderRepository.save(order);
domainEventPublisher.publish(new OrderCreatedEvent(order)); // ドメインイベント発行
return order;
}
}
DDD に変えて得られるメリット:
- 在庫チェック:OrderItem 値オブジェクト内に封装
- クーポンロジック:Order エンティティ内部に集約
- 計算精度:Money 値オブジェクトにより保証
- ビジネス変更時はドメイン層だけ修正すれば OK
仮に新要件が出たとします:
「クーポンは“注文金額 100 円以上で 20 円引き”、かつ新規ユーザー限定にしたい」
従来の方法だと Service 層、Util クラスが影響を受けます:
-
CouponUtil.applyCoupon()
のロジックを変更 - Service 層で新規ユーザーかどうかのチェックを追加
一方、DDD ならドメイン層だけの修正で済みます:
-
Order.validateCoupon()
メソッドを修正するだけです
どんな場面で DDD を使うべき?
実際のところ、すべてのケースで DDD を使うべきなのでしょうか?それはやりすぎです。
- ✅ ビジネスが複雑(EC、金融、ERP など)
- ✅ 要件が頻繁に変わる(90%のインターネットサービス)
- ❌ 単純な CRUD(管理画面、データレポート)
この言葉はとても理にかなっています:
「ビジネスルールを変更するとき、コントローラや DAO を触らず、ドメイン層だけを修正すればよい」
それが DDD が本当に実現できている状態です。
私たちはLeapcell、バックエンド・プロジェクトのホスティングの最適解です。
Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:
複数言語サポート
- Node.js、Python、Go、Rustで開発できます。
無制限のプロジェクトデプロイ
- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。
比類のないコスト効率
- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。
洗練された開発者体験
- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。
簡単なスケーラビリティと高パフォーマンス
- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。
Xでフォローする:@LeapcellHQ