第13章:Domain Serviceは“最後の手段”🧩🛟✨
〜Serviceに逃げすぎない、でも“必要なときは堂々と使う”回〜
この章でできるようになること🎯💖
- 「それ、Entity/Value Objectに置けない?」をちゃんと判断できる👀✨
- Domain Serviceを“必要最小”で作れる(巨大Serviceにしない)🧼
- UseCase(Interactor)とDomain Serviceの違いを説明できる🗣️
- “Serviceに逃げて貧血ドメイン”になるのを防げる🩸🛡️
まず超大事な結論🌟
Domain Serviceは「置き場所がない重要なドメイン処理」のための最終手段だよ🧩 Eric Evans(DDD)でも「Entity/Value Objectの責務として自然じゃない重要処理なら、サービスとして切り出す」と説明されてるよ📌 (Domain Language)
でもね、ここで事故る人が多いの…🥺💦 **“なんでもService”**にすると、Domainがスカスカになって、結局変更に弱くなるんだ〜🫠
Domain Serviceってなに?🤔🧠
✅ ざっくり定義
- 複数のEntity/VOにまたがる
- だけど、どれか1つのEntityに置くと不自然(責務が歪む)
- ドメインの言葉(ユビキタス言語)で命名する
- 入出力はドメインオブジェクト中心(DTOじゃない)
- できるだけステートレス(状態を持って溜めない)
「サービス契約(どう振る舞うか)を定義し、ユビキタス言語で名前を付ける」って点も強調されてるよ📘 (Domain Language)
3秒でわかる!置き場所の判定フロー🧭✨
① まずEntityに置けない?👑
-
その処理が「そのEntityの自然な振る舞い」なら Entity
- 例:
Memo.Rename(title)、Account.Withdraw(money)
- 例:
② Value Objectに置けない?💎
-
値そのものの計算や正規化なら Value Object
- 例:
Money.Add()、Email.Normalize()
- 例:
③ それでも置けない(でもドメイン的に超重要)?🧩
-
複数Entityにまたがって、どっちにも所属しない感じなら Domain Service
- 例:A口座→B口座への送金(2つの口座が絡む)

