技術的負債になる前に - FinTechスタートアップが 1 ヶ月半で実施した DB 統合の判断と実装
こんにちは!ソフトウェアエンジニアの inari111 です。
いつの間にか入社から半年が経っていました!
はじめに
先日の モジュラモノリスからモノリスへ。RemitAid の Go アプリケーションアーキテクチャと課題 ではモジュラモノリスからモノリスへ移行する背景について書きました。
RemitAid では、モジュラモノリスを採用し、merchant DB と transfer DB という 2 つのデータベースを運用していました。
しかし、サービスの成長とともに、この構成が開発効率やデータ整合性の面で課題を抱えるようになり、モジュラモノリスの解消を行うことにしました。
Phase1 では DB の統合を行い、Phase2 では merchant モジュールと transfer モジュールをなくし、徐々にモノリスに移行することを予定しています。
本記事では、この Phase1 の DB 統合をどう進めたかについて書きたいと思います。
DB 統合前の構成
前の記事と重複する部分もありますが、統合前の構成について紹介します。
大まかな構成は上記の図のようになっています。
私たちのシステムは、クリーンアーキテクチャに基づいたモジュラモノリスとして実装されています。
merchant モジュールと transfer モジュールという 2 つの独立したモジュールが存在し、merchant モジュールからは merchant DB へ、transfer モジュールからは transfer DB へ接続していました。これらのモジュールは、composer と呼ばれる usecase レイヤーを通じて連携する構成になっています。
フロントエンドは React を用いたアプリケーションで、 Amplify でホスティングされています。
なぜ DB 統合に踏み切ったのか
なぜ DB 統合をすることにしたのかについて説明します。
主に 2 つの問題がありました。
1. 開発効率の低下
開発を進めるうちに、merchant DB と transfer DB の境界が次第に曖昧になってきました。
新しいテーブルを追加する際、「これはどちらの DB に作成すべきか?」という議論がしばしば発生し、意思決定に時間を取られるようになりました。
現時点では大きな負担ではありませんでしたが、テーブルが増えていくほどそのコストは無視できないものになっていきます。
さらに、両方の DB に同じようなテーブルを作りたくなるケースも出てきました。
2. データ整合性の問題
最も深刻だったのは、本来同一トランザクションで処理すべきデータが 2 つの DB に分散してしまったことです。
お客様の大切なお金を扱うサービスとして、データの不整合は起こしてはならない問題です。
しかし、DB が分離している構造では、将来的に問題になる可能性が出てきました。
この問題は最初から起きていたわけではなく、サービスの成長とともに少しずつあるべき形と乖離してしまったと推測しています。
決断の理由とタイミング
FinTech サービスである以上、トランザクション境界の適切な管理は譲れない要件でした。データの不整合が起こり得る状態を放置することは、お客様の資産を扱う立場として許されません。
正直なところ、現在のサービス規模を考えると、DB 分割によるメリットはそれほど大きくありませんでした。むしろ、1 つの DB で開発を進めた方が、開発効率も保守性も向上すると判断しました。
また、タイミングも重要な要素でした。もし半年後、1 年後まで先延ばしにしていたら、データ量もコードベースも肥大化し、ユーザー数も増え、統合コストは今の何倍にも膨れ上がる可能性があります。
実際、今回の統合に関する作業は約 1 ヶ月半で完了できましたが、これは早期に決断したからこそ実現できた期間だったと思います。
方針の検討
DB 統合を進めることが決定した後、チーム内で方針を検討しました。
決定した方針は以下の通りです。
- merchant DB, transfer DB どちらかにテーブルを寄せるのではなく、新規の DB に merchant DB と transfer DB のテーブルを移行する
- 新規 DB にしたほうが切り戻しが容易なため
- メンテナンスモードに切り替え、サービスを停止した状態で行う
- MySQL の Collation が一部意図しないものになっていたので、Collation 統一を行う
DB 統合の準備
データ移行用 CLI の実装
データ移行用 CLI は Go で実装し、下記の機能を用意しました。
実装には Claude Code がとても役に立ちました。
コマンド | 説明 |
---|---|
create-schema |
新しいスキーマを作成 |
migrate-structure |
テーブル構造をコピー |
migrate-data |
データを新しいスキーマにコピー |
add-indexes |
インデックスを追加 |
add-constraints |
外部キー制約を追加 |
verify |
移行の検証 |
analyze |
行数カウントで詳細な移行状況を分析 |
オプション: --all , --verbose (全テーブルを表示) |
|
rollback |
移行をロールバック(スキーマを削除) |
cleanup |
失敗した移行のクリーンアップ(データのみ削除) |
dry-run |
変更を加えずに移行をシミュレート |
test-connection |
データベース接続をテスト |
migrate-structure コマンドでは各テーブルの Collation の統一も行うようにしました。
メンテナンスモード
今回はアプリケーションをメンテナンスモードに切り替え、ユーザーが操作できない状態にしてから DB の統合を行うことにしました。
メンテナンスモードを実現するために以下の 3 つの要件がありました
- 1. ユーザーが操作できない状態を作る
- 2. メンテナンスモードを設定しているときは社内の人間のみ画面を操作できるようにする
- 3. 外部サービスから飛んでくる Webhook はブロックせず正常系で返し、ログに内容を出しておく
1 に関してはフロントエンドでメンテナンス用のページを返すだけで実現できますが、2 の要件を満たすことができません。
そこで、今回は WAF を使うことにしました。WAF では、エンジニアの自宅 IP を通すようにし、それ以外の IP はブロックしてメンテナンス用の HTML を返すようにしました。
3 に関しては、メンテナンス中に Webhook が飛んできて DB が更新されるのを防ぐのが目的です。Webhook の内容をログに記録し、HTTP ステータス 200 を返すことで DB まで到達しないようにしました。
アプリケーションの修正
アプリケーション側の修正もいくつか行いました。
- 新 DB に繋げるようにデータベース接続設定を変更
- SQLBoiler を使用してモデルの再生成(スキーマ名の変更に伴う)
- テストコードの修正
- その他細かい修正
自動生成したコードが多いのですが、差分はこのようになりました。
dev 環境での検証
弊社は dev と production の 2 環境があります(staging は現在作成中です)。
dev 環境でスナップショットを作成し、スナップショットから復元した temporary なクラスターを使うことで、複数回の DB 統合の検証を行うことができました。
ローカルで検証を行うには限界があるので、この方法のおかげで何度も試行錯誤を重ねることができました。
production 環境での実行
production の DB 統合当日、下記の手順で進めました。メンテナンス時間は約 2 時間でした。
- 外部サービスから飛んでくる Webhook をログに出して return し、DB に到達しないように変更
- WAF を使ったメンテナンスモードを設定し、ユーザーが画面を操作できないようにする
- データ移行 CLI を使ったデータ移行
- 新 DB に接続するようにした Go アプリケーションのデプロイ
- 動作確認
- 外部サービスから飛んでくる Webhook 処理を DB まで到達するように戻す
- メンテナンス中に飛んできていた Webhook があるか確認
- メンテナンスモード解除
- ユーザーへメンテナンス終了のアナウンス
今回の DB 統合で下記のようになりました。
統合前は merchant DB と transfer DB という 2 つの独立したデータベースがありましたが、統合後は単一の DB にすべてのテーブルが集約されました。これにより、トランザクション境界の管理が適切に行えるようになり、データの整合性を保証できる構成になりました。
結果と学び
無事に DB 統合を終えることができました。
感じたこと、学んだことをいくつか挙げます。
- Claude Code をフル活用して実装の短縮ができた
- dev や staging 環境で繰り返し検証することが大事
- サービス立ち上げ時から初期のフェーズはシンプルな構成で作ったほうが変化に強く、開発速度も上がる
- 判断が難しいところではあるが、データ量・コード量・ユーザー数が増える前に大きめの改修をしておいたほうがコスパがいい
おわりに
約 1 ヶ月半かけて行った DB 統合について紹介しました。
1 ヶ月半のうち 1 ヶ月は私はこのプロジェクトに張り付きになりましたが、他のメンバーが新規開発を行ってくれていたのでチーム全体のアウトプットはそこまで落とすことなく進めることができました。これも AI によって開発効率が上がったことによるものだと思います。
今回無事に Phase1 を終えたので、次は Phase2 に向けて ADR を書いたりサンプル実装を作ったりと進めていき、開発効率を高められるようにチーム一丸となって取り組んでいきます。
RemitAid では一緒に働く仲間を募集しています。
興味がある方はこちらからどうぞ!
Podcast 「RemiTalk」を最近始めましたので、もし良ければ聴いてみてください!
Podcast 文字起こしはこちら
Discussion