梱包業界向けSaaS
梱包業界向けに本番稼働しているWebおよびモバイルのSaaSでのフルスタック開発。データ量の増加によって、すべての顧客で応答性が低下していた状況でした。
Webクライアント。フロントエンドのためのバックエンド(BFF)も兼ねる。
- React
- Next.js
- dev
- staging
- production
モバイルクライアント。
- React Native
- Expo
- dev
- staging
- production
クライアントに置くべきではない、特定の重い処理。
- Lambda
- dev
- staging
- production
00 / 概要
2024年12月から2025年12月までの契約業務です。対象は梱包業界向けに本番稼働しているSaaSで、Webとモバイルのクライアントがあります。現在のCVによると、この契約期間中、プロダクトは日本国内の10社以上で利用されていました。プロジェクトチームは約10名でした。
主な業務は、データ量の増加によってすべての顧客でプロダクトが遅くなった状態から、応答性を取り戻すことでした。並行して、Node.jsのランタイムをバージョン16から20へ移行し、コードベースに加わる外部開発者の支援も行いました。
01 / 課題
プロダクトは、それが前提としていた想定を超えて成長していました。保存データが積み上がるにつれ、読み込み時間と全般的な応答性がプラットフォーム全体で悪化しました。特定の顧客や特定の画面だけではなく、使っているすべての人に影響していました。
原因は症状より下の層にありました。データベース構造は、プロダクトを他の企業へ展開する前に短期間で組み上げられたもので、そのドキュメントは最新の状態ではありませんでした。既存のデータモデルがどう動いているのかについて、チームに正確な共通理解がなかったため、安全に変更するにはまず現状の挙動を把握する必要がありました。
02 / 制約
- 稼働中の顧客。 顧客企業はこのシステムに依存していたため、変更は顧客の業務を止めずに本番へ反映する必要がありました。
- 分かれたデプロイ。 Webクライアント、モバイルクライアント、バックエンドは別々のリポジトリにあり、それぞれ独自のAmplifyアプリとdev、staging、productionの環境を持っていました。共通データへの変更は、その3つすべてで整合している必要がありました。
- 信頼できる地図がない。 ドキュメントが古くなっていたため、確かな情報源はコードと稼働中のシステムだけでした。
03 / 診断
全体像は、ドキュメントではなく事実から組み立て直しました。コードを読み、クエリをプロファイリングし、Amazon CloudWatchでリクエストの挙動を追いました。コードが追いにくい箇所は、手を入れる前にデータの流れを図に描きました。
この遅さは単一の不具合ではなく、データが増えるほど互いに効き合う複数のパターンでした。
- テーブル全体を読むDynamoDBのScanを、キー指定のQueryで足りる箇所で使っていたこと。
- グローバルセカンダリインデックス(GSI)の不足。頻出のアクセスパターンに効率的な経路がない状態。
- 実際のデータの読み方と一致していないパーティションキーとソートキー。
- AppSyncのリゾルバーでのN+1リクエスト。一覧を取る1回の後に、要素ごとにもう1回。
- 画面が必要とする分ではなく、データセット全体をクライアントへ読み込んでいたこと。
- ページネーションがないこと。
どれも、テーブルが小さく顧客が1社であれば許容できます。しかし、利用企業が増えていく中でこれらが重なり、プラットフォーム全体が遅くなりました。
04 / 変更内容
- 検索が実際に実行するクエリに合わせて設計したインデックスと、プロダクトがデータを読む実態に合わせたアクセスパターン。
- ページネーション。画面は表示する分だけを要求する形に。
- 独立した検索の仕組み。
- キャッシュの導入。
- N+1パターンを取り除くためのリゾルバーの修正。
- そもそもクライアントが出す必要のなかったリクエストの削除。
- 案件テーブル、つまり梱包案件を発生から完了まで引き回すレコードを、新しい構造へ移行。
- 大量データをエクスポートする経路のタイムアウト対応。旧構造の限界が最も目に見えて表れていた箇所。
作業が終わった時点で、データモデルのドキュメントを更新しました。次の変更が、またリバースエンジニアリングから始まらないようにするためです。
05 / 進め方
変更は複数のマイルストーンに分けてリリースしました。各マイルストーンはstagingで一段ずつ検証し、並行してチームのエンジニアがコードレビューを行いました。本番リリースは、顧客がシステムを使わない夜間に実施しました。
Node.js 16 → 20の移行では、依存関係の問題とLambdaランタイム側の問題に当たりました。検証は、自動テストと、あらかじめ決めた手順の手動確認で行いました。テナント(自社の案件を内部に持つ1つの梱包会社)をセットアップし、その会社がするようにSaaSを使う、という手順です。
プロジェクトに加わる外部の業務委託メンバーの支援も行いました。コードレビュー、オンボーディング、ドキュメントの作成、最初のタスクを一緒に進めることです。
この契約はパフォーマンスだけではありません。プロダクトは梱包案件を端から端まで追います。案件を登録し、見積もりを出し、梱包を設計し、梱包し、請求し、コンテナへの積み付けを計画し、トラックに積み込むまでです。私はこの流れ全体にわたる機能の実装と改善を担当しました。スケジュールの再読み込み、見積もり、請求、梱包の各フロー、画面の自動遷移、通知などです。あわせて、モバイルのレイアウトとリスト操作の問題を修正し、プロダクトをさらにモバイルへ広げるためのPoC(技術検証)も実施しました。
06 / 結果
変更後、検索のタイムアウトは減り、顧客からのクレームも大きく減りました。プロダクトとその普及は約10名のチーム全体の成果であり、私の貢献は上に述べたエンジニアリングの部分です。
07 / 振り返り
いまなら変えることが2つあります。
1つ目は、リリースがうまくいかなかった場合に本番をどう元の状態へ戻すかを、変更ごとに事前に計画することです。dev、staging、productionと段階を踏むことはリスクを下げますが、本番リリースの成功を保証はしません。データ移行は、まさにその差が痛い場面です。
2つ目は、変更が他のコンポーネントに与える影響を、検証の途中ではなく着手前に洗い出すことです。1つのデータ層を共有する3つの独立したデプロイ単位があるなら、その地図は先に描く価値があります。