Tech Blog

街中の電動キックボードシェアリングを自作して理解する — 設計・実装・運用の記録シリーズ

Flutter Spring Boot Java PostgreSQL Mobility IoT 設計

はじめに

ここ数年、街中の駅前やコンビニ駐車場の隅に 電動キックボードシェアリング のポートが当たり前のように置かれるようになりました。アプリでロック解除して乗り、目的地のポートで返却する — 利用者として使う分にはとてもシンプルです。

ただ、エンジニアとして触っていると次のような疑問が湧いてきます。

  • 地図上に並ぶ「利用可能機体」「空き枠」の数値は、どうやってリアルタイムに集計されているのか
  • アプリのボタンを押すと、なぜ路上の物理的なキックボードが解錠されるのか
  • 「ここで返却できない」という判定は、サーバーのどこで・どんな情報をもとに行っているのか
  • 1台の機体に対して「ユーザーが乗ろうとしている」「作業員がバッテリーを抜こうとしている」「管理者が遠隔停止しようとしている」が同時に起きたら、どう競合制御するのか

これらは外から眺めていても見えてきません。

このシリーズは、それらを自分なりに考察 → 設計 → 製造することで理解しよう という個人プロジェクトの記録です。完成品の紹介ではなく、「街でよく見るあのサービスを、もし自分が一から作るなら何を考えるか」 の試行錯誤を、設計判断と実装の両面から残しています。

このプロジェクトは現在も開発中です。 シリーズ各記事は完成報告ではなく、進行中の設計・実装の記録として書いています。 機能追加や設計変更に合わせて随時更新していく予定です。


なぜ電動キックボードシェアリングを題材に選んだか

題材として電動キックボードシェアリングを選んだ理由は3つあります。

1. 街で見かける身近なサービスでありながら、内部構成が非自明である

利用者として使うのは簡単ですが、裏側には モバイルアプリ・REST API・データベース・IoT 通信・外部決済・地図サービス といった、現代の業務システムでよく出てくる要素が一通り揃っています。1つの題材で「モバイル UI」「サーバーサイド」「DB 設計」「外部連携」「リアルタイム通信」を横断的に考えられます。

2. 複数のアクターが同じデータを取り合う構造を持っている

ユーザー(乗る人)・作業員(バッテリー交換する人)・管理者(遠隔監視する人)の3者が、同じ「機体」というリソースを別の操作目的で取り合います。状態遷移や排他制御の練習として現実的な題材です。

3. 「設計を書きながら作る」スタイルの試金石になる

要件定義 → 基本設計 → 詳細設計 → 実装、という古典的な流れを 個人プロジェクトでも回せるか を試したいと思っていました。一人で設計書を書いて一人で実装すると、書いた本人が設計書を無視し始めて崩壊しがちです。それを どう防ぐか も含めて、運用面の知見を残したかった。


想定したシステムの全体像

設計の出発点として、3アクター × 3アプリケーション + バックエンド + IoT という構成を置きました。

[利用者層]
  一般ユーザー          作業員(Swapper)         管理者
      |                      |                      |
      v                      v                      v
+----------------+   +----------------+   +-------------------------+
| ユーザーアプリ |   | 作業員アプリ   |   | 管理者システム (SSR)    |
| Flutter        |   | Flutter        |   | Spring Boot Thymeleaf   |
+----------------+   +----------------+   +-------------------------+
         \                  |                  /
          \                 |                 /
           +----------------+----------------+
                            |
                            v
                +--------------------------+
                | バックエンドAPI基盤      |
                | Java 21+ Spring Boot     |
                +--------------------------+
                 |        |         |       \
                 v        v         v        v
        +----------------+ +----------------+ +----------------------+
        | 認証認可       | | 業務機能       | | IoTゲートウェイ      |
        | FIDO2/JWT/RBAC | | Ride/Fleet/Swap| | MQTT Pub/Sub         |
        +----------------+ +----------------+ +----------------------+
                                                       |
                                                       v
                                            +----------------------+
                                            | MQTT Broker          |
                                            +----------------------+
                                                       |
                                                       v
                                            +----------------------+
                                            | 車載IoTデバイス      |
                                            | BMS/GPS/Smart Lock   |
                                            +----------------------+
アプリ技術スタック対象ユーザー
ユーザーアプリFlutter (iOS/Android)キックボードを利用する顧客
作業員アプリFlutter (iOS/Android)バッテリー交換・現場整備をする作業員
管理者システムSpring Boot + Thymeleaf (SSR)稼働状況監視・遠隔制御をする運営者
バックエンドAPIJava 21+ / Spring Boot 3.x3アプリの共通基盤
データベースPostgreSQL + PostGIS位置データのポリゴン演算が必要なため
IoT通信MQTT Broker → 車載デバイス解錠コマンド・テレメトリ受信
外部連携FIDO2/SMS/eKYC/Stripe/地図/通知認証・本人確認・決済・地図

外部依存(SMS / 決済 / eKYC / IoT)は本物に繋ぐ前に dev profile でモック化 して、まずは「画面を一通り動かしてフロー全体が成立するか」を確認できる構成にしました。本シリーズの記事の多くが、この dev profile での割り切り判断を扱っています。


採用技術と選定理由

