Upgrade to Pro — share decks privately, control downloads, hide ads and more …

肥大化するRailsへの処方箋 AI駆動開発のポテンシャルを引出す「モジュラ・モノリス」の実践...

肥大化するRailsへの処方箋 AI駆動開発のポテンシャルを引出す「モジュラ・モノリス」の実践アプローチ

Railsアプリの肥大化で境界が曖昧になり技術負債に悩んでいませんか?
AI時代に並列開発で生産性を高めるためには、モジュラ・モノリスが有効です。
なぜ今注目されるのか・AIとの相性の良さ・気軽に始められる実践アプローチ・実際のチーム導入事例を紹介。

Avatar for Suguru Sakashita

Suguru Sakashita

March 26, 2026

More Decks by Suguru Sakashita

Other Decks in Programming

Transcript

  1. プロダクト本部 オペレーション開発部 阪下 駿 Suguru Sakashita 経歴 大手通信会社にて経理として新卒のキャリアをスタート。 その後、Webエンジニアにジョブチェンジし、Ruby on

    Rails のバックエンド開発を経験。 2024年にTOKIUMに入社し、書類データ化システムのチーム リーダーを務める。 自己紹介 2
  2. 会社名 株式会社TOKIUM 設立日 2012年6月 所在地 東京都中央区銀座6-18-2 野村不動産銀座ビル 12F 従業員数 約240名

    (2025年11月、正社員のみ) 代表取締役 黒﨑 賢一 TOKIUMの志 未来へつながる時を生む 会社概要 3
  3. チームメンバーは全3人 10年モノのモノリスRails MVCを逸脱し、肥大化したアーキテクチャー MVCではどうしても苦しい、複雑な状態遷移があるドメイン engineer engineer engineer ↓ app/ ├──

    commands/ ├── controllers/ ├── domains/ ├── helpers/ ├── jobs/ ├── mailers/ ├── models/ ├── parameters/ ├── policies/ ├── queries/ ├── serializers/ ├── services/ ├── specifications/ ├── validators/ └── views/ チーム参画当時 9
  4. Controller Controller A Controller B Controller C Model Model X

    Model Y Model Z DB table_x table_y table_z JOIN JOIN Controller → Models → DB の依存に境界がないため、 どのControllerからも任意のModelを参照でき、Model間も自由に結合する なぜつらいのか ― 密結合が伝播しやすい構造 11
  5. app/model 継承 / 利⽤ 継承 / 利⽤ 継承 / 利⽤

    永続化先は 1 つ Order::Use Payment::Use Shipping::Use ::User (ActiveRecor users テーブル email order_notification_enable default_payment_metho shipping_priority support_locale 名前空間で分けても、実テーブルが1つだと 本質的に関心は分離されない 結果として各コンテキストの「最大公約数」 を背負って肥大化する 変更時に「どの業務へ波及するか」を読み づらくなる なぜつらいのか ― 共有テーブルの問題 12
  6. Service A Controller Model DB Service B Controller Model DB

    Service C Controller Model DB Service D Controller Model DB API で密結合 = 分散モノリス Engineer Engineer 少人数で複数サービスを管理 → 認知負荷・運用コスト増大 ではマイクロサービスにすればよいか? 13
  7. そ う だ そ う だ モ ジ ュ ラ

    ー モ ノ リ ス 、 モ ジ ュ ラ ー モ ノ リ ス 、 移 行 移 行 14
  8. モノリス Controllers Models DB Schema A Schema B JOIN 共有

    DB(JOIN で密結合) Controller → Models → DB が直接依存 モジュラーモノリス Module A Controller Model Module B Controller Model Module C Controller Model DB Schema A Schema B Schema C 単一 DB(スキーマで論理分離) ドメイン境界で分割・単一デプロイ マイクロサービス Service A Controller Model DB Service B Controller Model DB Service C Controller Model DB サービスごとに独立 DB 完全分離・個別デプロイ モジュラーモノリスとは モノリスの開発体験を保ちながら、内部をモジュールに分割するアーキテクチャ “ 16
  9. 単⼀ DB (論理分離) packs/ Customer packa Order packag 永続化 永続化

    公開 API 経由で連携 Order::Custome Customer::Custom order_customers email order_notification_enable shipping_priority customers email company_nam billing_plan 例: Order::Customer と Customer::Customer のように業務ごとに 独立したモデル・テーブル定義が可能 連携は公開API経由に絞ることで、変更影響 を局所化できる デプロイはモノリスのままなので、運用は シンプル モジュラーモノリスのメリット 18
  10. モノリス モジュラーモノリス マイクロサービス デプロイ 単一 単一 サービスごと コードベース 1つ 1つ(モジュール分割)

    サービスごと データ 共有DB 論理分離 物理分離 モジュール間通信 直接参照 明示的インターフェース ネットワーク(API) 開発の独立性 × △ ◦ 運用の複雑さ × △ ◦ 3つのアーキテクチャ比較 19
  11. rails-app/ ├── app/ # 既存の Rails コード │ ├── controllers/

    │ ├── models/ │ └── ... ├── packs/ # モジュラーモノリスのパッケージ群 │ ├── product/ # ドメインパッケージ │ ├── order/ # ドメインパッケージ │ ├── notification/ # 基盤パッケージ │ └── ... └── ... ディレクトリ構成 22
  12. Package A Package Controlle Public API Service Mode Mode DB

    Public API A Service Mode Mode DB Controller は既存の権限管理ロジックが根深く、 段階的な移行のためパッケージ外に残すことにした プロジェクトでの構成 23
  13. Shopify はモジュラーモノリスの実践で、パッケージの分類について次のように述べている: ― A Packwerk Retrospective (Shopify Engineering, 2024) ドメインとして分割されるパッケージと、システム上の役割で分割されるパッケージが共存

    する 後者は Shopify では Platform コンポーネントと呼ばれ、基盤的な位置づけ → つまり、機能的なパッケージが存在してもよい 機能的なパッケージは存在してもよいの? "We are faced with a dichotomy: some components are domains, while others are designed around the functional role they play in the application." “ 25
  14. packs/customer/ ├── app/ │ ├── models/ # ドメインモデル │ │

    └── customer/ │ ├── public/ # 外部公開インターフェース │ │ └── customer/ │ │ └── public.rb │ └── use_cases/ # ユースケース実装 │ └── dto/ # データ転送オブジェクト ├── docs/ # パッケージ固有のドキュメント └── spec/ # テストコード ├── factories/ ├── models/ ├── public/ └── use_cases/ パッケージ内部の構成 26
  15. class OrdersController < ApplicationController def index # Order パッケージから注文一覧を取得 orders

    = Order::Public.list_by_customer( customer_id: current_customer.id ) # Customer パッケージから顧客の表示名を取得 customers = Customer::Public.names_by_ids( customer_ids: orders.map(&:ordered_by).uniq ) # Shipping パッケージから配送ステータスを取得 statuses = Shipping::Public.statuses_by_order_ids( order_ids: orders.map(&:id) ) @orders = orders.map do |o| { order: o, customer: customers[o.ordered_by], status: statuses[o.id] } end end end Controller はパッケージの公開APIのみを通じてアクセスし、内部実装には依存しない 実装例 ― Controller から公開APIを呼び出す 27
  16. 公開API(パッケージの入口 → 公開オブジェクトに変換) # packs/customer/public/customer/public.rb module Customer::Public def self.register(email:) result

    = ::Customer::Register.call(email: email) Dto::Customer.new(**result) end end ユースケース(内部モデルをオーケストレーション) # packs/customer/app/usecases/customer/register.rb class Customer::Register def self.call(email:) customer = ::Customer::Customer.create!(email: email) settings = ::Customer::NotificationSetting.create_defaults!(customer_id: customer.id) { id: customer.id, email: customer.email, notification_channels: settings.pluck(:channel) } end end 公開オブジェクト(ActiveRecordモデルを外部に露出させない) # packs/customer/public/dto/customer.rb class Customer::Public::Dto::Customer attr_reader :id, :email, :notification_channels def initialize(id:, email:, notification_channels:) @id = id @email = email @notification_channels = notification_channels end end 実装例 ― 公開APIの内部構造 28
  17. パッケージ名がそのままテーブル名のprefixになる パッケージ間で外部キー参照を持たないように規約で注意 PKとは別の公開識別子を共有する(マイクロサービスのような考え方) Payment packag Order packag Customer packa 同⼀

    package 内の F customer_custome ──────── id public_id(uuid) email status order_orders ──────── id public_id(uuid) customer_public_id(uui ordered_at order_items ──────── id order_id sku quantity payment_charges ──────── id order_public_id(uuid amount status package 間は内部 id を参 照せず public_id / uuid を値とし て保持する 実装例 ― DB定義 29
  18. packs/customer/ ├── app/ │ ├── models/ │ │ └── customer/

    │ │ └── user.rb │ ├── public/ │ │ └── customer/ │ │ └── public.rb │ └── use_cases/ └── spec/ → app/ ├── models/ │ └── user.rb ├── use_cases/ └── ... パッケージが Rails Way に従っていれば、ファイル移動と名前空間の調整で済む モノリスに戻すのは比較的かんたん 33
  19. Step 1 ― 規約ベースで小さく始める packs/ にパッケージを切り、公開API経由のアクセスを規約にする 境界の探索段階では身軽に試行錯誤できる 既存への依存が少ない新規機能などがベスト Step 2

    ― 境界が固まったら Packwerk で強制 package.yml を追加し、依存ルールを定義 既存の違反を許容しつつ、新規の違反のみ検出 おすすめのはじめ方 34
  20. 公開APIさえ守れば、パッケージ内部の設計は自由に選択できる packs/payment/ ├── app/ │ ├── aggregates/ # 集約 │

    ├── commands/ # コマンド │ ├── events/ # ドメインイベント │ ├── public/ # 外部公開インターフェース │ └── projections/ # リードモデル └── spec/ 状態遷移が複雑なドメインでは、Rails Way に縛られず イベントソーシングなどのアーキテクチャを採用している ただし、既存実装に戻せなくなる点がトレードオフ パッケージごとにアーキテクチャーが異なっても良い 36
  21. モノリス Model A Model B Model C Model D Model

    E Model F AI ? ? ? 影響範囲が不明 暗黙の依存が絡み合い AIが影響範囲を特定できない モジュラーモノリス Order Public API Notification Public API Customer Model UseCase Public API AI ✓ 責務と影響範囲が明確 公開APIが境界となり AIが責務と影響範囲を把握できる AI駆動開発との親和性が高い 37
  22. Step 1: 公開 API の契約を決める Order::Public Customer::Public Shipping::Public Step 2:

    パッケージごとに並列で実装 Order AI Agent 1 Model UseCase Public API DTO spec/ テスト作成 Customer AI Agent 2 Model UseCase Public API DTO spec/ テスト作成 Shipping AI Agent 3 Model UseCase Public API DTO spec/ テスト作成 契約(公開API)さえ決めれば、内部実装は同時並行で進められる パッケージ単位でAIに並列開発を任せやすい 38
  23. Order Model Public UseCase ✓ Customer Model Public UseCase ✗

    AI がミス Shipping Model Public UseCase ✓ Notification Model Public UseCase ✓ Product Model Public UseCase ✓ Payment Model Public UseCase AIに実装を任せた結果の失敗の影響が限定的 パッケージ丸ごとAIに書き直させるコストも限定的 AIの失敗コストを最小化できる 40
  24. 未来へつながる時を生む 株式会社TOKIUM 東 京 本 社 〒104-0061 東京都中央区銀座6丁目18-2 野村不動産銀座ビル12階 西 日

    本 営 業 所 〒550-0015 大阪府大阪市西区南堀江1丁目1番14号 四ツ橋中埜ビル7階 URL: https://www.keihi.com/company 44