1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DDD × TDD × スキーマ駆動開発を “ちゃんと” 組み合わせると、何が嬉しいのか

Last updated at Posted at 2025-12-07

はじめに

「DDD とか TDD とかスキーマ駆動開発とか、言葉だけグルグル回っていて、
実際どんな世界が見えるのかはイマイチ掴めていない…」

こう感じている方は多いのではないでしょうか。

本記事では、DDD・TDD・スキーマ駆動開発をセットで扱ったときに、開発現場で何が嬉しいのか を、まずは「全体像レベル」でまとめます。

細かいクラス設計や、完璧な流儀の話ではなく

  • 「3つを組み合わせると、日々の開発体験がどう変わるのか」
  • 「どんな線でつなぐと気持ちよく設計できるのか」

といった 世界観と流れ にフォーカスします。

この記事のゴール

この記事を読み終えると、読者の方は次のような状態になることを目指します。

  • DDD / TDD / スキーマ駆動開発が、バラバラのキーワードではなく「1本の流れ」としてイメージできる
  • 「仕様 → スキーマ → モデル → ユースケース → テスト」という線が、なぜ気持ちよいのか を言語化できる
  • 「いきなり全部は無理でも、ここから取り入れてみよう」という 最初の一歩 を決められる

※コード例は TypeScript で書いていますが、言いたいことは言語に依存しないようにしています。

こういうモヤモヤ、ありませんか?

Webアプリ・業務アプリを作っていて、例えばこんな状況に覚えがないでしょうか。

  • API仕様が決まらないので、フロントエンドが待ちぼうけ
  • バックエンドの人と話すたびに仕様が変わる
  • 画面・API・DBで、同じビジネスルールがコピペ状態
  • テストは「とりあえずE2Eだけ」なので、リファクタが怖い
  • ディレクトリ構成は “なんとなく層っぽい” けど、結局どこに何を書けばいいか分からない

一言でいうと、

仕様・モデル・テスト・API がバラバラの方向を向いている

状態です。

ここに DDD × TDD × スキーマ駆動開発 をバラバラではなく 「セットで」 入れると、見える世界がかなり変わります。

まずは3つのキーワードを、ざっくり1行ずつ

DDD(ドメイン駆動設計)

ビジネスの「言葉」と「ルール」をコードの構造(クラスや関数、モジュール境界)にちゃんと写す。

  • ユーザーや企画者が使う言葉を ユビキタス言語 として定義する
  • Order, Reservation, DateRange など、「意味のある型」を作る
  • 「どの層にどんなロジックを書くか」をはっきり決める

TDD(テスト駆動開発)

「こう動いてほしい」をテストで先に書き、それを満たすようにコードを育てていく開発スタイル。

  • 仕様の穴や矛盾に 実装前に 気づける
  • テストが「将来の自分・チームへのドキュメント」になる
  • リファクタしても、テストが通っていればひとまず安心できる

スキーマ駆動開発(OpenAPI など)

API やデータ構造の “設計図(スキーマ)” を先に書き、それを元にクライアント・サーバー・モックを機械的に作っていくスタイル。

  • OpenAPI / JSON Schema / GraphQL SDL などを使う
  • スキーマから型定義・クライアントコードを自動生成する
  • モックサーバーもスキーマから起こせば、バックエンド未実装でもフロントが進められる

よくある失敗パターン:バラバラに導入してしまう

正直なところ、これらを 単体で つまみ食いすると、次のような状態になりがちです。

  • 「DDDっぽい層の名前」だけ増えたコードベース
  • テストはあるけど、ユースケースレベルの意図は曖昧
  • OpenAPIはあるけど、誰も見ていない “飾りのドキュメント”

つまり、

キーワードは導入したのに、日々の開発体験が大して変わっていない

という状態です。

これ、わりと “DDD・TDD・スキーマ駆動あるある” ではないでしょうか。

3つをセットで考えると「線」が1本通る

ここからが本題です。

抽象的なつながり方

3つをそれぞれバラバラではなく、「流れ」としてつなげると、こんなイメージになります。

  • スキーマ駆動開発 で「外との契約」を先に決める
  • DDD で「中のルールとモデル」を整理する
  • TDD でその接続と振る舞いをテストで固定する

この3つを 順番のある流れ としてつなぐと、

仕様 → スキーマ → モデル → ユースケース → テスト

という 1本の線 が通ります。

一言でいうと

「どこに何を書くか」が決まり、仕様変更にビクビクしない設計が手に入る。

この線が通ると、
「この話はどこに書くべきか?」という迷いが減り、
レビューやリファクタのコストも下がっていきます。

ちょっとだけ具体化してみる(予約機能の例)

例えば「何かの予約機能」を作るとします。

1. 会話から「言葉」を拾う(DDDの入口)

ビジネス側との会話の中で、こんな単語が出てきたとします。

  • 予約(Reservation)
  • 枠(Slot)
  • 利用者(User)
  • 利用可能期間(AvailableRange)

ここでやるのは、次のような 言葉の整理 です。

  • 予約 は違う概念ですよね?」
  • 「キャンセルされた予約はどう扱うべきですか?」
  • 「1つの枠に複数人予約できますか?」

この整理結果が、そのまま Reservation, Slot, User, AvailableRange
といった モデルの候補 になります。

2. 「外との契約」をスキーマで固める(スキーマ駆動)

次に、その言葉を使って APIのスキーマ(OpenAPIなど) を書きます。

ざっくりイメージだけ書くと:

paths:
  /reservations:
    post:
      summary: 予約を作成する
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateReservationRequest'
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReservationResource'

ここで重要なのは次の2点です。

  • エンドポイントやフィールド名に、さっき整理した ユビキタス言語 をそのまま使うこと
  • 「API のための言葉」を新たにでっち上げないこと

