街中の電動キックボードシェアリングを自作して理解する — 設計・実装・運用の記録シリーズ
はじめに
ここ数年、街中の駅前やコンビニ駐車場の隅に 電動キックボードシェアリング のポートが当たり前のように置かれるようになりました。アプリでロック解除して乗り、目的地のポートで返却する — 利用者として使う分にはとてもシンプルです。
ただ、エンジニアとして触っていると次のような疑問が湧いてきます。
- 地図上に並ぶ「利用可能機体」「空き枠」の数値は、どうやってリアルタイムに集計されているのか
- アプリのボタンを押すと、なぜ路上の物理的なキックボードが解錠されるのか
- 「ここで返却できない」という判定は、サーバーのどこで・どんな情報をもとに行っているのか
- 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) | 稼働状況監視・遠隔制御をする運営者 |
| バックエンドAPI | Java 21+ / Spring Boot 3.x | 3アプリの共通基盤 |
| データベース | PostgreSQL + PostGIS | 位置データのポリゴン演算が必要なため |
| IoT通信 | MQTT Broker → 車載デバイス | 解錠コマンド・テレメトリ受信 |
| 外部連携 | FIDO2/SMS/eKYC/Stripe/地図/通知 | 認証・本人確認・決済・地図 |
外部依存(SMS / 決済 / eKYC / IoT)は本物に繋ぐ前に dev profile でモック化 して、まずは「画面を一通り動かしてフロー全体が成立するか」を確認できる構成にしました。本シリーズの記事の多くが、この dev profile での割り切り判断を扱っています。
採用技術と選定理由
| 層 | 技術 | 選定理由 |
|---|---|---|
| モバイルアプリ | Flutter 3.x | iOS/Android 両対応で UI コードを単一に保ちたい |
| バックエンドAPI | Java 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 | ホームマップ | 現在地周辺のポート・空き機体を地図表示 |
| S03 | QRスキャン | 利用する機体のQRコード読取 |
| S04 | 目的地ポート選択 | 返却予定の空きポートを地図から選択 |
| S05 | ログイン / SMS認証 | 電話番号 + OTP によるアカウント認証 |
| S06 | クレジット登録 | 決済手段(カード等)の登録 |
| S07 | 交通ルールテスト | 法令で義務付けられたテストの確認 |
| S08 | eKYC書類提出 | 免許証等の撮影・本人確認 |
| 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 → バックエンド割り切り → 設計運用」と、システム構築の流れに沿った読み方ができます。