RAKSUL TechBlog

RAKSULグループのエンジニアが技術トピックを発信するブログです

グローバルチームで AI コーディングを加速させるための Rails 開発指針

プラットフォーム統括部でエンジニアをしている灰原です。

今年もあっという間に年の瀬ですね。多くのソフトウェアエンジニアにとって、今年一番のトピックはコーディングエージェントの台頭ではないでしょうか。ラクスルにおいても、各チームでコーディングエージェントの導入が進んでおり、「とりあえず試してみる」フェーズから「より上手く使う」フェーズに移行していると感じます。 そんな中で私が所属するチームで行った、グローバルチームで AI コーディングをフル活用して Rails アプリを開発していくための工夫を紹介します。

※ 本文中のサンプルコードはすべて架空のものです。

チーム構成と課題

前提条件として、簡単に私のチームを紹介します。

私のチームは、東京・麻布台オフィスのメンバーと、ベトナム開発拠点のメンバーによる混成チームです。 ロール別にみると、以下のような組成になります。

  • 東京・麻布台オフィス: プロダクトマネージャー (x1)・テックリード (x1)
  • ベトナム開発拠点: プロジェクトマネージャー (x1)・エンジニア (x2)

私はテックリードとして参画しており、実装はベトナム開発拠点のエンジニア2名 + 私で担当しました。

グローバルチームの課題・AI コーディングの課題

私たちのチームでは、主に2つの課題に直面していました。

1つ目は、グローバルチーム特有のコミュニケーションコストです。地理的な距離や言語の壁により、設計意図や実装方針をメンバーに確かに伝えることには、やはり一定の難易度があります。これにより、AI コーディングで実装そのもののスピードが上がったとしても、実装に取り掛かるための仕様確認や、コードレビューでのやり取りがボトルネックになってしまいます。

2つ目は、担当するプロジェクトの仕様の複雑さです。これは、前述のコミュニケーションコストの課題を加速させます。さらに、仕様の整理が不十分なままメンバーそれぞれで実装を進めると、コードが絡み合い、バグの温床になりかねません。また当然、AI コーディングのスピードを落とすことにもなります。

これらの課題を解決するために、私たちは「人間同士のコミュニケーションコストを下げながら、AI が一貫したコードを生成できる仕組みを作る」ことを目指しました。以降のセクションでは、その具体的な取り組みを紹介します。

AI コーディングのスタイルを使い分ける: SDD or not?

AI コーディングのスタイルは大きく2つに分類できます。1つは Vibe Coding や Agentic Coding のように、プロンプトでの対話を中心にしたスタイルです。そしてもう1つは、最近になって注目を集めている SDD(Spec-Driven Development)です。

SDD を実践するためのツールである spec-kit の README では、SDD について以下のように説明されています。

Spec-Driven Development flips the script on traditional software development. For decades, code has been king — specifications were just scaffolding we built and discarded once the "real work" of coding began. Spec-Driven Development changes this: specifications become executable, directly generating working implementations rather than just guiding them.

https://github.com/github/spec-kit?tab=readme-ov-file#-what-is-spec-driven-development

特に注目すべきは "specifications become executable" というワードです。SDD では、まずは仕様書を作成し、それをコーディングエージェントへの入力として実装を生成させます。このプロセスが、仕様書が「実行可能」になると呼ばれる所以でしょう。

2025年12月現在、SDD のプロセスを補強するための様々なツールが登場しています。ただし、私たちがプロジェクトに取り組んでいた夏頃には、独自に SDD を実施していました。当時の私たちは SDD という用語には辿り着いていませんでしたが、実質的に SDD と同じ方法で開発に取り組んでいました。

SDD は与えられた仕様を確実に実装するうえで非常に有用です。特に、仕様書を作成するフェーズで、仕様そのものを改めて整理できたり、場合によっては仕様バグを発見することができました。また、実装タスクが終わるごとに仕様書を参照するプロンプトを実行することで、実装が仕様に適合していることを簡単に確かめられるようになりました。仕様書をもとにユニットテストやシナリオテストを生成するのが確実な方法でしょう。

ただし、既存のコードベースに対する変更を SDD で進めるのは難易度が高いという課題があります。既存のコードベースには、文書化されていない仕様や暗黙の依存関係が存在するためです。

そこで私たちは、コードベースを分離したうえで、コンポーネントごとに SDD で進めるか対話ベースで進めるかを決める方針を採りました。具体的には、新規のビジネスロジックを独立したコードベースに切り出し、そこは SDD で開発します。一方、その新たなコードベースの機能をインテグレーションする形で既存コードの改修を進める部分は、対話ベースの AI コーディングで行いました。

