はじめに
割り勘アプリ wa/ri は、iOS / Android / Web / LINE(LIFF)の4プラットフォームに対応している。
クライアントが4つあると、普通に考えれば「同じロジックを4回実装する」か「共通ライブラリを作って各クライアントに持たせる」という方向に進みがちだ。
wa/riではどちらも選ばなかった。割り勘の計算ロジックはすべてバックエンド(NestJS)に集約し、クライアントはAPIを叩くだけという設計にした。
その理由と、実際に運用してみての効果を書く。
構成の全体像
クライアント(iOS / Android / Web / LIFF)
↓ API呼び出し
バックエンド(NestJS)← ここにロジックが全部ある
↓
PostgreSQL(Prisma ORM)
クライアントはUIの描画とAPIとの通信に専念する。計算・精算ロジックはバックエンドが持つ。
なぜフロントで計算させないのか
問題①:重複実装のコスト
割り勘の計算ロジックは、wa/riの場合そこそこ複雑だ。
- 借方/貸方の積み上げ(簿記ベースのデータ構造)
- 3フェーズの段階的相殺(自己相殺 → 二者間相殺 → 債権譲渡)
- 計算と同時に人間が読める履歴(tradeHistory)を逐次生成
これを Swift・Dart(Flutter)・場合によってはJavaScript(LIFF)で別々に実装すると、コードが3〜4本に増える。 バグも3〜4箇所に生まれる可能性がある。
問題②:テストのコスト
ロジックが各クライアントに分散していると、テストも分散する。
- Swiftでテストを書く
- DartでもFlutterのテストを書く
- WebやLIFF用にもテストを書く
同じロジックに対して複数の言語でテストを書くのは、個人開発において現実的ではない。
問題③:修正のコスト
バグを直すとき、ロジックが各クライアントにあると全クライアントを修正してリリースし直す必要がある。
iOSとAndroidはストア審査がある。審査が通るまで修正が反映されない。Web・LIFFはすぐ反映できても、iOSだけ古い挙動のままになる、という状態が発生しうる。
バックエンド集約で何が変わったか
テストはバックエンドだけ書けばいい
ロジックがNestJSに集約されているため、バックエンドのテストを通れば全クライアントの正しさが担保される。
wa/riでは計算ロジックをモジュールとして独立させており、そこにテストを集中させている。iOSのコードを触らなくても、Dartのコードを触らなくても、バックエンドのテストがグリーンであれば全クライアントで正しく動く。
修正はサーバー更新だけで完結する
バックエンドを直してデプロイすれば、iOS / Android / Web / LIFF のすべてに即反映される。
ストア審査もビルド待ちも不要。個人開発でマルチプラットフォームをやっている身としては、これは実運用でかなり効いている。
クライアントの実装がシンプルになる
クライアントはAPIを叩いてレスポンスを表示するだけでいい。UIロジックと計算ロジックが明確に分離されるため、各クライアントのコードが見通しやすくなる。
トレードオフ
正直に言うと、この設計にはデメリットもある。
オフライン対応が難しい
ロジックがサーバーにあるため、ネットワークがない状態では計算ができない。
wa/riは「友人との割り勘」という用途上、オフラインで使うシーンはほぼないと判断したが、用途によってはこれが致命的になる。
レイテンシの問題
計算のたびにAPIリクエストが発生する。ローカルで完結する計算と比べると、レスポンスに数百ミリ秒の遅延が生じる。
UX上問題にならないレベルに収めるための工夫(ローディング表示、楽観的更新など)は別途必要になる。
個人開発こそこの設計が効く
チームで開発する場合、クライアントとサーバーを別チームが担当するケースも多い。その場合はクライアント側にある程度ロジックを持たせる方が開発しやすいこともある。
一方、個人開発でマルチプラットフォームをやる場合、この設計は特に有効だ。
- 全部自分で書く
- テストも自分で書く
- バグ修正も自分でやる
この状況で「同じロジックを複数の言語で複数回書く」のは、明らかに持続不可能だ。ロジックを一箇所に集めることで、個人の認知コストと保守コストを大幅に下げられる。
まとめ
「フロントで計算させない」という設計の本質は、ロジックの責務をどこに置くかを明確にすることだ。
wa/riの場合、それはバックエンドだった。その選択によって、テスト容易性・修正コスト・クライアントのシンプルさという3つの恩恵を同時に得られた。
マルチプラットフォームの個人開発を考えている人に、一つの参考になれば。

No responses yet