これにより、外部インターフェース(契約) がひとまず固まります。

3. スキーマから型やクライアントを機械的に生やす

OpenAPI が書けたら、そこから TypeScript の型やクライアントを生成します。

// 例: OpenAPI Generator が吐いた型
export interface ReservationResource {
  id: string;
  slotId: string;
  userId: string;
  status: "PENDING" | "CONFIRMED" | "CANCELLED";
  reservedAt: string; // ISO文字列
}

ここでのポイントは、

  • フロントエンド側が 「何が返ってくるか」 を型として持てる
  • APIクライアントを手書きしなくてよいので、バグの温床が1つ消える

という点です。

4. 「中身のルール」は Domain モデルとして表現する(DDD)

一方で、アプリ内部では ReservationReservationStatus などのドメインモデル を定義します。

// 値オブジェクト的な Status
export class ReservationStatus {
  private constructor(
    private readonly value: "PENDING" | "CONFIRMED" | "CANCELLED",
  ) {}

  static pending() {
    return new ReservationStatus("PENDING");
  }
  static confirmed() {
    return new ReservationStatus("CONFIRMED");
  }
  static cancelled() {
    return new ReservationStatus("CANCELLED");
  }

  canCancel(): boolean {
    return this.value === "CONFIRMED" || this.value === "PENDING";
  }

  toString() {
    return this.value;
  }
}

// エンティティ
export class Reservation {
  constructor(
    public readonly id: string,
    public readonly slotId: string,
    public readonly userId: string,
    private status: ReservationStatus,
  ) {}

  cancel() {
    if (!this.status.canCancel()) {
      throw new Error("キャンセルできない状態です");
    }
    this.status = ReservationStatus.cancelled();
  }
}

ここでのイメージは、

  • API の ReservationResource「外の顔」
  • ReservationReservationStatus「内臓」

という構図です。

両者は Mapper でつなぎます。

// API → Domain
function toDomain(resource: ReservationResource): Reservation {
  return new Reservation(
    resource.id,
    resource.slotId,
    resource.userId,
    new ReservationStatus(resource.status),
  );
}

5. ユースケースを TDD で「固定」する

最後に、ユースケース(アプリケーションサービス)を TDD で書きます。

describe("予約キャンセルユースケース", () => {
  it("確定済みの予約はキャンセルできる", async () => {
    // Arrange
    const repo = new InMemoryReservationRepository(/* ... */);
    const usecase = new CancelReservationUseCase(repo);

    // Act
    await usecase.execute({ reservationId: "resv-1" });

    // Assert
    const updated = await repo.findById("resv-1");
    expect(updated.status.toString()).toBe("CANCELLED");
  });

  it("すでにキャンセル済みの予約はエラーになる", async () => {
    // Arrange 〜 略 〜

    // Assert
    await expect(
      usecase.execute({ reservationId: "resv-2" }),
    ).rejects.toThrow("キャンセルできない状態です");
  });
});

ポイントは、

  • ユースケース名 = テストの describe になることが多い
  • ここに書いていることはそのまま
    「ビジネスサイドに見せても通じる仕様書」になり得る

という点です。

組み合わせることで得られる3つのメリット

ここまでの流れを、「何が嬉しいか」という視点で改めて整理します。

1. 仕様の一貫性が保てる

  • ユビキタス言語
    → スキーマ(OpenAPIなど)
    → ドメインモデル
    → ユースケーステスト

同じ言葉 を共有します。

その結果、

  • 「画面とAPIの言葉が違う」
  • 「APIの statusapproved なのに、画面では confirmed と表示している」

といった ちぐはぐ が起きにくくなります。

2. 変更に強くなる

  • APIの変更 → スキーマ変更
    → 型が崩れるところがコンパイルエラーで見える
  • ビジネスルールの変更 → ドメインモデルの振る舞い変更
    → ユースケーステストが教えてくれる

という形で、どこを直せばいいか」が 機械的に分かるようになります。

3. チームでの会話が楽になる

  • DDD のユビキタス言語があるおかげで「あのテーブル」「あのフラグ」みたいな曖昧な会話が減る
  • スキーマ駆動のおかげで「このAPI、何返してたっけ?」が 常に1ファイルを見れば分かる
  • TDD のおかげで、「ここを変えたら他が壊れそう」がテストで検知できる

結果として、設計レビューや仕様検討の場での議論がコードと地続き になります。

「全部を一度にやらない」のも大事

ここまで読むと、

「3つ全部を完璧にやらないといけないのか…?」

と感じるかもしれませんが、
実務では段階的に取り入れていくのが現実的です。

例えば、次のようなステップでも十分です。

  1. まず API スキーマをちゃんと書いて、そこから型とクライアントだけでも生成してみる
  2. 次に、ビジネスロジックが複雑な部分だけでも、DDD 的にモデルとユースケースを切り分けてみる
  3. 最後に、そのユースケース単位で TDD を始めてみる

いきなり「全部のコードをDDD化+TDD化+スキーマ駆動化」しようとすると、
ほぼ確実に挫折するので、スコープを絞って試す のがポイントです。

おわりに(まとめ)

この記事で一番伝えたかったのは、ざっくり言うとひとつだけです。

DDD × TDD × スキーマ駆動開発を
バラバラではなく “ひとつの流れ” として扱うと、
開発体験も設計の質もかなり変わる。

設計のキーワードだけ知っている状態から
「実際どういう世界を目指しているのか」が
ほんの少しでもイメージできていれば嬉しいです。

「ここをもっと具体的に知りたい」「この辺の話を掘ってほしい」などあれば、
コメントなどでフィードバックをいただけると助かります。

どの部分を深堀りすると実務で使いやすいか、
今後の記事のテーマ選定の参考にさせていただきます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?