プログラマのための IT 教科書

ドメインとは何か

この部の 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分で解放    ↓ 失敗       ↓ キャンセル  ↓ ここから先は返品

ここで重要なのは、この図はコードを読んでも書けないということです。 書けるのは業務を知っている人だけです。

「キャンセル」は1つではない

同じ「キャンセルする」でも、状態によってやることが全部違います。

状態必要な処理
決済前在庫を戻すだけ
決済後・出荷前在庫を戻す + 返金
出荷後返品受付 + 到着確認 + 返金 + 送料の負担判定

「キャンセル機能を作って」を額面通りに受け取って 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 { ... }
値オブジェクトの利点
  1. 不正な値が存在できない — 生成時に検証すれば、以降どこでも安全
  2. 取り違えられない — UserID と OrderID がどちらも string だと入れ替わる
  3. ルールの置き場所ができる — 「通貨が違えば足せない」がここに書ける

すべてを値オブジェクトにする必要はありません。 間違えると被害が大きいもの(金額・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. 言葉を1つの意味だと思い込む — 「キャンセル」も「ユーザー」も複数ある
  2. 業務の言葉とコードの言葉が違う — 毎回翻訳が必要になり、誤解が生まれる
  3. 金額や ID を生の int / string で持つ — 取り違えても型が通る
  4. ルールがサービス層に散る — コピーされ、片方だけ直される
  5. 1つの巨大なモデルに全部詰める — 文脈が違うものが混ざり、誰も触れなくなる
  6. 一緒に変わるものを別サービスに分ける — 分散トランザクションで詰む
  7. 手作業でカバーされている部分を知らずに変更する — 業務が止まる
  8. 仕様を「どうなってますか」と抽象的に聞く — 具体例でぶつける

まとめ

  • ドメイン = そのソフトウェアが対象にしている現実の業務。 技術と違って、その事業に固有で、調べても出てこない
  • 同じ言葉を同じ意味で使う(ユビキタス言語)。 設計の混乱は、たいてい言葉の混乱として先に現れる
  • 業務の言葉をそのままコードの名前にする
  • エンティティ(ID で同一)と値オブジェクト(値で同一)を区別し、 間違えると痛いもの(金額・ID)から型にする
  • ルールは、それを知っているべきものに持たせる
  • 文脈が変われば同じ言葉でも別のモデル。 そこがサービスの境界になる
  • 獲得の手順は、用語集を作る・画面を見る・具体例で聞く・手作業を聞く

公式ドキュメント

迷ったら一次情報に戻ってください。

対象リンク
DDD Reference(Evans・無料 PDF)https://www.domainlanguage.com/ddd/reference/
Martin Fowler: UbiquitousLanguagehttps://martinfowler.com/bliki/UbiquitousLanguage.html
Martin Fowler: BoundedContexthttps://martinfowler.com/bliki/BoundedContext.html

章末問題

在庫管理サービスと販売サービスで、それぞれ「商品」を表すモデルを持っています。重複しているので1つに統合すべきでしょうか。

コードでは status カラムに 'submitted' が入る状態を、業務の人は「注文受付」と呼び、企画は「仮注文」と呼んでいます。どうすべきですか。

次の章では、この考え方を体系にした ドメイン駆動設計(DDD) を、 集約・リポジトリ・コンテキストマップまで降りて扱います。

読み終わったら記録しておくと、目次で進み具合が分かります。