5年間のRails開発者がDDDに出会って考えが変わった話

に公開

はじめに

5年間、Ruby on RailsでMVC + Serviceパターンの API開発をしてきました。Railsの「規約に従えば爆速で動くものができる」という思想が好きで、その生産性の高さは強力な武器でした。

そんな自分が、Python + FastAPIで構築されたDDDの実装パターン(Entity / Repository / UseCase)と、クリーンアーキテクチャをベースにしたプロダクトに関わることになりました。最初の印象は、正直に言って戸惑いでした。

  • 概念が難しい
  • なんだか回りくどい
  • 1機能を追加するだけで変更ファイル数がやたらと多い

「Railsならこれ、Service1個で済むのに」と何度も思いました。

でも、書き続けるうちに「回りくどさ」の正体が腹落ちしてきて、印象が一変しました。

この記事では、「フレームワーク中心アーキテクチャ(Rails MVC + ActiveRecord)」と「レイヤード/依存性逆転アーキテクチャ(DDD + クリーンアーキテクチャ)」 を4つの観点で比較しながら、自分の中で何が変わったのかを書いていきます。

なお、本記事で「DDD」と表記しているのは、現場でよく見るこの組み合わせ全体(DDDの実装パターン + クリーンアーキテクチャ)を指す呼称として用いています。ユビキタス言語や境界づけられたコンテキストといったDDD本来の戦略的設計そのものの話ではなく、実装アーキテクチャの対比として読んでもらえると、しっくりくると思います。


1. 依存の方向 — 「DBに縛られない」という自由

Railsは上から下への一方向依存

Railsのアーキテクチャは、すべてがDB(ActiveRecord)を起点にしています。Modelはテーブルと1対1で対応し、ControllerもServiceもModelに依存します。

Controller → Service → Model (= Table)

この構造のメリットは分かりやすさです。テーブルを作れば、それに対応するModelが自動的に動き、すぐにビジネスロジックを書けます。

一方で、デメリットはスキーマ変更が全レイヤーに波及することです。テーブルのカラムを変えると、Model、それを使うService、Controllerまで影響が及びます。

例えば、最初は users テーブルに address カラムを1つ持っていたとします。後から「ユーザーが複数の住所(自宅・職場・配送先)を持てるようにしたい」となり、addresses テーブルに切り出すことになりました。

すると、user.address を参照していた全箇所を user.addresses.first などに書き換えることになります。Modelの関係定義(has_many :addresses)、それを参照するService、Controller、APIレスポンスの整形処理、N+1対策(includes(:addresses))、テストデータ、すべてに影響が及びます。

つまり、「住所を複数持てるようにする」というビジネス上の変更と、「テーブルを分割する」というDB都合の変更が、混ざって全レイヤーに散らばることになります。

DDD + クリーンアーキテクチャは依存性逆転で「Domainが中心」

DDD + クリーンアーキテクチャでは、依存関係が逆転しています。中心にあるのはDomain(ビジネスルール)で、それは何にも依存しません。DBアクセスはInfrastructure層が担当し、DomainはRepositoryのインターフェース経由でデータにアクセスします。

UseCase → Domain ← Repository (interface) ↑ Infrastructure (実装)

この構造だと、変更が綺麗に分離されます。先ほどの住所切り出しの例で言えば、Domainには「ユーザーは複数の住所を持つ」というビジネスの変更だけが反映され、テーブル分割やN+1対策といったDB都合の変更はInfrastructure層の中に閉じ込められます。

Rails MVC + ActiveRecordだとビジネスとインフラの変更が混ざって全体に散らばるのに対し、DDD + クリーンアーキテクチャでは「ビジネスの変更(Domain)」と「永続化の変更(Infrastructure)」がレイヤーで分かれるのが大きな違いです。

価値を理解した瞬間

最初は「なぜEntityとテーブルを別々に書くのか、二度手間じゃないか」と思っていました。しかし、一度DBスキーマを大きく変える局面があり、その時にビジネスの変更とインフラの変更が綺麗に分離されている実感を得て、この設計の価値を理解しました。Railsだったら、確実に両方が混ざった状態で全レイヤーに変更が波及していたはずです。


2. ビジネスロジックの書き方 — レイヤーが「正しい置き場所」を教えてくれる

RailsのServiceは「何でも入る箱」

RailsのService層には、DDD + クリーンアーキテクチャで言うところの3つの責務が混在しがちです。

  • Entity: ビジネスルール(「この注文はキャンセル可能か?」)
  • UseCase: フローの組み立て(「注文をキャンセルし、在庫を戻し、メールを送る」)
  • Repository: データアクセス(DB操作)

これらが1つのServiceクラスに集約されています。

自由度の高さがもたらす負債

Railsは自由度が高いです。それは強みでもありますが、ロジックの置き場所がチームの規約と個人の判断に委ねられるという弱点も持っています。

結果として、

  • Serviceがどんどん太る
  • 似たロジックがあちこちに散らばる
  • 「あれ、この処理どこに書いたっけ?」が頻発する

ということが起きます。

レイヤー構造が「置き場所」を強制する

レイヤー分離は、コードの置き場所を構造的に決めてくれます。

  • ビジネスルール → Entity
  • フローの組み立て → UseCase
  • データアクセス → Repository(インターフェース)/ Infrastructure(実装)

