ドメインとは何か
この部の 1 / 6 章 ・ 全体で 56 / 76 章 ・ 読了目安 45 分
- 同じ言葉が複数の意味を持つことに気づける
- 業務のルールをコードで表現できる
- ドメイン知識を獲得する具体的な手順を持つ
職場で毎日聞くのに、誰も説明してくれない言葉があります。
「そこはドメインの知識がないと判断できない」 「ドメインモデルとして正しくない」 「その処理はドメイン層に置くべき」
DNS・TCP・TLSで扱った DNS の「ドメイン名」とはまったく別の意味です。 この章では、この「ドメイン」を扱います。
新人とベテランの差が最も大きく出るのは、技術力ではなくここです。
ドメイン = そのソフトウェアが相手にしている現実
ドメイン(domain)とは、そのソフトウェアが対象にしている業務や領域そのものです。
| サービス | ドメイン | ドメインの中身 |
|---|---|---|
| EC サイト | 商取引 | 注文・在庫・配送・返品・請求 |
| 勤怠管理 | 労務 | 出退勤・休暇・残業・36協定 |
| 病院予約 | 医療事務 | 診療科・保険証・初診と再診 |
| 銀行 | 金融 | 口座・振替・与信・法令対応 |
プログラミングの知識には含まれていない、現実世界のルールです。 そして、ソフトウェアの価値はここにあります。
技術(HTTP・DB・Kubernetes) → どの会社に行っても同じ
ドメイン(業務のルール) → その会社・その事業に固有
チームで「この人に聞けば分かる」と言われている人は、 たいてい技術が飛び抜けているのではなく、ドメインを知っています。
- なぜこのテーブルにこのカラムがあるのか
- なぜこの処理だけ例外的なのか
- この変更が、どの業務を壊すのか
これは調べても出てこない情報で、時間をかけて獲得するしかありません。 だからこそ、早く獲得し始めた人が有利になります。
実例1: 「注文」とは何か
EC サイトを作るとします。「注文機能を作って」と言われました。
素直に考えると、こうなります。
orders テーブル: id, user_id, product_id, quantity, created_at
しかし業務の人に聞くと、こういう話が出てきます。
「カートに入れただけの状態と、注文は違います」 「注文が確定するのは決済が通った時点です」 「在庫を押さえるのは、決済の前です。押さえてから10分で解放します」 「出荷後のキャンセルは返品という別の扱いになります」 「後払いの場合、支払いは出荷の後です」
つまり「注文」は1つの行ではなく、状態を持ったプロセスでした。
[カート] → [在庫確保] → [決済待ち] → [注文確定] → [出荷準備] → [出荷済] → [受領]
│ │ │ │
↓ 10分で解放 ↓ 失敗 ↓ キャンセル ↓ ここから先は返品
ここで重要なのは、この図はコードを読んでも書けないということです。 書けるのは業務を知っている人だけです。
同じ「キャンセルする」でも、状態によってやることが全部違います。
| 状態 | 必要な処理 |
|---|---|
| 決済前 | 在庫を戻すだけ |
| 決済後・出荷前 | 在庫を戻す + 返金 |
| 出荷後 | 返品受付 + 到着確認 + 返金 + 送料の負担判定 |
「キャンセル機能を作って」を額面通りに受け取って
DELETE /orders/:id を作ると、会計が合わなくなります。
新人が実装で詰まる原因の多くは、技術ではなく 言葉を1つの意味だと思い込んだことにあります。
ユビキタス言語 — 同じものを同じ名前で呼ぶ
チーム全員(エンジニアも、業務の人も)が、 同じ言葉を同じ意味で使う。これをユビキタス言語と呼びます。
言葉が揃っていないと、こうなります。
実例2: 「ユーザー」が3つの意味を持っていた
あるチームで、こんな会話がありました。
企画「ユーザーが増えたので、ユーザー一覧に検索を付けたい」 エンジニアA「
usersテーブルですね」 エンジニアB「管理画面のユーザーですか? 購入者のことですか?」
調べると、users テーブルには次が混在していました。
- 会員登録した購入者
- ゲスト購入で自動生成されたレコード
- 管理画面のオペレーター
- 退会済みの人(
deleted_atが入っているだけ)
「有効なユーザー数」を出す SQL が、人によって違いました。 当然、報告される数字も違います。
言葉を分けると、こうなります。
| 呼び分けた名前 | 定義 |
|---|---|
| 会員(Member) | 登録済みで、退会していない人 |
| 購入者(Customer) | 1回以上注文を確定した人(ゲストを含む) |
| オペレーター(Operator) | 管理画面にログインできる人 |
言葉を分けた瞬間に、テーブル設計もコードも整理されます。 逆に言えば、設計が混乱している時は、たいてい言葉が混乱しています。
用語の曖昧さは、慣れた人ほど気づきません。 「みんな分かってるから」で流れてしまいます。
「これはどっちの意味ですか」と聞けるのは、入って間もない人の特権です。 そして、その質問はチームにとって価値があります(ドキュメントを書くの 「質問はドキュメントの穴の報告である」)。
コードにも同じ言葉を使う
会話で「注文確定」と呼んでいるものを、コードで submitOrder と書くと、
会話とコードの間で毎回翻訳が必要になります。
// 悪い: 業務で使われていない言葉
func (s *Service) UpdateOrderStatus(id string, status int) error
// 良い: 業務の言葉がそのまま出てくる
func (s *Service) ConfirmOrder(ctx context.Context, id OrderID) error
func (s *Service) CancelBeforeShipment(ctx context.Context, id OrderID) error
func (s *Service) AcceptReturn(ctx context.Context, id OrderID, reason ReturnReason) error下のコードは、業務の人が読んでも何をしているか分かります。 これは「読みやすいコード」の話ではなく、 仕様の誤りを business 側が発見できるという実利があります。
ドメインモデル — 業務のルールをコードで表す
業務のルールを、データ構造と型として表現したものがドメインモデルです。
エンティティと値オブジェクト
業務に出てくるものは、2種類に分けられます。
| エンティティ | 値オブジェクト | |
|---|---|---|
| 同一性 | ID で決まる | 値が同じなら同じもの |
| 変化 | 状態が変わる | 変わらない(作り直す) |
| 例 | 注文・会員・商品 | 金額・住所・メールアドレス・期間 |
注文 #1001 は、中身が変わっても #1001 のまま → エンティティ
1000円 は、どの 1000円 も同じ → 値オブジェクト
実例3: 金額を int で持つのをやめる
// よくある形
type Order struct {
Total int // 単位は? 税込み? 通貨は?
}このコードは、次の事故を防げません。
total := priceYen + shippingUSD // 通貨が違うのに足せてしまう
total := subtotal + taxRate // 金額と率を足しても、型は通る値オブジェクトにすると、業務のルールを型で守れます。
type Currency string
type Money struct {
amount int64 // 最小単位(円なら円、ドルならセント)。ビットとバイトのとおり整数で持つ
currency Currency
}
func NewMoney(amount int64, c Currency) (Money, error) {
if c == "" {
return Money{}, errors.New("通貨が指定されていません")
}
return Money{amount: amount, currency: c}, nil
}
// 通貨が違えば足せない。このルールがコードとして存在する
func (m Money) Add(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, fmt.Errorf("通貨が異なります: %s と %s", m.currency, other.currency)
}
return Money{amount: m.amount + other.amount, currency: m.currency}, nil
}TypeScript でも同じ考え方です。
// 生の string をどこにでも渡せてしまう状態をやめる
type Email = string & { readonly __brand: 'Email' };
export function toEmail(raw: string): Email {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw)) {
throw new Error(`メールアドレスの形式ではありません: ${raw}`);
}
return raw as Email;
}
// これで、検証を通っていない文字列は Email として渡せない
function sendInvite(to: Email): void { ... }- 不正な値が存在できない — 生成時に検証すれば、以降どこでも安全
- 取り違えられない —
UserIDとOrderIDがどちらもstringだと入れ替わる - ルールの置き場所ができる — 「通貨が違えば足せない」がここに書ける
すべてを値オブジェクトにする必要はありません。 間違えると被害が大きいもの(金額・ID・期間・メールアドレス)から始めます。
ルールを、置くべき場所に置く
データを持つだけの構造体と、処理を全部持つ巨大なサービスに分かれた状態を ドメイン貧血症(anemic domain model) と呼びます。
// ルールがサービス側に散らばっている
func (s *OrderService) Cancel(ctx context.Context, id string) error {
o, _ := s.repo.Find(ctx, id)
if o.Status == "shipped" { return errors.New("出荷済みはキャンセル不可") }
if o.Status == "cancelled" { return errors.New("すでにキャンセル済み") }
o.Status = "cancelled"
return s.repo.Save(ctx, o)
}同じ判定が、管理画面用のサービス、バッチ、API の3箇所にコピーされます。 そして1箇所だけ直され、挙動がずれます。
// ルールを注文自身が持つ
func (o *Order) Cancel() error {
switch o.status {
case StatusShipped:
return ErrAlreadyShipped // 返品の手続きが必要
case StatusCancelled:
return ErrAlreadyCancelled
}
o.status = StatusCancelled
return nil
}「この判定は、どこに書いてあるのが自然か」を考えてください。 注文がキャンセルできるかどうかは、注文自身が知っているべきことです。
境界づけられたコンテキスト — 同じ言葉が別の意味になる
ここが、マイクロサービスの分割と直結します。
実例4: 「商品」は1つではない
EC サイトの「商品」を、部署ごとに聞いてみます。
| 見る立場 | 「商品」に必要な情報 |
|---|---|
| 販売 | 名前・説明文・価格・画像・レビュー・おすすめ度 |
| 在庫 | SKU・倉庫ごとの数量・入荷予定・ロット |
| 配送 | 寸法・重量・温度帯・危険物区分 |
| 会計 | 原価・税区分・勘定科目 |
同じ「商品」という言葉ですが、必要な属性が全く違います。
これを1つの巨大な products テーブルにまとめると、
- カラムが50を超え、どれが誰のものか分からなくなる
- 販売チームの変更が、配送の処理を壊す
- 誰も消せないカラムが増えていく
そこで、文脈(コンテキスト)ごとに別のモデルとして扱います。
[販売コンテキスト] [在庫コンテキスト] [配送コンテキスト]
Product StockItem Package
- 名前・価格 - SKU・数量 - 寸法・重量
↑ 商品ID だけで対応づける ↑
コンテキストの境界が、サービスの境界になります(マイクロサービスの実務)。 「技術的にきれいだから分ける」のではなく、 言葉の意味が変わるところで分けるのが正しい分割です。
1つの業務処理を完了するのに、5つのサービスを跨いで 更新しなければならない設計になっていたら、境界が間違っています。
分散トランザクションが必要になり、整合性が壊れ、 デバッグはマイクロサービスの実務の分散トレース無しには不可能になります。
「一緒に変更されるものは、一緒に置く」が原則です。
ドメイン知識をどう獲得するか
新人が最短で立ち上がるための、具体的な手順です。
1. 用語集を自分で作る
入って最初の1週間で始めてください。
| 用語 | 定義 | 混同しやすいもの | 出典 |
|------|------|------------------|------|
| 会員 | 登録済みかつ退会していない人 | 購入者(ゲスト含む) | 田中さん 8/8 |
| 注文確定 | 決済が成功した時点 | 注文受付(カート投入) | 仕様書 p.3 |
| 在庫引当 | 決済前に数量を確保すること。10分で解放 | 出荷指示 | 在庫チームに確認 |
「聞いた日付と、誰に聞いたか」を残すのが要点です。 後で定義が変わった時に、経緯を追えます。
そして、この用語集は次に入る人が使えます。 新人にとって、最も早く出せる目に見える成果でもあります。
2. 画面と帳票を見る
業務の人が実際に見ている画面を見せてもらってください。 そこには、コードには現れない優先順位が表れています。
「この項目、毎日見てるんですか?」という質問から、 仕様の背景が出てくることがよくあります。
3. DB のテーブルとカラムを読む
-- コメントが残っていることがある
SHOW FULL COLUMNS FROM orders;不自然なカラムには、たいてい業務上の理由があります。
legacy_code is_special のようなカラムを見つけたら、
消す前に「これは何ですか」と聞いてください。
4. 業務の人に聞く時の質問
技術者が聞きがちな「どういう仕様ですか」は、答えにくい質問です。 代わりに、具体例で聞きます。
□ 「典型的な1件の流れを、実際の例で見せてもらえますか」
□ 「これが起きるのは、月に何回くらいですか」 ← 例外処理の優先度が分かる
□ 「これまでで一番困ったケースは何ですか」 ← 隠れた要件が出てくる
□ 「この場合はどうなりますか」(境界値を具体的にぶつける)
□ 「今は手作業で何をカバーしていますか」 ← 本当の要件はここにある
システムの穴は、人間の手作業で埋められています。
「月末に Excel で突き合わせています」「特定の顧客だけ電話で確認します」 ——こうした話は、仕様書には絶対に書かれていません。
そしてシステムを変更すると、その手作業が回らなくなります。 新機能を作る時、既存の手作業を壊していないかは必ず確認してください。
「退会したユーザーを一覧から除外してほしい」という依頼を受けました。まず何をしますか。
実務の落とし穴まとめ
- 言葉を1つの意味だと思い込む — 「キャンセル」も「ユーザー」も複数ある
- 業務の言葉とコードの言葉が違う — 毎回翻訳が必要になり、誤解が生まれる
- 金額や ID を生の
int/stringで持つ — 取り違えても型が通る - ルールがサービス層に散る — コピーされ、片方だけ直される
- 1つの巨大なモデルに全部詰める — 文脈が違うものが混ざり、誰も触れなくなる
- 一緒に変わるものを別サービスに分ける — 分散トランザクションで詰む
- 手作業でカバーされている部分を知らずに変更する — 業務が止まる
- 仕様を「どうなってますか」と抽象的に聞く — 具体例でぶつける
まとめ
- ドメイン = そのソフトウェアが対象にしている現実の業務。 技術と違って、その事業に固有で、調べても出てこない
- 同じ言葉を同じ意味で使う(ユビキタス言語)。 設計の混乱は、たいてい言葉の混乱として先に現れる
- 業務の言葉をそのままコードの名前にする
- エンティティ(ID で同一)と値オブジェクト(値で同一)を区別し、 間違えると痛いもの(金額・ID)から型にする
- ルールは、それを知っているべきものに持たせる
- 文脈が変われば同じ言葉でも別のモデル。 そこがサービスの境界になる
- 獲得の手順は、用語集を作る・画面を見る・具体例で聞く・手作業を聞く
公式ドキュメント
迷ったら一次情報に戻ってください。
| 対象 | リンク |
|---|---|
| DDD Reference(Evans・無料 PDF) | https://www.domainlanguage.com/ddd/reference/ |
| Martin Fowler: UbiquitousLanguage | https://martinfowler.com/bliki/UbiquitousLanguage.html |
| Martin Fowler: BoundedContext | https://martinfowler.com/bliki/BoundedContext.html |
章末問題
在庫管理サービスと販売サービスで、それぞれ「商品」を表すモデルを持っています。重複しているので1つに統合すべきでしょうか。
コードでは status カラムに 'submitted' が入る状態を、業務の人は「注文受付」と呼び、企画は「仮注文」と呼んでいます。どうすべきですか。
次の章では、この考え方を体系にした ドメイン駆動設計(DDD) を、 集約・リポジトリ・コンテキストマップまで降りて扱います。