④ もし「DB保存」「メール送信」「外部API」みたいな“段取り”なら?📦
- それは UseCase(Interactor) の仕事🎮
- Domain Serviceは“段取り屋”になっちゃダメ🙅♀️
(Domain ServiceとApplication Serviceの役割の分け方は、定番の整理でもこう説明されるよ)(Enterprise Craftsmanship)
よくある事故パターン🧨😇
❌ 事故1:CRUD Serviceになってる
UserService.CreateUser()MemoService.Save()→ だいたいUseCaseかRepository側の話だよ〜😵💫
❌ 事故2:なんでもServiceに寄せてEntityが空っぽ
- Entityが
Idとget/setしかない - ルールが全部
HogeServiceにいる → “貧血ドメイン”一直線🩸🫠
❌ 事故3:Domain Serviceが外部都合を知ってる
- HttpClient、DbContext、Controllerの型、DTO → それ入れた瞬間「中心が汚れる」やつ😱
ハンズオン🎮:Domain Serviceが必要になる典型例「送金」💸🏦
(メモ題材と別だけど、Domain Serviceが一番わかりやすい王道例だよ✨)
登場人物👥
Account(口座Entity)Money(金額VO)MoneyTransferService(送金Domain Service)
ポイントはこれ👇
- 引き出す/預けるは Accountの自然な振る舞い
- 「2つの口座を同時に扱う送金」は どっちの口座の責務でもない → だからDomain Serviceが“ちょうどいい”🧩✨
1) Value Object:Money💎
public readonly record struct Money(decimal Amount, string Currency)
{
public Money
{
if (Amount <= 0) throw new ArgumentOutOfRangeException(nameof(Amount), "金額は正の値にしてね");
if (string.IsNullOrWhiteSpace(Currency)) throw new ArgumentException("通貨が必要だよ", nameof(Currency));
}
public static void EnsureSameCurrency(Money a, Money b)
{
if (!string.Equals(a.Currency, b.Currency, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("通貨が違うと送金できないよ");
}
}
2) Entity:Account👑
public sealed class Account
{
public Guid Id { get; }
public Money Balance { get; private set; }
public Account(Guid id, Money initialBalance)
{
Id = id;
Balance = initialBalance;
}
public bool CanWithdraw(Money money)
{
Money.EnsureSameCurrency(Balance, money);
return Balance.Amount >= money.Amount;
}
public void Withdraw(Money money)
{
Money.EnsureSameCurrency(Balance, money);
if (!CanWithdraw(money)) throw new InvalidOperationException("残高が足りないよ");
Balance = new Money(Balance.Amount - money.Amount, Balance.Currency);
}
public void Deposit(Money money)
{
Money.EnsureSameCurrency(Balance, money);
Balance = new Money(Balance.Amount + money.Amount, Balance.Currency);
}
}
3) Domain Service:MoneyTransferService🧩
public interface IMoneyTransferService
{
void Transfer(Account from, Account to, Money amount);
}
public sealed class MoneyTransferService : IMoneyTransferService
{
public void Transfer(Account from, Account to, Money amount)
{
if (from.Id == to.Id) throw new InvalidOperationException("同じ口座には送金できないよ");
// ここが “複数Entityにまたがるドメインルール” の置き場所✨
Money.EnsureSameCurrency(from.Balance, amount);
Money.EnsureSameCurrency(to.Balance, amount);
from.Withdraw(amount);
to.Deposit(amount);
}
}
✅ このService、えらいところ👇
- DB保存しない🗄️❌
- HTTPも知らない🌐❌
- DTOも知らない🍱❌
- 口座とお金(ドメイン)だけで完結👑💎
じゃあUseCase(Interactor)は何するの?🎮🧵
UseCaseは「段取り係」だよ〜📋✨
- 口座をRepositoryから取ってくる
- Domain Serviceを呼ぶ
- 保存する
- 成功/失敗をPresenterへ渡す
(Domain ServiceとUseCaseの役割をちゃんと分けるのがコツだよ🧼)
public sealed class TransferMoneyInteractor
{
private readonly IAccountRepository _repo;
private readonly IMoneyTransferService _transfer;
public TransferMoneyInteractor(IAccountRepository repo, IMoneyTransferService transfer)
{
_repo = repo;
_transfer = transfer;
}
public void Handle(Guid fromId, Guid toId, Money amount)
{
var from = _repo.Get(fromId);
var to = _repo.Get(toId);
_transfer.Transfer(from, to, amount);
_repo.Save(from);
_repo.Save(to);
}
}
ミニ課題💪✨(“Serviceに逃げない筋トレ”)
課題1:置き場所クイズ🧠🎯
次の処理、どこに置く?(Entity / VO / Domain Service / UseCase)
メール送信して完了通知する📧Moneyの通貨が一致するか検証する💱2つのAccount間で送金する💸DBからAccountを検索して取ってくる🗄️Memoのタイトル文字数を制限する✍️
答え👇
- UseCase(段取り)
- VO
- Domain Service
- UseCase(+Repository)
- Entity か VO(TitleをVOにするならVOが強い💎)
AI活用🤖✨(Copilot / Codex向けプロンプト例)
- 「この処理、Entityに置くべき?Domain Serviceに置くべき?理由もセットで提案して」🧠
- 「Domain Serviceが肥大化しそう。責務分割案を3つ出して」🧩
- 「UseCaseとDomain Serviceの責務が混ざってないかレビューして、混ざってたら直して」🧼
- 「“貧血ドメイン”っぽい匂いがする箇所を指摘して、Entityに戻すリファクタ案ちょうだい」🩸
まとめ🎀✅
- まずEntity/VOに置けないか粘る👑💎
- それでもダメで、複数Entityにまたがる重要ルールならDomain Service🧩
- Domain Serviceは段取りをしない(保存・通知・外部呼び出しはUseCaseへ)🎮
- “なんでもService”にすると設計が崩れるから、最後の手段として丁寧に🛟✨
ちいさな最新メモ🆕📌
2026年1月時点では、.NET 10.0系の更新(1/13付など)や、.NET 8/9/10の月例アップデート情報がMicrosoftから出てるよ🧰✨ (Microsoft for Developers)
次は「第13章のミニ課題」を、あなたの題材(メモアプリ)に寄せて “Domain Serviceが本当に必要になる場面” を一緒に作っちゃう?📝💖