「このコードはどこに書くべきか?」で悩む時間が消えます。書く前から置き場所が決まっているからです。

「迷わないためのコスト」だった

最初は「ファイル分割が冗長すぎる」と思っていました。しかし、よく考えればこれは 迷わないためのコスト でした。

事前に構造を決めておくことで、書く時の判断コストを下げる。長期的に見れば、開発速度や、新メンバーがコードを書き始めるまでの時間の短縮につながります。


3. テスト戦略 — DB不要のテストが生む開発体験の差

RailsのテストはDB前提

Railsでは、ServiceがActiveRecordに直接依存しているため、テストは基本的にDB前提になります。

  • セットアップが重い
  • 実行が遅い
  • テストデータの不整合で壊れやすい

「テストを書きたくても、書く心理的ハードルが高い」状況になりやすいです。

Domain層がDB非依存だからテストが速い

依存性逆転によって、Domain層は何にも依存しない純粋なオブジェクトになります。だからDomainテストはDB不要で書けて、ミリ秒で完了します

UseCaseのテストも、RepositoryをMockすればフロー部分だけを高速に検証できます。

CI時間が10分から3~4分に

Railsで開発していた頃は、GitHub ActionsでのCIテストに10分以上かかるのが普通でした。DBのセットアップ、フィクスチャの読み込み、テスト全体の実行で、PRを出してから結果が返ってくるまでに長い待ち時間が発生していました。

DDDのプロジェクトに移ってからは、CIが3分程度で完了するようになりました。Domainテストの大部分がDB不要で走るので、テスト全体の実行時間が桁違いに短くなったからです。

テストの速さは、開発のリズムを変えます。PRを出してすぐ結果が返ってくるので、サクッと修正してサクッと回せる。テストを書く習慣も、テストを増やすことへの心理的ハードルも、自然に変わっていきます。


4. AI時代、実装のボトルネックは「書く」から「判断する」へ

AI以前: 「書く速さ」も生産性の大きな要素だった

もちろんAI登場以前から、設計判断・バグ調査・コードレビューといった「判断のコスト」は生産性に大きく影響していました。ただそれと並んで、コードを書くこと自体のコスト も無視できない比重を占めていたのは確かだと思います。Railsの「規約に従えば爆速で動く」という思想は、特にこの「書くコスト」を圧縮する方向に強く効いていたと思います。

AI以後: 「判断する」の比重がさらに大きくなった

AI(特にClaude CodeのようなAIコーディングエージェント)が普及した今、コードを書くこと自体のコストは大きく下がりました。その分、もともとあった「判断するコスト」の比重がさらに大きくなった というのが、自分の実感です。

「AIが書いたコードが本当に正しいか」を判断する時間が、実装全体の支配的なコストになりつつあります。

レイヤー分離は判断コストを2方向から下げる

判断コストは、大きく分けて2つの局面で発生します。

1. 読む時の判断

AIが書いた差分を読む時、レイヤー分離は「このファイルには何が書かれているはず」という想定を作ってくれます。

  • Domainファイルを開く時 → ビジネスルールが書いてあるはず
  • UseCaseファイルを開く時 → フローの組み立てが書いてあるはず
  • Infrastructureファイルを開く時 → データアクセス(Repositoryの実装)が書いてあるはず

この想定があると、AIの差分を「想定とのズレ」として高速に判断できます。Railsだと1つのServiceに何でも入りうるので、毎回ゼロから「これは何のコードだ?」と読む必要があります。

2. 動かして確かめる時の判断

前述のテスト戦略がここで効いてきます。Domainテストがミリ秒で完了するので、AI生成コードが正しく動くかの判断が桁違いに速いです。CIを待たずにローカルで即座に「壊れていないか」を確認できます。

レイヤード設計は結果的にAI時代に適応していた

DDD + クリーンアーキテクチャが体現するレイヤー分離の思想は、AIを想定して設計されたわけではありません。

それにもかかわらず、結果的にAI時代の制約に最も適応した構造になっていました。これは特定の手法というより、レイヤー分離されたアーキテクチャ全般 に言える話だと思います。


結論

Rails MVC + Serviceの強み

  • 立ち上がりの速さ
  • 学習コストの低さ
  • 規約の強さ

素早く立ち上げたい場面では、今もRailsの強みが活きます。

DDD+クリーンアーキテクチャの強み

  • チーム規模・変更頻度・プロダクト寿命が伸びるほど、レイヤー分離の価値が複利で効いてくる
  • AI時代に判断コストが増えていく中で、DDDの実装パターンと依存性逆転を組み合わせたアーキテクチャは 「変更を安全にする設計」 として真価を発揮する

DDDを学んで変わったこと

最初に感じた「回りくどさ」は、コードを長く運用したり、チームで開発したり、AIと協働したりするほど効いてくる仕組みでした。

Railsで書いた過去のコードを振り返った時にも、「ここはDomainを切り出したかった」「このServiceは責務が混ざっている」と解像度が上がりました。

どちらが正解ではない

どちらが正解、という話ではありません。プロダクトの規模、チームの状況、開発フェーズに応じて、適切なアーキテクチャを選ぶことが大事だと思います。その上で、DDD/クリーンアーキテクチャの思想を知っているかどうかで、選択肢の幅が変わると今は感じています。

neoAI

Discussion