段階的導入
レガシー Rust コードベースを一括置換すると、境界・エラー・永続化の穴が同時に広がる。Kamae では触れたワークフローごとに、DTO パース → 型付き状態 → ポート分離 → 原子性永続化の順で段階的に締める。
各段の詳細は 境界防御、ドメインモデリング、アプリケーション配線 を参照する。
Kamae はまず新しいコードパスに適用する。既存コードは、機能追加やバグ修正で触る箇所から段階的に引き締める。ドメイン全体の書き直しでリリースを止めない。
レガシーな慣習と衝突する場合、触っていないコードはローカルな慣習に従い、新旧の境界では新しい境界を明示的に文書化する。
レガシーな形を認識する
Section titled “レガシーな形を認識する”Rust サーバーコードベースでよくある出発点:
- 貧血モデル(anemic struct)と free function や service モジュール
- ORM の行型をドメインエンティティとして使う
- newtype の代わりに
Stringの ID や status 文字列 - ビジネスロジック中の
anyhowやunwrap - ハンドラが SQL や HTTP を直接呼ぶ
これらは移行の出発点であり、失敗ではない。次に起きそうなバグを取り除く最小の変更を選ぶ。
一度に一段ずつ進める。各ステップは単独でレビューできること。
| Step | 変更 | 典型的な触りどころ | リスク |
|---|---|---|---|
| 0. 境界のみ | DTO/row -> 新エンドポイントやコンシューマ向け TryFrom | handlers, message consumers | 低 |
| 1. ID と値オブジェクト | RequestId, Money, OccurredAt などの newtype | 変更フローで使う models | 低 |
| 2. ドメインエラー | 新ユースケースでの thiserror enum | application layer | 低 |
| 3. 型付き状態 | 重要な集約 1 つ分の state struct/enum | その集約の domain module | 中 |
| 4. ポート | 新ユースケースの背後に小さな repository trait | application + infrastructure | 中 |
| 5. トランザクションとバージョン | 原子的 save、outbox、楽観的バージョンチェック | persistence adapter | 中〜高 |
コードベースがすでに満たしているステップだけスキップする。
クレート全体ではなく機能で絞り込む(Strangler Fig)
Section titled “クレート全体ではなく機能で絞り込む(Strangler Fig)”レガシーモジュールに対して:
- 変更したワークフロー用に新しい use-case struct を追加する。
- 新パスが実証されるまで、旧エントリポイントはレガシーコードを呼び続ける。
- 新 API バージョン、フラグ、コマンドを新ユースケースへルーティングする。
- パリティテストが通ったら旧パスを削除する。
legacy handler -> legacy service -> DBnew handler -> AssignDriver use case -> port -> adapter -> DB移行スライスは集約 1 つ、またはエンドポイント 1 つを優先する。
レガシー移行の段階的ロードマップ
Section titled “レガシー移行の段階的ロードマップ”例: axum + sqlx のモノリスサービスで POST /requests/{id}/assign を移行する。
Phase 1 — 挙動を固定し、テストを追加(1 週目)
Section titled “Phase 1 — 挙動を固定し、テストを追加(1 週目)”- 統合テストで現行 HTTP 契約を記録する(ステータスコード、JSON 形状)。
- レガシーパス周辺に logging/metrics を追加し、トラフィックを計測する。
- まだ挙動は変えない。
Phase 2 — 境界 DTO(1〜2 週目)
Section titled “Phase 2 — 境界 DTO(1〜2 週目)”apiモジュールにAssignDriverBodyとAssignDriverDtoを導入する。- ハンドラの直接フィールドアクセスを
AssignDriverCommand::try_from(dto)に置き換える。 - レガシーサービスはまだ文字列を受け取る。検証は
TryFromに移る。 - 同じルートのまま出荷する。テストは緑のまま。
境界防御 を参照。
Phase 3 — 触った ID の newtype(2 週目)
Section titled “Phase 3 — 触った ID の newtype(2 週目)”domaincrate または module にRequestId,DriverIdnewtype を追加する。TryFromを newtype 構築に変更する。レガシーサービスは境界で.as_str()を受け取る。- 新しい
domainモジュールだけに追加 clippy を有効化する。
ドメインモデリング を参照。
Phase 4 — ユースケース抽出(3 週目)
Section titled “Phase 4 — ユースケース抽出(3 週目)”- レガシー SQL を private メソッドにインラインした
AssignDriverUseCaseを作成する。 - ハンドラは
use_case.execute(cmd)のみ呼ぶ。 - このパスの
anyhowをAssignDriverError(thiserror)に置き換える。
エラーハンドリング を参照。
Phase 5 — 1 集約の型付き状態(3〜4 週目)
Section titled “Phase 5 — 1 集約の型付き状態(3〜4 週目)”WaitingRequestとEnRouteRequestをモデル化し、割当ロジックをWaitingRequest::assign_driver(self, ...)に移す。- DB のレガシー
status: Stringは残す。adapter が row <-> state struct をマップする。 - HTTP なしで遷移の単体テストを追加する。
状態遷移 を参照。
Phase 6 — リポジトリポート(4〜5 週目)
Section titled “Phase 6 — リポジトリポート(4〜5 週目)”RequestResolverとRequestStoretrait を定義する。- ユースケースから SQL を
SqlxRequestStoreに移す。 - ユースケースは trait のみに依存する。
mainで配線する。
永続化、集約、イベント と アプリケーション配線 を参照。
Phase 7 — トランザクション、バージョン、outbox(5〜6 週目)
Section titled “Phase 7 — トランザクション、バージョン、outbox(5〜6 週目)”version列と条件付きUPDATEを追加する。- state save と outbox insert を 1 トランザクションに包む。
- リトライクライアント向けに idempotency key を追加する。
永続化、集約、イベント を参照。
Phase 8 — レガシーパス削除(6 週目以降)
Section titled “Phase 8 — レガシーパス削除(6 週目以降)”- feature flag またはルートトラフィックが新パス 100% であることを確認する。
- レガシーサービス関数と死んだ
status文字列チェックを削除する。 - 移行モジュールに
kamae-rs-reviewを実行する。
ペースはチーム規模に合わせて調整する。可能なら各フェーズを独立 PR にする。
差分をレビュー可能に保つ
Section titled “差分をレビュー可能に保つ”チーム展開の実践ルール:
- 避けられる限り、機械的リファクタと挙動変更を 1 PR に混ぜない。
- 旧パスを削除する前に、新境界にテストを追加する。
- 触ったフィールドだけ newtype と DTO 変換を導入し、後で広げる。
- 強化する crate または module で追加 clippy/rustdoc チェックを有効化する。
- 新旧の意味論が異なる場合のみ、短いコメントまたは ADR を残す。
ラダーを登り止めるタイミング
Section titled “ラダーを登り止めるタイミング”すべての struct に state machine や repository trait は不要。次の場合は現段階で止める:
- コードが安定し、低リスクで、めったに変わらない
- 集約に意味のあるライフサイクルや不変条件がない
- チームが persistence や並行性の挙動をまだ十分にテストできない
バグ、コンプライアンス要件、並行性が現状の形では弱すぎると示したら、一段上げる。
エージェントとレビュアーの期待
Section titled “エージェントとレビュアーの期待”移行時:
- スコープ判断に 段階的導入 を読み込む
- 実装する段のトピックガイドを読み込む
- 周囲がレガシーでも、変更パスに
kamae-rs-reviewを使う - crate 全体が移行済みのふりをせず、残るレガシーリスクを明示する