これはリソースマネジメントの観点でも工夫しており、SDD と対話ベースそれぞれのスタイルごとにアサインするメンバーを固定しました。これにより、それぞれのスタイルで必要な AI スキルを、プロジェクトの中でまずは個々人が深く会得することを目指しました。

モジュラモノリスで境界線を引く: Packwerk

前述のとおり、私たちは新規のビジネスロジックを独立したコードベースに切り出し、SDD で開発する方針を採りました。このコンポーネント分割を実現するために採用したのが Packwerk です。

Packwerk は Shopify が開発した Gem で、Rails アプリケーションを "pack" という単位でモジュール分割できます。各 pack は外部から呼び出し可能な Public API を持ち、それ以外のクラスへの直接アクセスは制限されます。

packs/
├── orders/
│   ├── app/
│   │   ├── models/
│   │   └── public/          # Public API
│   └── package.yml
└── inventory/
    ├── app/
    │   ├── models/
    │   └── public/          # Public API
    └── package.yml

この構成は、SDD と対話ベースの AI コーディングを使い分けるうえで非常に有効でした。新規の pack は SDD で開発し、既存コードから新規 pack の Public API を呼び出す部分は対話ベースで進める、という分担が自然にできるようになります。

また、AI コーディングの観点でも大きなメリットがありました。AI に与えるコンテキストを pack 単位で絞り込めるため、「この pack の Public API を使って〇〇を実装して」と指示すれば、AI は関連するファイルだけを参照してコードを生成します。私は Claude Code を使っていますが、pack ごとに CLAUDE.md を作っておくことで、体感としてはかなり良い精度でコードが生成されるようになりました。

インターフェースを明確にする: dry-types, dry-struct

Packwerk で境界を引いた次のステップとして、pack ごとの Public API のインターフェースを厳密に定義しました。ここで活躍したのが dry-types と dry-struct という Gem です。

dry-types は Ruby 向けの型システムの実装であり、dry-struct によりその型を使った構造体を定義できます。この dry-struct を使って、各種 pack の Public API の引数・戻り値の型を表現しました。例えば、以下のようにして create_order メソッドの引数を定義します。

# packs/orders/app/public/orders/types/requests/create_order_request.rb
module Orders
  module Types
    module Requests
      class CreateOrderRequest < Dry::Struct
        attribute :user_id, Types::Strict::Integer
        attribute :items, Types::Array.of(OrderItem)
        attribute :shipping_address, Types::Strict::String
        attribute :note, Types::Strict::String.optional
      end
    end
  end
end

AI に「CreateOrderRequest を使って注文作成機能を実装して」と指示すると、AI は型定義を読み取り、必要な属性を正しく設定したコードを生成してくれます。「items は配列でなければならない」「note はオプショナル」といった制約も、型定義から自律的に理解してくれました。

さらに、この型定義はドキュメントとしても機能します。グローバルチームのメンバーが Public API を使ったコードを書く際に、dry-struct の定義を見ることで、理解の助けになります。これにより、コミュニケーションコストが削減されました。

また、ビジネスロジックが複雑な箇所では、処理の中間データにも dry-struct を適用しています。例えば、とある処理では Planner が作成した「実行計画」を Executor が実行するという設計をしました。この「実行計画」のデータ構造も、以下のように dry-struct で定義しています。

# app/models/order/cancellation/plan.rb
class Order::Cancellation::Plan < Dry::Struct
  attribute :refund_amount, Types::Strict::Integer
  attribute :inventory_adjustments, Types::Array.of(InventoryAdjustment)
  attribute :canceled_at, Types::Strict::Time
end

このように中間データにも型を与えることで、複数クラスに渡る複雑なビジネスロジックの実装でも、人間と AI の両方にとって理解しやすいコードを生成できるようになりました。

ビジネスロジックを model に押し込める: 37signals-style Concerns

Packwerk と dry-struct でインターフェースを整えた上で、もう一つ重要な設計方針を導入しました。それは「ビジネスロジックを model の名前空間に押し込める」というルールです。

私たちがこれまで構築してきた Rails アプリでは、Service Object にビジネスロジックを記述する傾向がありました。私自身は Service Object を悪いものだと考えていません。ただし、Service Object が乱立してドメインが薄まってしまうリスクを常に抱えており、これを防ぐためには開発者それぞれがコードベース全体を常に見渡せていなければなりません。このプロジェクトでは「良い Service Object」を AI に作らせることが難しいのではないか、という仮説を立てました。

