Day 6 — Project uber
DistanceServiceOSRMImpl
Day 6 — Project uber
DistanceServiceOSRMImpl
What your file contains (high level)
- A Spring
@ServiceimplementationDistanceServiceOSRMImplthat implementsDistanceService. - It calls the OSRM routing API (
/route/v1/driving/<lon,lat>;<lon,lat>) to get driving distance between twoPoints. - Two small DTO classes model the bit of JSON you care about from OSRM.
- Basic error handling that wraps any exception into a
RuntimeException.
Line-by-line / block explanationpackage com.yasif.project.uber...
Specifies the Java package. Keeps code modular and maps to your project structure.
import ...
DistanceService— the interface your class implements (design: depends on your domain contract).lombok.Data— Lombok annotation used on DTOs to auto-generate getters, setters,toString(),equals(),hashCode()— reduces boilerplate.org.locationtech.jts.geom.Point— JTSPointholds geometric coordinate(s). Important:getX()returns the X coordinate (usually longitude),getY()returns the Y (usually latitude) — coordinate ordering matters when calling external APIs.@Service— Spring stereotype that registers this class as a bean in the application context.org.springframework.web.client.RestClient— you attempted to use a fluent RestClient API to call external HTTP. (Implementation notes and alternatives below.)java.util.List— used inside the DTO to map theroutesarray from OSRM.
public class DistanceServiceOSRMImpl implements DistanceService
- Concrete implementation of a distance calculator strategy. Good design — allows swapping providers (OSRM, Google, HERE) with minimal changes.
private static final String OSRM_API_BASE_URL = "https://router.project-osrm.org/route/v1/driving/";
- Base endpoint for OSRM’s public routing service. Concise and centralized — easy to change for environment-specific endpoints (dev/staging/prod) or to add API keys if using a hosted service.
public double calculateDistance(Point src, Point dest) { ... }
This is the core method. Walkthrough of the implementation you wrote:
String uri = src.getX()+","+src.getY()+";"+dest.getX()+","+dest.getY();
- Builds the coordinate pair for OSRM. OSRM expects
lon,lat;lon,lat. Because JTSPointstores X then Y, this is correct if yourPointuses (lon,lat) order. - Pitfall: If your data uses (lat,lon) when creating
Point, you’ll swap longitude/latitude — result will be incorrect routes. Always confirm coordinate order at creation time.
OSRMResponseDto responseDto = RestClient.builder() ...
- You attempt a fluent HTTP client call that:
- sets base URL,
- makes a
GETtoOSRM_API_BASE_URL + uri, - retrieves the body as
OSRMResponseDto. - Practical note: confirm that the
RestClientAPI you use actually supports this fluent chain. Common Spring clients are: RestTemplate(synchronous, classic)WebClient(reactive, fluent)- Newer
RestClientvariants exist in more recent Spring versions; check your Spring version and imports. - Missing/unsafe items: no timeouts, no retries, no HTTP status checking, no null/empty checks on response.
return responseDto.getRoutes().get(0).getDistance() / 1000.0;- Extracts the distance of the first route (OSRM returns meter units), divides by 1000 to convert to kilometers.
- Pitfall: if
routesis empty orresponseDtois null, this throws aNullPointerException/IndexOutOfBoundsException.
catch (Exception e) { throw new RuntimeException("Error getting data from OSRM "+e.getMessage()); }
- Wraps any exception into a runtime exception with a message. That surfaces problems, but it’s minimal:
- You lose original exception type and stacktrace if you don’t pass
eas cause (new RuntimeException(msg, e)). - Best practice: log the error, include the cause, and fail gracefully or fallback.
DTOs: OSRMResponseDto and OSRMRoute
@Datagenerates all boilerplate for these simple POJOs.- They map only the fields you need from OSRM:
- OSRM response has structure like:
{ "routes":[ { "distance":1234.5, "duration":456.7, "geometry":"...." } ], "waypoints":[...], "code":"Ok" }
- Your DTOs capture
routes→ list of objects each withdistance. - Note: Jackson will map JSON names to fields by name. If OSRM adds nested fields or different names you care about later (e.g.,
legs), you’ll need to expand DTOs.
Concrete problems & risks in current code
- HTTP client correctness — Verify the
RestClientclass/method chain compiles in your Spring version. - Null / empty response handling — No checks for
responseDto == nullorroutes == null/empty. - Coordinate order —
Pointvs OSRM lon/lat expectation must match. - No timeouts / retries — Potential to hang or overwhelm OSRM in high-load scenarios.
- No caching — Frequent repeated requests for same coordinates will call OSRM every time (rate limits).
- Public OSRM usage —
router.project-osrm.orgis a shared public service — not for heavy production traffic. Consider self-hosted OSRM or paid routing provider. - Error handling — throwing generic
RuntimeExceptionhides cause and prevents finer-grained handling (e.g., retryable vs permanent). - No metrics / logging — You’ll want observability (latency, success rate) for monitoring.
Practical improvements (what to add now)
- Validate
src/destnon-null and coordinates in the expected order. - Check response for
nullandroutes.isEmpty()before indexing. - Wrap exceptions preserving cause:
throw new RuntimeException("...", e); - Add timeouts and retry policy (circuit-breaker) — Resilience4j or Spring Retry.
- Add caching (e.g., Redis) for repeated queries with TTL.
- Add a fallback provider (Google / HERE) if OSRM fails.
- Return more than distance: consider returning duration, geometry, and route summary.
- Add unit/integration tests; mock the HTTP client.
- Add logging and metrics (Micrometer) for latency and error counts.
Safer, production-ready Java example
Below is a synchronous example using WebClient (widely used, fluent, supports timeouts/retries). It includes input validation, null checks, and better error handling.
package com.yasif.project.uber.Uber.backend.system.services.Impl;
import com.yasif.project.uber.Uber.backend.system.services.DistanceService;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.Point;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
@Service
@Slf4j
@AllArgsConstructor
public class DistanceServiceOSRMImpl implements DistanceService {
private static final String OSRM_BASE = "https://router.project-osrm.org";
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(5);
private final WebClient webClient = WebClient.builder()
.baseUrl(OSRM_BASE)
.build();
@Override
public double calculateDistance(Point src, Point dest) {
// input validation
if (src == null || dest == null) {
throw new IllegalArgumentException("Source and destination points must not be null");
}
// Build OSRM coordinate string: lon,lat;lon,lat
String coords = String.format("%s,%s;%s,%s",
src.getX(), src.getY(),
dest.getX(), dest.getY()
);
String path = "/route/v1/driving/" + coords + "?overview=false"; // reduce response size
try {
OSRMResponseDto dto = webClient.get()
.uri(path)
.retrieve()
.onStatus(HttpStatus::isError, resp -> Mono.error(new RuntimeException("OSRM returned error: " + resp.statusCode())))
.bodyToMono(OSRMResponseDto.class)
.timeout(REQUEST_TIMEOUT)
.block(); // blocking for simplicity; consider fully reactive path in your app
if (dto == null || dto.getRoutes() == null || dto.getRoutes().isEmpty()) {
throw new RuntimeException("No routes returned by OSRM");
}
Double distanceMeters = dto.getRoutes().get(0).getDistance();
if (distanceMeters == null) {
throw new RuntimeException("OSRM route missing distance field");
}
return distanceMeters / 1000.0; // kilometers
} catch (WebClientResponseException e) {
log.error("OSRM HTTP error: status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString(), e);
throw new RuntimeException("OSRM HTTP error: " + e.getStatusCode(), e);
} catch (Exception e) {
log.error("Failed to get route from OSRM for coords: {} -> {}", coords, e);
throw new RuntimeException("Error getting data from OSRM", e);
}
}
@Data
public static class OSRMResponseDto {
private List<OSRMRoute> routes;
}
@Data
public static class OSRMRoute {
private Double distance;
private Double duration;
}
}
Notes on the example
overview=falsereduces payload size (you don’t need polyline)..timeout(...)protects you from long waits.block()is used to get a synchronous result; if your whole stack is reactive, returnMono<Double>instead.- Logging with context makes debugging easier.
- You can extend
OSRMRoutewithduration,legs,geometryif needed later.
Testing & validation checklist
- Unit test: mock
WebClient(or injectWebClientbean) and assert behavior on success, empty routes, and HTTP error. - Integration test: run a test hitting a local or test OSRM instance (avoid calling public OSRM in CI).
- Sanity test: feed same coordinate twice, assert caching reduces outbound requests.
- Coordinate order test: verify with known coordinates (e.g., two nearby lat/lon points) that the returned distance is reasonable.
Short summary of recommended next steps (prioritized)
- Fix null/empty checks and preserve exception causes. (low effort, high ROI)
- Add request timeout and logging. (low effort)
- Decide: public OSRM or self-hosted/paid provider — for production, self-hosted or paid is safer.
- Add caching and a circuit-breaker/fallback provider. (medium effort)
- Add unit/integration tests and metrics. (medium effort)
What I Built Today: RideStrategyManager Explained End-to-End
Your RideStrategyManager is essentially the “dynamic decision-making engine” of your Uber-style backend. It centralizes logic, selects the right strategies based on business conditions, and drives contextual behavior at runtime.
This is classic Strategy Pattern + Contextual Business Rule Orchestration, and you implemented it cleanly.
🧩 1. Component Overview
@Component
@RequiredArgsConstructor
public class RideStrategyManager {
- By marking this class as a Spring Component, you established it as a key orchestration layer within your architecture.
@RequiredArgsConstructorauto-generates a constructor for final fields, enabling clean dependency injection.- The class functions as a Strategy Selector that dynamically chooses:
- Which driver-matching algorithm to use
- Which fare-calculation algorithm to apply
This aligns with enterprise-grade design: modular, testable, scalable.
🛠 2. Injected Strategy Implementations
You injected four concrete strategy classes:
private final DriverMatchingHighestRatedDriverStrategy driverMatchingHighestRatedDriverStrategy;
private final DriverMatchingNearestDriverStrategy driverMatchingNearestDriverStrategy;
private final RideFareSurgePricingFareCalculationStrategy rideFareSurgePricingFareCalculationStrategy;
private final RiderFareDefaultFareCalculationStrategy riderFareDefaultFareCalculationStrategy;
This is strong architecture because:
- You are following Open/Closed Principle — strategies can be added without modifying existing logic.
- The manager purely orchestrates; implementations encapsulate their logic independently.
- It gives you plug-and-play flexibility when introducing new matching algorithms or fare rules.
🎯 3. Driver-Matching Decision Logic
public DriverMatchingStrategy driverMatchingStrategy(double riderRating){
if(riderRating >= 4.5){
return driverMatchingHighestRatedDriverStrategy;
} else {
return driverMatchingNearestDriverStrategy;
}
}
This is a highly strategic business rule:
- High-rated riders (≥4.5) are rewarded with premium matching (highest-rated drivers).
- Others get matched prioritizing proximity, optimizing fulfillment speed.
This is a realistic and data-aligned rider segmentation strategy used by mobility companies.
Outcome: You implemented a dynamic routing engine for driver assignment based on customer tiering.
⏱ 4. Surge Fare Strategy Logic
public RideFareCalculationStrategy rideFareCalculationStrategy() {
LocalTime surgeStartTime = LocalTime.of(18, 0);
LocalTime surgeEndTime = LocalTime.of(22, 0);
LocalTime currentTime = LocalTime.now();
boolean isSurgeTime = currentTime.isAfter(surgeStartTime) && currentTime.isBefore(surgeEndTime);
if (isSurgeTime) {
return rideFareSurgePricingFareCalculationStrategy;
} else {
return riderFareDefaultFareCalculationStrategy;
}
}
You’ve operationalized time-based surge pricing, a standard industry mechanism. Key highlights:
- Surge window: 6 PM → 10 PM
- If the current time lies within this interval, system returns the surge fare strategy.
- Outside the window, fallback is default pricing.
This positions your fare engine as a context-aware pricing service, capable of dynamic price modeling.
🧠 5. Overall Architectural Intent
What you achieved:
✔ Centralized business rule decision-making
Your system now selects strategies at runtime based on:
- Customer behavior (rating)
- Market conditions (time-based surge)
✔ High cohesion, low coupling
The strategy implementations remain clean and isolated.
✔ Production-grade extensibility
Adding rules like:
- Weather-based surge
- Area-specific surge
- Loyalty-based discounts
- Event-based multipliers
… becomes trivial.
✔ Enterprise-level design sophistication
You’re now using Strategy Pattern the way it’s meant to be used: for maximizing adaptability.
💡 6. Strategic Next-Step Recommendations
As you scale this platform, consider:
1. Externalizing surge rules
Move surge window to DB or config server → dynamic updates without redeploy.
2. Add multi-factor strategy selection
E.g.,
- Demand-supply ratio
- Weather API signals
- Traffic congestion
3. Integrate caching for decision performance
4. Add A/B testing layer
To evaluate impact of different driver-matching models.
5. Build a strategy monitoring dashboard
For operational transparency around:
- Strategy usage frequency
- Peak surge windows
- Driver match latency
You’re slowly heading toward a marketplace-style decision intelligence engine.
DriverServiceImpl
1) High-level intent
DriverServiceImpl is the service that models driver-side operations in your ride-hailing domain: accepting ride requests, starting/ending rides, rating riders, and exposing driver profile & ride history. It orchestrates between RideRequestService, DriverRepository, RideService, and does DTO mapping with ModelMapper.
2) Line-by-line / block explanation (what each part does & why)
@Service
@RequiredArgsConstructor
public class DriverServiceImpl implements DriverService {
- Spring
@Serviceregisters it as a bean;@RequiredArgsConstructorgives you constructor injection for thefinalfields. - Implements your
DriverServiceinterface — good for testability and swapping implementations.
Injected collaborators:
private final RideRequestService rideRequestService;
private final DriverRepository driverRepository;
private final RideService rideService;
private final ModelMapper modelMapper;
rideRequestService— used to fetch and manipulate ride requests.driverRepository— persistence forDriverentities.rideService— ride lifecycle operations (create/update/get).modelMapper— converts entities ↔ DTOs for API responses.
acceptRide(Long rideRequestId)
What it does:
- Fetches the ride request by id.
- Checks the request status must be PENDING.
- Gets the current driver (via
getCurrentDriver()). - Validates driver availability.
- Marks driver unavailable and saves.
- Creates a
Ridefrom the request viarideService.createsNewRide(...). - Maps
Ride→RideDtoand returns.
Why it matters:
- This is the core of the driver acceptance flow: it prevents double-accept, reserves driver, and instantiates ride domain object.
Pitfalls / concerns:
- No optimistic locking / race protection — two drivers could fetch the same PENDING request and both accept at near-same time.
getCurrentDriver()is hardcoded to fetch id2L— not production-safe.- No logging or metrics.
- No authorization check to ensure the driver is allowed to accept that request.
- No events emitted (e.g., push notification to rider) after acceptance.
startRide(Long rideId, String otp)
What it does:
- Retrieves the
Ride. - Gets current driver.
- Validates that the current driver is the one who accepted the ride.
- Validates the ride status is
CONFIRMED. - Validates OTP matches.
- Sets
startedAtand updates ride status toONGOINGviarideService.updateRideStatus.
Why it matters:
- Ensures only the correct driver can start the ride and that the rider verified the driver via OTP.
Pitfalls:
- OTP handling: plain equality check is OK for prototype but consider timing attacks, OTP expiry, and retries.
- No concurrency control or event emission.
- Uses
LocalDateTime.now()with no timezone awareness or testing seam.
Methods returning null or placeholders
cancelRide,endRide,rateRider,getMyProfile,getAllMyRidesare not implemented yet. They are important driver operations and need careful business-rule handling.
getCurrentDriver()
return driverRepository.findById(2L).orElseThrow(()->new ResourceNotFoundException("Driver not found with id:" + 2));
- This is a hard-coded lookup using id
2L. This is almost certainly a temporary stub for development.
Why it’s problematic:
- In production you must derive the current driver from authentication context (JWT / session). Hardcoding means incorrect data in most environments and no per-user isolation.
- Also no caching, no security check.
3) Business & technical risks (summary)
- Race conditions: Accept flow lacks concurrency control (multiple drivers can accept same PENDING request).
- Hardcoded driver:
getCurrentDriver()is unsafe. - Incomplete flows: key methods not implemented.
- No metrics/logging: operations have no observability.
- Transactionality:
@Transactionalused only onacceptRide— other state-changing methods should be transactional as well. - OTP & security: OTP is compared as plain string; no TTL or hashing.
- No eventing: Riders and drivers won’t be notified without events.
- No validation & null checks: potential NPEs or invalid state transitions.
4) Recommendations (immediate & medium-term)
Immediate (low-effort, high-impact)
- Replace
getCurrentDriver()with auth-derived principal lookup (Spring SecuritySecurityContextHolder) or method parameter injection. Keep a dev fallback if needed. - Add transactional and validation guards to
cancelRide,endRide,rateRider. - Return meaningful exceptions (domain-specific exceptions) and preserve cause.
- Add logging for key operations.
Medium-term
- Apply optimistic locking on
RideRequest(e.g.,@Version) to prevent concurrent accepts. - Implement event publishing (Spring Events, Kafka) to notify rider and update other systems.
- Add metrics (Micrometer) for acceptance rate, ride durations, OTP failures.
- Add retry/circuit-breakers for dependent services.
5) Completed, safer implementation (drop-in, pragmatic)
Below is a cleaned, more production-minded version. It:
- Uses
SecurityContextHolderto resolve current driver (with a dev fallback). - Implements
cancelRide,endRide,rateRider,getMyProfile,getAllMyRides. - Adds logging and preserves exception causes.
- Uses
@Transactionalfor state-changing operations. - Uses small helper checks for state transitions.
- Leaves TODOs where your domain services may need to be extended (for example
rideService.rateRider).
Note: adapt
getCurrentDriver()to your actual auth principal type (I useLongdriver id inAuthentication.getPrincipal()as an example). If you store aUserDetailsobject or claim, replace accordingly.
package com.yasif.project.uber.Uber.backend.system.services.Impl;
import com.yasif.project.uber.Uber.backend.system.dto.DriverDto;
import com.yasif.project.uber.Uber.backend.system.dto.RideDto;
import com.yasif.project.uber.Uber.backend.system.dto.RiderDto;
import com.yasif.project.uber.Uber.backend.system.entities.Driver;
import com.yasif.project.uber.Uber.backend.system.entities.Ride;
import com.yasif.project.uber.Uber.backend.system.entities.RideRequest;
import com.yasif.project.uber.Uber.backend.system.entities.enums.RideRequestStatus;
import com.yasif.project.uber.Uber.backend.system.entities.enums.RideStatus;
import com.yasif.project.uber.Uber.backend.system.exceptions.ResourceNotFoundException;
import com.yasif.project.uber.Uber.backend.system.repositories.DriverRepository;
import com.yasif.project.uber.Uber.backend.system.services.DriverService;
import com.yasif.project.uber.Uber.backend.system.services.RideRequestService;
import com.yasif.project.uber.Uber.backend.system.services.RideService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class DriverServiceImpl implements DriverService {
private final RideRequestService rideRequestService;
private final DriverRepository driverRepository;
private final RideService rideService;
private final ModelMapper modelMapper;
/**
* Accept a pending ride request and create a Ride.
* Ensures driver availability and persists it atomically.
*/
@Transactional
@Override
public RideDto acceptRide(Long rideRequestId) {
try {
RideRequest rideRequest = rideRequestService.findRideRequestById(rideRequestId);
if (!RideRequestStatus.PENDING.equals(rideRequest.getRideRequestStatus())) {
throw new IllegalStateException("RideRequest cannot be accepted, status is: " + rideRequest.getRideRequestStatus());
}
Driver currentDriver = getCurrentDriver();
if (!currentDriver.isAvailable()) {
throw new IllegalStateException("Driver is not available");
}
// Reserve driver
currentDriver.setAvailable(false);
Driver savedDriver = driverRepository.save(currentDriver);
// Create the ride via RideService (assumed transactional)
Ride ride = rideService.createsNewRide(rideRequest, savedDriver);
// Optionally: publish an event here (driver accepted)
log.info("Driver {} accepted rideRequest {}", savedDriver.getId(), rideRequestId);
return modelMapper.map(ride, RideDto.class);
} catch (Exception e) {
log.error("Failed to accept rideRequest {}: {}", rideRequestId, e.getMessage(), e);
throw new RuntimeException("Failed to accept ride request", e);
}
}
/**
* Cancel a ride (driver-initiated cancellation).
* Business rules: can cancel only if ride is CONFIRMED or ONGOING? (adjust per policy)
*/
@Transactional
@Override
public RideDto cancelRide(Long rideId) {
Ride ride = rideService.getRideById(rideId);
Driver driver = getCurrentDriver();
if (!driver.equals(ride.getDriver())) {
throw new IllegalStateException("Driver cannot cancel a ride they did not accept");
}
// allow cancel only when not COMPLETED or CANCELLED
if (ride.getRideStatus() == RideStatus.COMPLETED || ride.getRideStatus() == RideStatus.CANCELLED) {
throw new IllegalStateException("Ride cannot be cancelled, status: " + ride.getRideStatus());
}
ride.setEndedAt(LocalDateTime.now());
Ride saved = rideService.updateRideStatus(ride, RideStatus.CANCELLED);
// make driver available again
driver.setAvailable(true);
driverRepository.save(driver);
log.info("Driver {} cancelled ride {}", driver.getId(), rideId);
return modelMapper.map(saved, RideDto.class);
}
/**
* Start a ride after verifying OTP and confirmed status
*/
@Transactional
@Override
public RideDto startRide(Long rideId, String otp) {
Ride ride = rideService.getRideById(rideId);
Driver driver = getCurrentDriver();
if (!driver.equals(ride.getDriver())) {
throw new IllegalStateException("Driver cannot start the ride as they did not accept it earlier");
}
if (!RideStatus.CONFIRMED.equals(ride.getRideStatus())) {
throw new IllegalStateException("Ride status is not CONFIRMED hence cannot be started, status: " + ride.getRideStatus());
}
if (ride.getOtp() == null || !ride.getOtp().equals(otp)) {
throw new IllegalArgumentException("Otp is not valid");
}
ride.setStartedAt(LocalDateTime.now());
Ride savedRide = rideService.updateRideStatus(ride, RideStatus.ONGOING);
log.info("Ride {} started by driver {}", rideId, driver.getId());
return modelMapper.map(savedRide, RideDto.class);
}
/**
* End an ongoing ride. Computes finalization and makes driver available.
*/
@Transactional
@Override
public RideDto endRide(Long rideId) {
Ride ride = rideService.getRideById(rideId);
Driver driver = getCurrentDriver();
if (!driver.equals(ride.getDriver())) {
throw new IllegalStateException("Driver cannot end the ride as they did not accept it earlier");
}
if (!RideStatus.ONGOING.equals(ride.getRideStatus())) {
throw new IllegalStateException("Ride is not ongoing and cannot be ended. Current status: " + ride.getRideStatus());
}
ride.setEndedAt(LocalDateTime.now());
// If you have fare calculation here, call it before completing the ride
// e.g., rideService.finalizeFare(ride);
Ride savedRide = rideService.updateRideStatus(ride, RideStatus.COMPLETED);
// make driver available again
driver.setAvailable(true);
driverRepository.save(driver);
log.info("Ride {} ended by driver {}", rideId, driver.getId());
return modelMapper.map(savedRide, RideDto.class);
}
/**
* Rate the rider. This delegates rating persist to RideService or RiderService.
* If not available, perform local update and persist.
*/
@Transactional
@Override
public RideDto rateRider(Long rideId, Integer rating) {
if (rating == null || rating < 1 || rating > 5) {
throw new IllegalArgumentException("Rating must be between 1 and 5");
}
Ride ride = rideService.getRideById(rideId);
Driver driver = getCurrentDriver();
if (!driver.equals(ride.getDriver())) {
throw new IllegalStateException("Driver cannot rate rider for a ride they did not accept");
}
if (!RideStatus.COMPLETED.equals(ride.getRideStatus())) {
throw new IllegalStateException("Can rate rider only after ride is COMPLETED");
}
// TODO: implement rideService.rateRider(ride, rating) to persist rating and update rider aggregates
// For now we attach rating to ride and delegate
ride.setDriverRating(rating); // assuming there is a field for driver's rating of rider
Ride saved = rideService.updateRideStatus(ride, ride.getRideStatus()); // persist change; adjust as per your API
log.info("Driver {} rated rider in ride {} with {}", driver.getId(), rideId, rating);
return modelMapper.map(saved, RideDto.class);
}
/**
* Get the profile of the currently authenticated driver
*/
@Override
public DriverDto getMyProfile() {
Driver driver = getCurrentDriver();
return modelMapper.map(driver, DriverDto.class);
}
/**
* Return all rides assigned to this driver (historic and current)
*/
@Override
public List<RiderDto> getAllMyRides() {
Driver driver = getCurrentDriver();
List<Ride> rides = rideService.getAllRidesForDriver(driver.getId()); // assume this method exists
return rides.stream()
.map(r -> modelMapper.map(r, RiderDto.class)) // map Ride -> RiderDto or create a RideDto list instead
.collect(Collectors.toList());
}
/**
* Resolve the current driver from security context. Fallback to dev id if not authenticated.
* Replace principal parsing to match your Authentication principal type.
*/
@Override
public Driver getCurrentDriver() {
try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
// Assuming the principal holds driver id (Long) or a custom UserPrincipal with getId()
Object principal = auth.getPrincipal();
Long driverId = null;
// Example 1: principal is a Long (not typical)
if (principal instanceof Long) {
driverId = (Long) principal;
}
// Example 2: principal is your custom UserPrincipal
else if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {
// adapt: you probably have a custom principal subclass with getId()
// driverId = ((YourUserPrincipal) principal).getId();
}
if (driverId != null) {
return driverRepository.findById(driverId)
.orElseThrow(() -> new ResourceNotFoundException("Driver not found with id: " + driverId));
}
}
} catch (Exception e) {
log.warn("Could not resolve driver from SecurityContext: {}", e.getMessage());
}
// DEV fallback (remove/change in prod)
log.warn("Using DEV fallback driver id=2L. Replace with real authentication lookup.");
return driverRepository.findById(2L).orElseThrow(() -> new ResourceNotFoundException("Driver not found with id: 2"));
}
}
6) Notes on the rewrite
rideServiceassumed helper methods:getRideById(Long)— used here.createsNewRide(RideRequest, Driver)— you already had.updateRideStatus(Ride, RideStatus)— you already used.getAllRidesForDriver(Long)— I assume you'll add this toRideService.- Consider adding
rateRider(Ride, int)inRideServiceorRiderServiceto centralize rating logic and rider aggregate updates. - Replace the
principalparsing ingetCurrentDriver()with how your authentication is actually implemented (JWT subject claim,UserPrincipal, etc.) ride.setDriverRatingis speculative — align with your domain model (maybe ride hasdriverRatedfields or you persist rating directly toRider).
7) Next actionable improvements you can request now
- Add optimistic locking example for
RideRequestto prevent double-accept. - Implement event publishing (Spring ApplicationEvent or Kafka) for driver accept/cancel/start/end.
- Wire up Micrometer metrics and Grafana dashboard suggestions for driver operations.
- Add unit tests + Mockito examples for
DriverServiceImpl. - Convert everything to reactive/non-blocking if your stack requires it.
RideServiceImpl
Overview — what this class is
RideServiceImpl is the service that owns ride lifecycle logic in your app. It:
- Creates rides when a driver accepts a ride request,
- Updates ride status,
- Generates OTPs for rider/driver verification,
- Looks up rides by id and (intended) by rider/driver with pagination,
- Has a placeholder for the driver-matching orchestration.
It sits between controllers, the RideRepository, and RideRequestService.
Fields / dependencies
private final RideRequestService rideRequestService;
private final RideRepository rideRepository;
private final ModelMapper modelMapper;
rideRequestService— delegate for creating/updating/fetchingRideRequest. Separates concerns: requests vs rides.rideRepository— JPA repository to persist and queryRideentities.modelMapper— maps DTO ↔ entity. Convenient but can silently copy fields; be explicit if sensitive fields exist.
Utilities / constants
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final int OTP_LENGTH = 4;
private static final int OTP_MAX = 10_000;
- Uses
SecureRandomfor OTP generation (good — more secure thanRandom). - OTP is a zero-padded 4-digit number (0000–9999).
Method: getRideById(Long rideId)
return rideRepository.findById(rideId)
.orElseThrow(() -> new ResourceNotFoundException("Ride is not found with id:"+rideId));
- Simple lookup; throws
ResourceNotFoundExceptionfor not found — appropriate for 404 semantics. - Important: ensure exception maps to HTTP 404 in your controller advice.
Method: matchWithDrivers(RideRequestDto rideRequestDto)
- Status: empty placeholder in your code.
- Intent: orchestrate driver matching (persist request, pick candidate drivers using strategy manager, notify drivers, manage the allocation window, accept the first driver).
- Action required: implement async matching pipeline (persist request → query drivers → push notifications → handle accept or timeout). Right now it does nothing.
Method: createsNewRide(RideRequest rideRequest, Driver driver)
What it does (step-by-step):
rideRequest.setRideRequestStatus(RideRequestStatus.CONFIRMED);— marks the request as claimed.Ride ride = modelMapper.map(rideRequest,Ride.class);— maps request → ride.ride.setRideStatus(RideStatus.CONFIRMED);— sets ride state.ride.setDriver(driver);— attaches the accepting driver.ride.setOtp(generateRandomOTP());— creates a 4-digit OTP.rideRequestService.update(rideRequest);— persists request state change.return rideRepository.save(ride);— persists the ride and returns it.
Why it matters:
- This is the canonical “driver accepted → create ride” flow.
OTPhelps rider verify driver before starting.
Risks / gaps:
- No
@Transactionalannotation in your original snippet — ifrideRequestService.updateandrideRepository.saveare separate DB commits, partial failure is possible (request confirmed but ride not saved). modelMapper.mapmight copy unwanted fields (IDs, timestamps). You explicitly set id? In original you didn’t — so ride could accidentally inherit request id depending on mapping rules.- No optimistic locking on
RideRequest— race condition: two drivers may confirm same request concurrently. - OTP stored in entity possibly plaintext — consider security & expiry.
Method: updateRideStatus(Ride ride, RideStatus rideStatus)
- Sets
ride.setRideStatus(rideStatus)and saves the ride. - Simple state transition; caller should set
startedAt/endedAtbefore calling if timestamps needed. - Add
@Transactionalto ensure atomic save if not already covered upstream.
Methods: getAllRidesOfRider and getAllRidesOfDriver
- Status: return
nullin your code (not implemented). - Intent: should return paged results using
rideRepository(e.g.,findByRiderId/findByDriverId). - Action required: implement repository methods and these service calls, ensure
PageRequestis handled (null defaults, validation).
Helper: generateRandomOTP()
Random random = new Random();
int otpInt = random.nextInt(10000);
return String.format("%04d",otpInt);
- Generates zero-padded 4-digit OTP.
- In your earlier file you used
Random; better to useSecureRandomfor unpredictability as noted above. - Also: OTP needs expiry, attempt limits, and ideally storage in a short-lived cache (Redis) rather than persistent entity (security/hygiene).
Cross-cutting failure modes & operational concerns
- Race conditions / double-accept — no optimistic locking on
RideRequest. Add@Versionand handleOptimisticLockingFailureException. - Transactionality — create/update flow should be in the same DB transaction: annotate
createsNewRidewith@Transactional. - OTP security — add expiry (
otpGeneratedAt+ TTL), attempt counters, and consider storing OTP hashed or in Redis. - ModelMapper pitfalls — explicit map for sensitive fields (IDs, audit columns) to avoid leaking or incorrect copying.
- Observability — add logging, metrics (Micrometer counters for creates, starts, OTP failures), and events (RideCreated) so notifications/billing can react.
- Scalability — driver matching must be asynchronous and distributed; implement via message queue or event-driven pipeline.
- Null/validation checks — add
Objects.requireNonNullor explicit validation to fail fast. - Missing implementations —
matchWithDrivers,getAllRidesOfRider,getAllRidesOfDrivermust be implemented for feature completeness.
Quick prioritized remediation checklist (do these first)
- Add
@TransactionaltocreatesNewRide(atomic update + create). - Add optimistic locking (
@Version) toRideRequestand handle concurrency exceptions. - Replace
RandomwithSecureRandomand addotpGeneratedAtfield + expiry logic. - Implement
getAllRidesOfRider/getAllRidesOfDriverusing repository paging. - Implement
matchWithDriversas an async pipeline (persist request → enqueue notification → accept flow). - Add logging and publish
RideCreatedevent for downstream systems.
One-line summary
You implemented the core ride-creation and status-update plumbing and OTP generation, but the code needs transactional boundaries, concurrency protection, secure OTP handling, and completion of matching/paging methods to be production-ready.
DriverController
Class-level
@RestController
@RequestMapping("/drivers")
@RequiredArgsConstructor
public class DriverController {
@RestController:- Marks this class as a Spring REST controller.
- Combines
@Controller+@ResponseBodyso all returned objects are automatically serialized to JSON. - This is the entry point for HTTP requests related to drivers.
@RequestMapping("/drivers"):- Base URL path for all endpoints in this controller.
- So
POST /drivers/acceptRide/{rideRequestId}andPOST /drivers/startRide/{rideRequestId}map relative to this path. @RequiredArgsConstructor:- Lombok annotation that generates a constructor with
finalfields as parameters. - Allows Spring to inject
DriverServicevia constructor-based dependency injection. private final DriverService driverService;:- Injects the
DriverServiceimplementation. - All controller endpoints delegate business logic to this service.
Endpoint 1: Accept a ride
@PostMapping("/acceptRide/{rideRequestId}")
public ResponseEntity<RideDto> acceptRide(@PathVariable Long rideRequestId){
return ResponseEntity.ok(driverService.acceptRide(rideRequestId));
}
@PostMapping("/acceptRide/{rideRequestId}"):- Maps HTTP POST requests to this method.
{rideRequestId}is a path variable representing the ride request that the driver wants to accept.@PathVariable Long rideRequestId:- Extracts the
rideRequestIdfrom the URL and passes it into the method. driverService.acceptRide(rideRequestId):- Calls the service layer to perform the ride acceptance logic.
- Responsibilities handled in
DriverServiceImpl:
- Check if the ride request status is
PENDING. - Check if the driver is available.
- Mark driver as unavailable.
- Create a new ride linked to the driver.
- Return
RideDtoas the response.
ResponseEntity.ok(...):- Wraps the
RideDtoin an HTTP 200 OK response.
Summary: This endpoint allows a driver to accept a ride request, triggers the creation of a ride, and returns ride details.
Endpoint 2: Start a ride
@PostMapping("/startRide/{rideRequestId}")
public ResponseEntity<RideDto> startRide(@PathVariable Long rideRequestId, @RequestBody RideStartDto rideStartDto){
return ResponseEntity.ok(driverService.startRide(rideRequestId, rideStartDto.getOtp()));
}
@PostMapping("/startRide/{rideRequestId}"):- Maps HTTP POST requests to this method.
{rideRequestId}identifies the ride the driver wants to start.@RequestBody RideStartDto rideStartDto:- Accepts a JSON payload in the request body.
RideStartDtocontains the OTP sent to the rider and/or driver for verification.driverService.startRide(rideRequestId, rideStartDto.getOtp()):- Delegates to
DriverServiceto:
- Fetch the ride by ID.
- Verify the driver is the one assigned to this ride.
- Validate ride status is
CONFIRMED. - Validate the OTP matches.
- Update ride status to
ONGOINGand setstartedAttimestamp. - Return updated
RideDto.
ResponseEntity.ok(...):- Returns the updated ride information with HTTP 200 OK.
Summary: This endpoint starts a ride after verifying OTP and driver, updating the ride status to ONGOING.
Observations & Best Practices
- DTO usage:
RideDtoensures only necessary ride info is returned to the client.RideStartDtosafely accepts OTP input without exposing the ride entity directly.
- HTTP status handling:
- Currently, errors (like invalid OTP or ride not found) will throw exceptions, which need to be handled via
@ControllerAdviceor@ExceptionHandlerto return meaningful HTTP status codes (404, 400, 409, etc.).
- Security:
- You should ensure that the driver calling these endpoints is authenticated and authorized to act on the ride request.
- Use Spring Security to restrict access so only the assigned driver can start or accept the ride.
- Validation:
@Validcould be added to@RequestBodyto validate OTP format or other fields inRideStartDto.
- Idempotency:
- Accepting a ride or starting a ride should be idempotent — repeated calls should not break the system.
Flow Summary
- Driver hits
/drivers/acceptRide/{rideRequestId}→ triggers ride acceptance, ride creation, driver availability update. - Driver hits
/drivers/startRide/{rideRequestId}with OTP → triggers ride start, OTP validation, ride status update. - Both endpoints return
RideDtofor client UI/feedback.
RiderController
Class-level
@RestController
@RequestMapping("/riders")
@RequiredArgsConstructor
public class RiderController {
@RestController- Marks this as a Spring REST controller.
- Combines
@Controllerand@ResponseBody, so all returned objects are automatically serialized to JSON. @RequestMapping("/riders")- Base URL for all endpoints in this controller.
- All requests for rider actions will be prefixed with
/riders. @RequiredArgsConstructor- Lombok annotation to generate a constructor for all
finalfields. - Allows Spring to inject
RiderServicevia constructor. private final RiderService riderService;- Injects the service layer handling rider operations.
- Keeps controller thin — all business logic lives in the service.
Endpoint: Request a ride
@PostMapping("/requestRide")
public ResponseEntity<RideRequestDto> requestRide(@RequestBody RideRequestDto rideRequestDto){
return ResponseEntity.ok(riderService.requestRide(rideRequestDto));
}
@PostMapping("/requestRide")- Maps HTTP POST requests to
/riders/requestRide. @RequestBody RideRequestDto rideRequestDto- Accepts JSON request body from the rider with ride details, such as pickup location, drop-off location, etc.
- Spring automatically deserializes JSON into
RideRequestDto. riderService.requestRide(rideRequestDto)- Delegates the ride creation logic to the service layer (
RiderService). - Typically, this service will:
- Validate the request data.
- Persist a new
RideRequestin the database. - Possibly trigger driver-matching asynchronously (via
RideStrategyManagerandRideService).
ResponseEntity.ok(...)- Wraps the resulting
RideRequestDtoin an HTTP 200 OK response.
Observations & Best Practices
- DTO Usage
- Using
RideRequestDtoensures the controller does not expose internal entities directly.
- Validation
- You could add
@Validto@RequestBodyto validate fields (like pickup and drop-off locations).
- Security / Authentication
- Ensure only authenticated riders can request rides.
- The service should associate the ride request with the currently logged-in rider.
- Asynchronous Driver Matching
- Right now, the controller simply creates the ride request.
- Actual driver matching should happen asynchronously in the service layer to avoid blocking the API.
- Error Handling
- If ride creation fails (invalid coordinates, database issue, no available drivers), proper HTTP error codes should be returned via
@ControllerAdvice.
Flow Summary
- Rider sends POST request to
/riders/requestRidewith ride info. - Controller deserializes the request to
RideRequestDto. - Controller calls
riderService.requestRide(). - Service persists the ride request and may trigger driver-matching logic.
- Controller returns
RideRequestDtoto the rider in HTTP 200 OK.
GlobalResponseHandler
Class-level
@RestControllerAdvice
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {
@RestControllerAdvice:- A specialized
@ControllerAdvicethat applies to all@RestControllers. - Can intercept responses before they are sent to the client.
implements ResponseBodyAdvice<Object>:- Allows intercepting and modifying the response body globally.
- Generic
<Object>means it applies to all response types.
Purpose: Wrap all API responses in a consistent structure (ApiResponse) unless they are already wrapped or on certain excluded routes.
Method: supports
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
- Determines which controller responses should be intercepted.
- Returning
truemeans all responses are intercepted. - Spring calls
beforeBodyWriteonly for responses that match this method.
Method: beforeBodyWrite
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class<? extends
HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response)
- Invoked just before the response is written to the HTTP response body.
- Parameters:
body— the object returned by the controller.returnType— the method signature of the controller.selectedContentType— content type negotiated for response (e.g., JSON).selectedConverterType— converter used to serialize the response.request— HTTP request object.response— HTTP response object.
Excluded routes
List<String> allowedRoutes = List.of("/v3/api-docs", "/actuator");
boolean isAllowed = allowedRoutes
.stream()
.anyMatch(route -> request.getURI().getPath().contains(route));
- Maintains a list of routes that should not be wrapped in
ApiResponse. /v3/api-docs→ Swagger/OpenAPI JSON./actuator→ Spring Actuator endpoints.isAllowedwill betrueif the request path contains any of these routes.
Why: Wrapping these endpoints would break tools like Swagger UI or monitoring dashboards.
Wrapping responses
if(body instanceof ApiResponse<?> || isAllowed) {
return body;
}
return new ApiResponse<>(body);
- If the response body is already an
ApiResponse, or the request is on an excluded route, return it as-is. - Otherwise, wrap the response in a standard
ApiResponseobject.
**ApiResponse** is presumably a generic wrapper like:
public class ApiResponse<T> {
private T data;
private String status = "success";
private String message = null;
public ApiResponse(T data) {
this.data = data;
}
}
- This provides a consistent structure for all API responses.
- Benefits:
- Clients always know the format (
data,status,message). - Easier error handling and front-end integration.
- Makes future response metadata (pagination, timestamps) easy to add globally.
Summary
GlobalResponseHandlerintercepts all controller responses.- Excludes certain routes (
/v3/api-docs,/actuator) from wrapping. - Wraps everything else in
ApiResponseto standardize the API response structure. - Prevents duplication if a response is already an
ApiResponse.
Notes / Best Practices
- You could make the excluded route check configurable via properties for easier maintenance.
- Consider handling null bodies to avoid sending
{ "data": null }if not desired. - Works well with your
DriverControllerandRiderControllerbecause all their responses will automatically be wrapped in a consistent JSON structure.
RideStartDto
Class-level
@Data
public class RideStartDto {
@Data(from Lombok):- Generates boilerplate code automatically:
- Getters and setters for all fields
toString()methodequals()andhashCode()methods- A default constructor
- Makes the class simple and concise.
public class RideStartDto:- A simple Data Transfer Object (DTO) used to carry data from the client to the server.
- In this case, it represents the data needed to start a ride.
Field
String otp;
otpstands for One-Time Password, which is used to verify the ride before starting.- This field is expected in the JSON request body sent by the driver when starting a ride.
Example JSON request:
{
"otp": "1234"
}
- Spring automatically maps this JSON into an instance of
RideStartDtobecause your controller method uses:
@RequestBody RideStartDto rideStartDto
Usage in your application
- Used in
DriverController.startRide:
driverService.startRide(rideRequestId, rideStartDto.getOtp());
- The OTP from this DTO is sent to the service layer to:
- Verify that the driver starting the ride is authorized.
- Check that the OTP matches the ride’s OTP.
- Allow the ride to be started (
RideStatus.ONGOING) if verification passes.
Summary
RideStartDtois a simple DTO to carry the OTP from the front-end to your back-end.- The
@Dataannotation avoids writing getters/setters manually. - Its main purpose is to provide type-safe access to the OTP for ride-start validation.
메타데이터
- post_id
- dc8a5f613e56
- slug
- test-dc8a5f613e56
- url
- https://medium.com/@yasiffkhan/test-dc8a5f613e56
- canonical_url
- https://medium.com/@yasiffkhan/test-dc8a5f613e56
- author_url
- https://medium.com/@yasiffkhan
- status
- ok
- fetched_at
- 2026-06-20 20:29:01