技術選定理由
モバイルアプリFlutter 3.xiOS/Android 両対応で UI コードを単一に保ちたい
バックエンドAPIJava 21 + Spring Boot 3.xエンタープライズ実績と、JPA/Security/Profile 等の枠組みが揃っている
管理者システムSpring Boot + Thymeleafバックエンドと同言語・同フレームワークで認知負荷を最小化
データベースPostgreSQL 17 + PostGISポートのポリゴン判定、機体位置の空間検索
IoT通信MQTT低遅延・高到達率で双方向のデバイス通信ができる
認証FIDO2/WebAuthn + JWTパスキーを主、JWT はステートレス API セッション
状態管理 (Mobile)Riverpod画面横断の状態と非同期取得を整理
HTTP (Mobile)dioリクエスト/レスポンスのインターセプタが書きやすい
地図表示flutter_map (OpenStreetMap)ライセンスを気にせず開発できる

用語・画面ID早見表

シリーズ各記事には、設計書側で割り振った識別子がそのまま出てきます。第三者の方が記事を読む際の参照表として、ここに一覧を置いておきます。

画面ID(ユーザーアプリ S01〜S13)

ユーザーアプリの画面遷移仕様で使っている識別子です。記事中で「S02 ホーム画面で…」のように出てきます。

ID画面名役割
S01スプラッシュアプリ起動・セッション確認
S02ホームマップ現在地周辺のポート・空き機体を地図表示
S03QRスキャン利用する機体のQRコード読取
S04目的地ポート選択返却予定の空きポートを地図から選択
S05ログイン / SMS認証電話番号 + OTP によるアカウント認証
S06クレジット登録決済手段(カード等)の登録
S07交通ルールテスト法令で義務付けられたテストの確認
S08eKYC書類提出免許証等の撮影・本人確認
S09ライド開始確認機体・目的地・料金の最終確認、解錠
S10ライド中走行中ナビ・経過時間・課金額表示
S11返却確認・写真返却ポート判定・証拠写真撮影
S12レシート利用時間・決済金額の表示
S13アカウントメニューユーザー情報・履歴・FAQ

設計運用で使う識別子

設計と実装の乖離管理のために導入したIDです。詳しくは 設計を実装でぶれさせない運用 を参照してください。

識別子意味
DVG-XXX乖離ID — 設計と実装のズレ1件を表す通し番号(Divergence の略)
TASK-XX実装差分タスクID — 乖離を解消する実装作業1件の通し番号
app-apiアプリ(Flutter)と API(Spring Boot)の 両側を巻き込む課題 であることを示すスコープ表記

開発で取り上げたテーマと記事マップ

このシリーズで書いた各記事は、上記システムを作る過程で 設計判断や実装で詰まったポイント を1テーマずつ切り出したものです。各記事はテーマを絞っているので、特定の技術や設計パターンを調べたいときに単独でも参照できます。


■ システム全体を最短で動かす

テーマ記事
ホーム地図 → 乗車 → 返却 → レシートまでを最小E2Eで通す電動キックボードシェアリングを最小E2Eまで動かすまで — Flutter + Spring Boot を dev profile で割り切る

■ モバイル UI 実装

テーマ記事
地図ライブラリを Google Maps から OpenStreetMap に変更した判断と OSM タイル利用マナーFlutter の地図ライブラリを Google Maps から flutter_map (OpenStreetMap) に変更した話 — 学習プロジェクトでの選定基準とOSMタイル利用マナー
ポートタップ時の機体一覧シートを DraggableScrollableSheet で作るBottomSheet でポート内機体選択 UI を作る — Flutter の DraggableScrollableSheet と FutureBuilder の組み合わせ

■ バックエンド・dev profile での割り切り

テーマ記事
PoC で「認証通った前提」に最小限の Filter で乗せるSpring Profile で dev だけ認証を緩める設計 — prod の構造を壊さない最小限の Filter
起動時に固定 seed データ(駅周辺ポート + 異常系混在)を投入DevPortSeedInitializer で開発DBを毎回同じ状態にする — Spring Boot の CommandLineRunner 活用

■ 設計と実装をぶれさせない運用

テーマ記事
乖離管理表・実装差分タスク一覧・三点突合表で「正本」を守る設計を実装でぶれさせない運用 — 乖離管理表と実装差分タスク一覧で「正本」を守る

進行中・未着手の領域

この項目は開発進捗に合わせて更新します。

  • FIDO2/WebAuthn パスキーの本実装(現状は dev profile で擬似的にバイパス)
  • Stripe を本物に繋ぐ決済フロー(現状はサービス層でモック応答)
  • eKYC ベンダー連携(現状は固定レスポンスを返すモック)
  • MQTT Broker と車載デバイス疑似クライアントの常時通信
  • 管理者システム(Thymeleaf SSR)のフリートマップ
  • 作業員アプリのバッテリー交換タスク導線

おわりに

このプロジェクトの目的は「完成した電動キックボードシェアリングサービス」を作ることではなく、「街中で見るあのサービスは、もし自分が作るなら何を考えるか」を実装と設計で記録すること です。

そのため、シリーズ各記事は「正解の実装」ではなく 「この場面ではこう判断して、そのために何を割り切ったか」 の記録として書いています。同じテーマを扱うときに、判断の選択肢を増やす材料になれば嬉しいです。

各記事は単独で読める粒度に絞ってあるので、興味のあるテーマから読み始めてください。シリーズ全体としては、上の記事マップの順番で読むと「最小E2E → モバイル UI → バックエンド割り切り → 設計運用」と、システム構築の流れに沿った読み方ができます。

気軽にメッセージください

技術相談・ご感想・ご質問があればメッセージをお願いします。