そこで私たちは、37signals のブログ記事 "Good Concerns" で紹介されているパターンを採用しました。Service Object ではなく、モデルを中心に据え、モデルの名前空間配下にロジックを配置するアプローチです。

具体的には、以下の3層構造でコードを整理しています。

1. Model で Concern を include する

# app/models/order.rb
class Order < ApplicationRecord
  include Cancellable
  include Refundable
  ...
end

Model 自体はシンプルに保ち、機能ごとに Concern を include します。

2. Concern はオーケストレーターとして振る舞う

# app/models/order/cancellable.rb
module Order::Cancellable
  extend ActiveSupport::Concern

  class_methods do
    def cancel!(request)
      plan = Internal::Order::Cancellation::Planner.new(request).run
      Internal::Order::Cancellation::Executor.new(plan).run
    end
  end
end

Concern 自体には複雑なロジックを書きません。代わりに、別クラスの処理を呼び出す「オーケストレーター」としての役割に徹します。 なぜなら、Concern にロジックを直接書いてしまうと、見かけ上のコードは分割されますが、Concern のコードは結局 Model に mix-in されるため、実質的に Fat Model に繋がるためです。

3. ビジネスロジックは PORO として実装する

# app/models/order/cancellation/planner.rb
class Order::Cancellation::Planner
  def initialize(request)
    @request = request
  end

  def run
    # 複雑なビジネスロジックを実装
    # 結果を型付きの Plan オブジェクトとして返す
    Order::Cancellation::Plan.new(
      refund_amount: calculate_refund,
      inventory_adjustments: build_adjustments,
      canceled_at: Time.current
    )
  end

  private

  def calculate_refund
    # ...
  end

  def build_adjustments
    # ...
  end
end
# app/models/order/cancellation/executor.rb
class Order::Cancellation::Executor
  def initialize(plan)
    @plan = plan
  end

  def run
    # Plan に基づいて実際の処理を実行
  end
end

この PORO (Plain Old Ruby Object) の配置の仕方が、この設計パターンの特徴です。Order モデルのキャンセル処理に関するロジックを記述する際には、上記のように app/models/order/cancellation/ というパスを切って、その配下に PORO を配置します。ここに定義されるのは Concern ではなく、独立した PORO なので、名実ともにビジネスロジックを切り出すことができます。

この設計パターンは AI コーディングと非常に相性が良いとわかりました。

まず、クラスの責務が明確です。「Planner は計画を立てる」「Executor は実行する」という単一責任が名前から読み取れるため、AI は適切な場所にコードを生成できます。

次に、ディレクトリ構造がドメインを反映しています。order/cancellation/ というパスを見れば、「注文のキャンセルに関するロジック」だと一目でわかります。AI に「キャンセルの Planner を修正して」と指示すれば、迷わず正しいファイルを編集してくれます。

さらに、dry-struct との組み合わせが強力です。Planner と Executor の間でやり取りされるデータは Plan として型定義されているため、AI はインターフェースを理解した上でコードを生成できます。

「ビジネスロジックを model に押し込める」というルールは一見制約が強く見えますが、実際には「どこに何を書くか」が明確になり、人間と AI の両方にとって開発しやすい環境が整いました。

最後に、このパターンはビジネスロジックを良く表現したモデル設計 (DBテーブル設計) がされていることが前提になることを、申し添えておきます。

おわりに

グローバルチームで AI コーディングを活用するために、私たちが行った工夫を紹介しました。

ポイントをまとめると以下の通りです。

  • SDD と対話ベースの使い分け: 新規ビジネスロジックは SDD で確実に仕様を実装し、既存コードの改修は対話ベースで柔軟に進める
  • Packwerk による境界の明確化: pack 単位でコードベースを分離し、AI に与えるコンテキストを絞り込むことで精度を向上させる
  • dry-types / dry-struct による型定義: Public API や中間データのインターフェースを型で明示し、AI とチームメンバー双方の理解を助ける
  • 37signals-style Concerns と PORO: ビジネスロジックを model の名前空間に凝集し、責務が明確なクラス設計により AI が迷わずコードを生成できるようにする

これらの取り組みにより、地理的な距離や言語の壁を超えて、一貫した品質のコードを効率よく開発できるようになりました。

AI コーディングというと、プロンプトの書き方に注目が集まりがちです。しかし、それ以上に「AI が迷わないコードベースを作る」ことが重要だと感じています。結局のところ、人間にとって読みやすい設計は AI にとっても理解しやすい設計であると実感しました。

今後もチームで試行錯誤を続けながら、より良い開発体験を追求していきたいと思います。この記事が、グローバルチームや AI コーディングに取り組む方の参考になれば幸いです!