「作り直したい」は判断材料にならない
個人で作った小さな予約システムを、数年動かしている。フロントはSPA、バックエンドはサーバーレス関数、データストアはドキュメントDB。いわゆるBaaSに全部載せた構成だ。
去年あたりから、作り直したいとずっと思っていた。ただ、その動機を正直に分解すると「新しいスタックを触りたい」がかなりの割合を占めている。動いているものを壊すには、これでは弱すぎる。
なので、判断できるものだけを計測した。やったのは3つ。
コミットログのうち、何割が「基盤との格闘」か
いちばん効いたのがこれだった。コミット履歴を頭から読んで、機能開発ではないコミットを数える。
全73コミットのうち、20件以上がこの種類だった。件名を並べるとはっきりする。
fix: 関数をCIからデプロイできるようにする
fix: 関数のエントリポイントをビルド出力に合わせる
fix: 関数を外部から呼び出し可能にする
fix: 一覧用の複合インデックスを追加
fix: 関数側にも型定義ライブラリを追加(ビルドエラー修正)
fix: tsconfig にパスマッピングを追加(3ファイル)
fix: バンドラの設定にもエイリアスを追加
デプロイ時に環境変数をファイル経由で関数に渡す
ステージングのシークレット検査を fail-fast から警告に緩和
どのシークレットが欠けているかを表示する診断を追加
約27%。4回に1回のコミットが、利用者に何も届けていない。バグ修正ですらなく、プラットフォームとの帳尻合わせだ。
この比率、相性を測る定量指標としてけっこう使えると思う。10%なら許容範囲だろう。30%近いなら、自分が遅いんじゃなくて構成に摩擦がある。自分のリポジトリで一度数えてみると、思っているより多いはずだ。
業務上の不変条件を、DBが保証できているか
ふたつめは、もっと本質的な話だった。
この予約システムには「定員を超えて予約を受け付けない」という不変条件がある。ドキュメントDBではこれをDB側で保証できないので、アプリケーションコードで守るしかない。
実際に並列で予約を投げるテストを書いたら、定員超過が通った。これはバグだが、直したところで同じ種類のバグがまた生える。守るべき条件がアプリ側にしかないので。
リレーショナルDBなら、これは制約1行で終わる話だ。
-- 定員は DB が保証する。アプリが何をしようと破れない
ALTER TABLE reservations
ADD CONSTRAINT capacity_within_limit CHECK (headcount <= capacity);
-- 二重送信も同様。アプリのリトライに耐える
ALTER TABLE reservations
ADD CONSTRAINT uniq_idempotency UNIQUE (facility_id, idempotency_key);
「アプリで気をつける」はレビューでは通るが、本番では破れる。不変条件をDBに置けるかどうかは、データストアを選ぶときの第一の基準にしていいと思う。
素朴な操作に、いくら払っているか
みっつめは読み取り量。「自分の予約一覧を見る」という、いちばん素朴な操作に何が起きているかを数えた。
クエリ能力が足りないぶんを「多めに取ってアプリ側で絞る」で埋めていたので、1回の照会で1,000件前後、条件によっては1,500件のドキュメント読み取りが発生していた。
規模が小さいうちは、これでもちゃんと動く。動いてしまうから気づかない。数えて初めて、この構成が払っているコストが見えた。
作り直さない理由も、同じ熱量で書く
ここが大事なところだった。設計書には、現行システムを擁護する節をわざわざ作っている。
現行の実装は、設計として素直だ。ドメインロジックはちゃんと分離されているし、型定義はフロントとバックで共有されているし、CIで型チェックとテストが回って、監査ログまである。小規模プロジェクトとしては十分に良い作りだ。
つまり悪いのは実装じゃない。「良い構造が、ランタイムの制約で本来の価値を出せていない」という状態だ。この区別をしておかないと、リアーキが過去の自分の否定になって、判断が感情に寄ってしまう。
コストも比べた。結果はほぼ同等で、移行の投資はランニングコストでは回収できない。回収するとしたら開発時間のほうだ。これも正直に書いた。
そのうえでの結論はこうなる。いま摩擦を感じていないなら、やらなくていい。自分の場合は27%という数字が出たのでやる、というだけの話だ。
置き換えじゃなくて、並行させる
実装フェーズで最初にやったのは、既存のコードを1行も消さないことだった。
新しいスタックはモノレポの別パッケージとして追加して、既存の実装はそのまま残す。フェーズごとに切り戻し手段を用意して、両系統を並行稼働させてデータを突き合わせる前提で計画した。
apps/web/ 新しいアプリケーション
packages/core/ DB・フレームワークに依存しない純関数(時刻計算・空き判定・定員判定)
packages/db/ スキーマ・マイグレーション・行レベルセキュリティ・クエリ
functions/ 既存の実装(そのまま残す)
frontend/ 既存のSPA(そのまま残す)
ビジネスロジックを core に純関数として抜き出したのは、移行のためだけじゃない。DBにもフレームワークにも依存しない層があると、次に基盤を替えるときのコストが下がる。今回の教訓をそのまま構造にしたものだ。
作り直しの最大のリスクは、技術的な失敗じゃなくて、途中で力尽きて両方が中途半端になることだった。だから各段階に「ここで止めても壊れない」状態を用意している。
感覚で決めるのをやめる
「動いているものには触るな」は正しい教えだが、触らない理由を毎回感覚で決めていると判断の質が上がらない。コミットログを数えて、不変条件をDBに置けるか確かめて、素朴な操作のコストを測る。それだけで、やる・やらないのどちらに転んでも説明できるようになった。
作り直さない理由を同じ熱量で書けないうちは、動機がまだ感情の側にある。今回いちばん役に立った自己チェックはこれかもしれない。