RideFlow is a simplified Java/Spring Boot ride-hailing backend inspired by Uber. The first milestone focuses on:
- JWT-based rider and driver authentication
- Driver online/offline status and live location updates
- Redis-backed nearest-driver lookup
- Ride request flow with a strict ride state machine
- Concurrency-safe ride acceptance with database locking
- Trip start and completion with pricing and simulated payment
- WebSocket/STOMP ride updates and driver offer notifications
- A built-in rider/driver demo UI served from
/
- Java 21
- Spring Boot 3.5
- PostgreSQL + PostGIS
- Redis
- Flyway
- Spring Security + JWT
- Spring WebSocket/STOMP
- OpenAPI via Springdoc
- Start infrastructure:
docker compose up -d- Start the application:
mvn spring-boot:runThe default local credentials are configured in application.yml.
- Open the demo UI:
http://localhost:8080/
POST /auth/registerPOST /auth/loginPOST /drivers/me/onlinePOST /drivers/me/offlinePOST /drivers/me/locationGET /drivers/nearbyPOST /rides/requestPOST /rides/{rideId}/acceptPOST /rides/{rideId}/startPOST /rides/{rideId}/completeGET /rides/{rideId}GET /riders/me/ridesGET /drivers/me/rides
/topic/rides/{rideId}/topic/drivers/{driverId}/offers/topic/riders/{riderId}
- Register one rider and one or more drivers.
- Put drivers online and push driver locations.
- Request a ride as the rider.
- Observe the candidate driver offer topic.
- Accept the ride as a driver.
- Start and complete the ride as the assigned driver.
flowchart LR
A["Rider registers / logs in"] --> B["Driver registers / logs in"]
B --> C["Driver updates location"]
C --> D["Driver goes online"]
D --> E["Rider requests ride"]
E --> F["Backend finds nearby available drivers"]
F --> G["Driver accepts ride"]
G --> H["Ride state = DRIVER_ASSIGNED"]
H --> I["Driver starts trip"]
I --> J["Ride state = IN_PROGRESS"]
J --> K["Driver completes trip"]
K --> L["Fare calculated and payment simulated"]
L --> M["Ride state = COMPLETED"]
flowchart TB
UI["Demo UI / Client"] --> API["Spring Boot API"]
API --> AUTH["JWT Auth"]
API --> RIDE["Ride State Machine"]
API --> GEO["Driver Geo Index"]
API --> WS["WebSocket Notifications"]
RIDE --> DB["PostgreSQL / PostGIS"]
GEO --> REDIS["Redis"]
RIDE --> PAY["Simulated Payment"]
RideFlow uses a simple but explicit fare policy that is close to how a ride-hailing MVP should behave.
The configured formula is:
fare = max(
minimum_fare,
base_fare
+ booking_fee
+ (billable_distance_km * per_km_rate)
+ (billable_duration_min * per_minute_rate)
)
The values are configured in application.yml under app.pricing.
- The backend first computes straight-line distance between pickup and dropoff using the Haversine formula.
- That direct distance is then multiplied by
route-distance-multiplier. - This multiplier is intentional. Straight-line distance underestimates a real road trip, so the multiplier makes the estimate more realistic without calling an external routing provider.
- The adjusted result becomes the billable distance.
- Estimated fare uses the billable distance and
estimated-average-speed-kphto infer trip time. - Estimated duration is rounded up and never goes below
minimum-estimated-duration-minutes. - Final fare uses actual trip time if the ride was started.
- Actual trip duration is measured from
startedAtto completion time, rounded up to the next minute, and never goes belowminimum-final-duration-minutes. - If a ride is completed without a
startedAttimestamp, the system falls back to the estimated duration path.
- The fare policy is deterministic and easy to explain in an interview.
- It avoids undercharging on very short trips because of the minimum fare floor.
- It avoids underestimating real road distance while still staying backend-only.
- It keeps pricing logic centralized in PricingService.java, so ride controllers and state transitions do not embed pricing rules.
The driver-rider matching path separates fast-changing location data from transactional ride state.
- A driver sends location updates through
POST /drivers/me/location. - The latest latitude and longitude are stored on the driver profile in PostgreSQL as the persistent source of truth.
- The same location is also written into the geo index:
- Redis GEO in the default profile
- in-memory index in the
localprofile for easier demo/testing
- Every location update also refreshes a heartbeat TTL.
When a rider requests a trip:
- The ride is created in PostgreSQL.
- The backend queries the geo index for drivers near the pickup point within the configured radius.
- Candidate drivers are sorted by distance ascending.
- The backend filters out drivers who are not actually
AVAILABLE. - The backend also filters out stale drivers whose heartbeat or
lastLocationAtis older than the freshness window. - The remaining drivers become ride-offer candidates.
This means geographical proximity alone is not enough. A driver must satisfy all of these conditions:
- be inside the configured search radius
- have a fresh heartbeat
- have a recent persisted location timestamp
- currently be in
AVAILABLEstatus
- Redis is optimized for fast nearby-driver lookup.
- PostgreSQL remains the source of truth for driver status, ride ownership, and ride assignment.
- This avoids using Redis alone as the authority for dispatch decisions.
- During ride acceptance, the actual winner is decided transactionally in the ride service, not by the geo index.
- If a driver stops sending location, the heartbeat expires and the driver drops out of matching.
- If a driver becomes
BUSY, the driver is filtered out even if their location is still nearby. - When a driver accepts a ride, the driver is removed from the geo index so they do not keep receiving competing offers.
- PostgreSQL remains the source of truth for ride ownership and ride state.
- Redis is used for fast geospatial lookup and driver heartbeats.
- PostGIS is enabled in the database and the schema stores PostGIS geometry alongside numeric coordinates through triggers.
- Fare calculation is config-driven:
base fare + booking fee + (billable km * per-km rate) + (billable minutes * per-minute rate), with a route-distance multiplier and a minimum fare floor.