Building Street-Level E-Scooter Sharing From Scratch to Understand It — Design, Implementation, and Operations Series
Introduction
Over the past few years, electric kickboard sharing ports have appeared at train stations, in convenience-store parking lots, and on city sidewalks. Tapping the app to unlock a scooter, riding it, and locking it at a destination port — as a user, the experience is simple.
But as an engineer, simple consumer flows tend to raise questions:
- How are the “available scooters” and “empty slot” counts on the map computed in near real time?
- How does pressing a button in the app cause the physical lock on a kickboard to release on the street?
- Where on the server, and based on what data, is the “this port cannot accept a return” judgment made?
- If a user tries to unlock a scooter at the same moment a swapper tries to remove its battery and an admin tries to remotely disable it, how is the contention resolved?
None of this is visible from the outside.
This series is the record of trying to understand all of that by speculating, designing, and building it from scratch — as a personal project. The articles aren’t a showcase of a finished product. They’re a record of the trial-and-error: “If I had to build this street-level service from zero, what would I have to think through?” — captured from both a design and an implementation angle.
This project is actively in development. Each article in this series is a record of in-flight design and implementation, not a completion report. New articles and design notes are added as features evolve.
Why E-Scooter Sharing as the Subject
There are three reasons.
1. It’s a service everyone has seen on the street, but the internals are non-obvious
As a user, it’s trivial. Under the hood, it spans mobile apps, REST APIs, databases, IoT messaging, external payments, and map services — a complete cross-section of what shows up in modern systems. One subject lets you exercise mobile UI, server-side logic, schema design, external integrations, and real-time communication in parallel.
2. Multiple actors contend for the same data
Users (who ride), swappers (who change batteries), and admins (who monitor remotely) all touch the same “scooter” resource for different operational purposes. State machines and concurrency become an honest practical concern, not a textbook exercise.
3. It’s a test of “design-while-building”
I wanted to test whether the classic Requirements → Architectural → Detailed → API design → Implementation flow can actually be sustained on a solo personal project. When the same person writes the design and the code, the designer-self tends to start ignoring the design — and the whole thing collapses. Capturing how to prevent that was an explicit goal.
The System I Set Out to Build
The starting architecture is three actors × three applications + a backend + IoT.
[User Tier]
General User Swapper Administrator
| | |
v v v
+----------------+ +----------------+ +-------------------------+
| User App | | Swapper App | | Admin Console (SSR) |
| Flutter | | Flutter | | Spring Boot Thymeleaf |
+----------------+ +----------------+ +-------------------------+
\ | /
\ | /
+----------------+----------------+
|
v
+--------------------------+
| Backend API Platform |
| Java 21+ Spring Boot |
+--------------------------+
| | | \
v v v v
+----------------+ +----------------+ +----------------------+
| AuthN / AuthZ | | Business Logic | | IoT Gateway |
| FIDO2/JWT/RBAC | | Ride/Fleet/Swap| | MQTT Pub/Sub |
+----------------+ +----------------+ +----------------------+
|
v
+----------------------+
| MQTT Broker |
+----------------------+
|
v
+----------------------+
| On-Vehicle IoT |
| BMS/GPS/Smart Lock |
+----------------------+
| Application | Stack | Target User |
|---|---|---|
| User App | Flutter (iOS/Android) | Riders |
| Swapper App | Flutter (iOS/Android) | Battery swappers, field maintainers |
| Admin Console | Spring Boot + Thymeleaf (SSR) | Operations monitoring and remote control |
| Backend API | Java 21+ / Spring Boot 3.x | Common platform for the three apps |
| Database | PostgreSQL + PostGIS | Polygon math for ports requires spatial extensions |
| IoT | MQTT Broker → on-vehicle device | Unlock commands and telemetry |
| External services | FIDO2/SMS/eKYC/Stripe/maps/notifications | AuthN, KYC, payments, maps |
External dependencies (SMS / payments / eKYC / IoT) are mocked under the dev profile before any real integration. The goal is to first confirm “every screen flows end-to-end” before plugging in the real stuff. Many of the articles in this series are about these dev-profile shortcut decisions.
Technology Choices
| Layer | Tech | Why |
|---|---|---|
| Mobile app | Flutter 3.x | Single codebase for iOS/Android |
| Backend API | Java 21 + Spring Boot 3.x | Enterprise track record; JPA / Security / Profile already there |
| Admin console | Spring Boot + Thymeleaf | Same language and framework as backend — minimizes cognitive load |
| Database | PostgreSQL 17 + PostGIS | Port polygon checks, scooter spatial queries |
| IoT | MQTT | Low-latency bidirectional device messaging |
| Authentication | FIDO2/WebAuthn + JWT | Passkeys as primary; JWT for stateless API session |
| State management (mobile) | Riverpod | Cross-screen state and async fetch coordination |
| HTTP (mobile) | dio | Interceptors are easy to write |
| Map rendering | flutter_map (OpenStreetMap) | License-free; suitable for development without account setup |
Glossary — Screen IDs and Operational IDs
Identifiers that came from the design docs appear verbatim in series articles. Use this lookup table when you hit something like “S02”.
Screen IDs (User App, S01–S13)
| ID | Screen | Role |
|---|---|---|
| S01 | Splash | App startup, session check |
| S02 | Home map | Display nearby ports and available scooters on the map |
| S03 | QR scan | Read the QR of the scooter to use |
| S04 | Destination port select | Pick the return port from the map |
| S05 | Login / SMS auth | Phone number + OTP account auth |
| S06 | Payment registration | Card or similar payment method registration |
| S07 | Traffic rules test | Legally required pre-ride test |
| S08 | eKYC documents | License capture and identity verification |
| S09 | Ride start confirm | Final confirm of vehicle / destination / fare; unlock |
| S10 | Ride in progress | In-ride navigation, elapsed time, running fare |
| S11 | Return confirm + photo | Return-port judgment, evidence photo |
| S12 | Receipt | Display final time and amount |
| S13 | Account menu | User info, history, FAQ |
Operational identifiers used in the design process
These are introduced for managing the gap between design and implementation. See Keeping Implementation From Drifting From Design for the full story.
| Identifier | Meaning |
|---|---|
DVG-XXX | Divergence ID — one number per divergence between design and implementation |
TASK-XX | Implementation-diff task ID — one number per work item that closes a divergence |
app-api | A scope tag indicating that an issue spans both the Flutter app and the Spring Boot API |
Topics Covered and Article Map
Each article extracts one specific design decision or implementation snag encountered while building this system. Articles are scoped narrowly, so you can also read them in isolation when looking up a particular technique.
■ Driving the system end-to-end the shortest way
| Topic | Article |
|---|---|
| Home map → ride → return → receipt as one minimal E2E run | Driving an E-Scooter Sharing App Through a Minimal E2E Run — Flutter + Spring Boot with dev profile shortcuts |
■ Mobile UI implementation
| Topic | Article |
|---|---|
| Why the map library changed from Google Maps to OpenStreetMap, and how to play nice with OSM tiles | Switching the Flutter Map Library From Google Maps to flutter_map (OpenStreetMap) — Selection Criteria for a Learning Project and OSM Tile Etiquette |
The scooter list sheet on port tap, built with DraggableScrollableSheet | Building a Port-Scoped Scooter Selection UI With a BottomSheet — Flutter’s DraggableScrollableSheet + FutureBuilder Combo |
■ Backend / dev-profile shortcuts
| Topic | Article |
|---|---|
| Lifting a PoC onto “as-if authenticated” with the smallest possible filter | Relaxing Authentication Only in dev with a Spring Profile — A Minimal Filter That Doesn’t Disturb the prod Structure |
| Injecting fixed seed data (station-area ports + anomaly cases) at startup | Resetting the Dev DB to the Same State Every Time With DevPortSeedInitializer — Spring Boot’s CommandLineRunner in Practice |
■ Operations that keep design and implementation aligned
| Topic | Article |
|---|---|
| Divergence register, implementation-diff tasks, three-way cross-check that protect the “source of truth” | Keeping Implementation From Drifting From Design — A Divergence Register and Three-Way Cross-Check That Protect the ‘Source of Truth’ |
In Progress / Not Yet Addressed
Updated as the project progresses.
- Real FIDO2/WebAuthn passkey implementation (currently bypassed in dev profile)
- Connecting Stripe for real payments (currently mocked at the service layer)
- eKYC vendor integration (currently a static response mock)
- Steady-state MQTT Broker + on-vehicle device pseudo-client
- Fleet map on the admin console (Thymeleaf SSR)
- Battery swap task flow on the swapper app
Closing
The goal of this project is not to ship “a finished e-scooter sharing service.” It’s to record “if I had to build the service everyone sees on the street, what would I have to think through?” in design and implementation.
So each article is not “the correct implementation” but rather a record of “in this situation I made this judgment call, and here’s what I accepted as a tradeoff.” If that broadens the option space the next time someone faces the same topic, that’s the win.
Read in any order. If you want to follow the build order, the article map above is roughly that order: minimal E2E → mobile UI → backend shortcuts → design operations.