Tech Blog

Building Street-Level E-Scooter Sharing From Scratch to Understand It — Design, Implementation, and Operations Series

by Tech Writer
Flutter Spring Boot Java PostgreSQL Mobility IoT Design

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   |
                                            +----------------------+
ApplicationStackTarget User
User AppFlutter (iOS/Android)Riders
Swapper AppFlutter (iOS/Android)Battery swappers, field maintainers
Admin ConsoleSpring Boot + Thymeleaf (SSR)Operations monitoring and remote control
Backend APIJava 21+ / Spring Boot 3.xCommon platform for the three apps
DatabasePostgreSQL + PostGISPolygon math for ports requires spatial extensions
IoTMQTT Broker → on-vehicle deviceUnlock commands and telemetry
External servicesFIDO2/SMS/eKYC/Stripe/maps/notificationsAuthN, 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

LayerTechWhy
Mobile appFlutter 3.xSingle codebase for iOS/Android
Backend APIJava 21 + Spring Boot 3.xEnterprise track record; JPA / Security / Profile already there
Admin consoleSpring Boot + ThymeleafSame language and framework as backend — minimizes cognitive load
DatabasePostgreSQL 17 + PostGISPort polygon checks, scooter spatial queries
IoTMQTTLow-latency bidirectional device messaging
AuthenticationFIDO2/WebAuthn + JWTPasskeys as primary; JWT for stateless API session
State management (mobile)RiverpodCross-screen state and async fetch coordination
HTTP (mobile)dioInterceptors are easy to write
Map renderingflutter_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)

IDScreenRole
S01SplashApp startup, session check
S02Home mapDisplay nearby ports and available scooters on the map
S03QR scanRead the QR of the scooter to use
S04Destination port selectPick the return port from the map
S05Login / SMS authPhone number + OTP account auth
S06Payment registrationCard or similar payment method registration
S07Traffic rules testLegally required pre-ride test
S08eKYC documentsLicense capture and identity verification
S09Ride start confirmFinal confirm of vehicle / destination / fare; unlock
S10Ride in progressIn-ride navigation, elapsed time, running fare
S11Return confirm + photoReturn-port judgment, evidence photo
S12ReceiptDisplay final time and amount
S13Account menuUser 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.

IdentifierMeaning
DVG-XXXDivergence ID — one number per divergence between design and implementation
TASK-XXImplementation-diff task ID — one number per work item that closes a divergence
app-apiA 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

TopicArticle
Home map → ride → return → receipt as one minimal E2E runDriving an E-Scooter Sharing App Through a Minimal E2E Run — Flutter + Spring Boot with dev profile shortcuts

■ Mobile UI implementation

TopicArticle
Why the map library changed from Google Maps to OpenStreetMap, and how to play nice with OSM tilesSwitching 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 DraggableScrollableSheetBuilding a Port-Scoped Scooter Selection UI With a BottomSheet — Flutter’s DraggableScrollableSheet + FutureBuilder Combo

■ Backend / dev-profile shortcuts

TopicArticle
Lifting a PoC onto “as-if authenticated” with the smallest possible filterRelaxing 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 startupResetting the Dev DB to the Same State Every Time With DevPortSeedInitializer — Spring Boot’s CommandLineRunner in Practice

■ Operations that keep design and implementation aligned

TopicArticle
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.

Feel free to send a message

Please send a message if you have any technical questions, feedback, or inquiries.