
Part 2 of our OnTheGoRentals dev diary. We tackle the hardest problem in rental systems: preventing double-bookings with JPQL interval queries, and building a secure JWT authentication architecture with Google OAuth2.
What You’ll Learn
-
Temporal Overlap Detection
Using JPQL interval queries to prevent double-bookings without pessimistic locking.
-
JWT Security Architecture
HttpOnly cookie refresh tokens, Google OAuth2 integration, and Spring Security 6.5 filter chains.
-
Atomic Check-and-Commit
Running overlap validation inside the same transaction as booking creation.
The Temporal Matrix: Solving Double-Bookings with JPQL
The single most complex problem in OnTheGoRentals was scheduling. A car is not a stateless object like a book or a retail product; its availability is fluid and bounded by time. To check if a car can be booked for a given range (e.g., [2026-07-20 to 2026-07-25]), we must query the database to ensure no overlapping bookings exist.
In high-traffic environments, standard application-level checks fail because of race conditions. Two clients can query the database simultaneously, see the same car as available, and both commit bookings for the exact same date range, creating a devastating double-booking conflict.
The Concurrency Challenge
During load testing, we simulated 200 concurrent users attempting to book the last available SUV in the fleet at the same time. With standard READ_COMMITTED isolation levels, multiple users succeeded in booking the same car for overlapping dates. The database allowed the commits because they happened within overlapping transaction windows.
The root cause? Our initial implementation used a simple findByCarIdAndStatus() query that returned all bookings for a car, then checked for overlaps in Java code. By the time the application checked, another transaction had already committed. This is the classic “check-then-act” race condition.
The Solution: JPQL Interval Overlap Query
Instead of pessimistic locking (which would cause 99% of concurrent booking requests to fail with exceptions), we implemented a JPQL interval overlap query in the repository layer. This query runs inside the same transaction as the booking creation, ensuring atomic check-and-commit:
@Query(“SELECT b FROM Booking b ” +
“WHERE b.car.id = :carId ” +
“AND b.status = :status ” +
“AND b.deleted = false ” +
“AND b.startDate < :proposedEndDate " +
“AND b.endDate > :proposedStartDate”)
List<Booking> findOverlappingBookings(
@Param(“carId”) Integer carId,
@Param(“status”) BookingStatus status,
@Param(“proposedStartDate”) LocalDateTime proposedStartDate,
@Param(“proposedEndDate”) LocalDateTime proposedEndDate
);
This query represents a classic mathematical overlap interval: if a requested range starts before an existing range ends, and ends after the existing range starts, they must overlap. Running this query under a @Transactional annotation with READ_COMMITTED isolation ensures that the check and the subsequent insert happen atomically.
The Double-Booking Check in Service Layer
The BookingServiceImpl.isCarDoubleBooked() method orchestrates the overlap detection:
private boolean isCarDoubleBooked(Car car, LocalDateTime proposedStartDate,
LocalDateTime proposedEndDate, Integer excludeBookingId) {
List<Booking> overlappingBookings = bookingRepository
.findOverlappingBookings(car.getId(), BookingStatus.CONFIRMED,
proposedStartDate, proposedEndDate);
if (overlappingBookings.isEmpty()) {
return false;
}
if (excludeBookingId != null) {
boolean otherBookingOverlaps = overlappingBookings.stream()
.anyMatch(b -> b.getId() != excludeBookingId.intValue());
return otherBookingOverlaps;
}
return true;
}
The excludeBookingId parameter is crucial for update scenarios. When a user modifies an existing booking’s dates, we need to check for overlaps with other bookings—not the booking being updated. This prevents the system from flagging a booking as conflicting with itself.
Total Cost Calculation
Pricing is another temporal calculation. The calculateTotalCost() method in BookingServiceImpl computes the rental cost based on the number of days:
private BigDecimal calculateTotalCost(Car car, LocalDateTime startDate, LocalDateTime endDate) {
long days = ChronoUnit.DAYS.between(startDate, endDate);
if (days < 1) days = 1; // Minimum 1-day rental
return car.getDailyRate().multiply(BigDecimal.valueOf(days));
}
This simple calculation belies the complexity of real-world rental pricing. In production, you’d add surge pricing, loyalty discounts, and seasonal adjustments. But the current implementation keeps it straightforward — fewer bugs, easier auditing, and a solid foundation for future extensions.
Authentication and Security: Building a Digital Vault
Authentication in a modern web app is notoriously hard to get right. Standard stateless setups send JWTs in local storage, opening the door wide for Cross-Site Scripting (XSS) attacks. We went down a different road, designing a Secure Double-Token Architecture built around HttpOnly cookies and short-lived access tokens.

When a user registers or logs in—either via traditional password authentication or through Google OAuth2—the backend issues an access token (JWT) and a securely-hashed refresh token. The access token is delivered in a JSON payload, whereas the refresh token is written to an encrypted HttpOnly, Secure, SameSite=Strict cookie. This architecture combines stateless authorization with robust security.
Security Lesson: Keep Tokens Scoped and Secure
By preventing the frontend Javascript from ever touching the refresh token, we isolated the session refresh cycle. Even if a rogue browser extension manages to infect the client-side environment, it cannot extract the cookie to hijack sessions offline. This is a baseline standard we enforce across all our identity platforms.
The real login page — note the “Login with Google” button for OAuth2:

And the signup page with full registration form:

Ready for Part 3?
In the final installment, we cover the infrastructure: MinIO object storage, the Prometheus + Grafana + Loki observability stack, Docker deployment, and the technical debt we identified along the way.
Related Reading
- From Rebuilding Auth to a Shared Identity Layer — Deeper strategies for structuring robust login services across modern web applications.
- Self-Hosted CI/CD on a Home Rack — Behind the scenes of the automation pipeline that deploys our Dockerized services.
- Vue 3 Composition API: Real-World Patterns — How we structured the Vue 3 frontend for OnTheGoRentals.