
Building a rental platform isn’t just about showing car images; it’s about state management, temporal availability, and iron-clad audit trails. OnTheGoRentals started as a solution for that, and it taught us a lot about modernizing legacy rental concepts with Spring Boot and Vue. In this deep-dive dev diary, we’re pulling back the curtain on how we architected a production-ready rental SaaS from database locking up to observability dashboards.
What You’ll Learn
-
The BFF Architecture
Building a secure Backend-for-Frontend with Spring Security and Vue 3’s Composition API.
-
Concurrency & Locking
Tackling temporal double-booking overlaps using database-level pessimistic write locking.
-
Full-Stack Observability
Deploying a real-time production metrics engine using Prometheus, Grafana, and Loki.
Genesis: The Accidental Fleet Manager
A few months ago, a local car rental agency approached us. Their operational dashboard was a sprawling, multi-tabbed Google Sheet. Booking overlaps were handled via frantic WhatsApp groups, client contracts were compiled manually in MS Word, and payment confirmations required eyes-on-glass monitoring of bank alerts. It was a classic administrative bottleneck screaming for modern software engineering.
We designed OnTheGoRentals with a strict goal: build a reliable system that can scale gracefully to multiple locations while keeping operational latency near-zero. Instead of rushing headfirst into microservices—which often introduces massive DevOps overhead for small-to-medium operations—we chose a Decoupled Monolith with BFF (Backend-for-Frontend) pattern.
Architecture Selection: Why This Stack?
Our stack consists of Java 21, Spring Boot 3.5.0, and Spring Security 6.5.0 on the backend, with a highly responsive SPA built on Vue.js 3 (Composition API). By structuring the backend to act as a dedicated BFF, we kept the system cohesive, easy to monitor, and highly optimized for our frontend application.
| Requirement | Architectural Answer | Primary Benefit |
|---|---|---|
| Robust Concurrency | Java Virtual Threads (JDK 21) | High throughput with low resource footprint during high IO booking writes. |
| Strict Security | Spring Security & HttpOnly JWTs | Inherent XSS protection with stateless horizontal scaling. |
| Rich Interaction | Vue 3 + Tailwind CSS | Snappy administrative dashboard with clean, modular views. |
The Blueprint: Implementing a Real BFF Pattern
A major design trap in modern full-stack development is letting your single-page app communicate directly with generic REST resource endpoints. This leaks business logic into the client, forces excessive network overhead, and complicates security mapping. Our Vue 3 app never directly talks to a raw data layer; it routes through custom BFF controllers inside the Spring Boot monolith.
The BFF orchestrates complex operations. For instance, loading the user’s booking history doesn’t just return a list of entity rows. It queries active rentals, calculates real-time late return penalties, checks against active promotions, and formats the response precisely in the shape the Vue client requires.
@RestController
@RequestMapping(“/api/v1/bff/bookings”)
public class BookingBffController {
private final BookingService bookingService;
private final PricingService pricingService;
@GetMapping(“/active”)
public ResponseEntity<ActiveBookingResponse> getActiveBookings(Principal principal) {
User user = userService.findByEmail(principal.getName());
List<Booking> bookings = bookingService.findActiveForUser(user.getId());
// BFF acts as orchestrator: computes real-time pricing and formats precisely for Vue
return ResponseEntity.ok(ActiveBookingResponse.from(bookings, pricingService));
}
}
On the Vue 3 side, we leveraged Vite 6 and the Composition API to consume these customized endpoints seamlessly. Using modular API service definitions, our components stay clean and completely independent of any server-side database structures.
Authentication & 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.
Let’s look at the JWT filter chain configuration we built in Spring Security 6.5. This handles extracting the token and building the security context without standard session overhead:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = extractTokenFromHeader(request);
if (token != null && jwtProvider.validateToken(token)) {
Authentication auth = jwtProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
The Temporal Matrix: Concurrency & Booking Logic
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.
War Story — The Phantom Booking Overlap
During initial load testing, we simulated 200 concurrent users attempting to book the last available SUV in the fleet at the same time. Because our transactions were configured with standard READ_COMMITTED isolation levels, 4 separate users succeeded in booking the same car for overlapping dates. The database allowed the commits because they happened within overlapping transaction windows.
To solve this, we implemented a strict database-level locking strategy using Spring Data JPA. We bypassed basic optimistic locking (using @Version) because it would cause 99% of concurrent booking requests to fail with exceptions. Instead, we used Pessimistic Write Locking on the Car Fleet entity during validation:
public interface CarRepository extends JpaRepository<Car, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(“SELECT c FROM Car c WHERE c.id = :carId”)
Optional<Car> findAndLockById(@Param(“carId”) Long carId);
}
This lock executes a native SELECT ... FOR UPDATE in MySQL. When Transaction A calls this method, it blocks Transaction B from reading or writing to that specific car row until Transaction A commits or rolls back. This ensures absolute, serialized verification of availability without leaking state into complex microservice queues.
Booking Query Logic
To check for overlapping bookings, the service executes the following logic:
SELECT COUNT(*) FROM Booking b
WHERE b.car_id = :carId
AND b.status IN (‘CONFIRMED’, ‘ACTIVE’)
AND (:startDate < b.end_date AND :endDate > b.start_date)
This overlapping 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 pessimistic write lock completely eliminated double-bookings.
Observability Stack: Engineering with Eyes Open
You can’t manage what you don’t measure. For OnTheGoRentals, we built a production-grade monitoring stack directly into our Docker layout. We didn’t want to wait for users to report slow dashboard loads or failed checkouts; we wanted real-time telemetry.
The system uses Prometheus to scrape custom Actuator metrics from the Spring Boot application, Loki with Promtail for centralized log collection, and Grafana for visual dashboards. This gives us full system visibility, from JVM heap utilization to individual endpoint latencies.
Monitoring Infrastructure Diagram
Spring Actuator
→
Prometheus
→
Grafana Dashboards
Looking Back: Technical Debt & Architectural Evolution
No project is perfect, and OnTheGoRentals is no exception. As we prepared the v2.0-beta release, we identified several structural areas ripe for future architectural evolution. The most prominent bottleneck is our transactional email relay system.
Currently, when a booking is confirmed, the Spring Boot application synchronously renders an HTML template using Thymeleaf and sends it to our email provider. This adds up to 1.5 seconds of latency to the checkout response. In our next iteration, we’ll shift this operation to an asynchronous event-driven model using Spring’s @EventListener and an underlying active MQ like Kafka or RabbitMQ.
Need to Build a Custom Enterprise Platform?
OnTheGoRentals proves that with a clean monolithic blueprint, deep concurrency planning, and solid DevOps monitoring, you can build a highly performant application that stays rock-solid under enterprise loads. If you want to replace your messy spreadsheets and administrative bottlenecks with clean, custom-crafted software, let’s build together.
Related Reading
- TorqueBooks: Workshop Management System Case Study — Exploring similar modular architectures using PocketBase.
- Self-Hosted CI/CD on a Home Rack — Behind the scenes of the automation pipeline that deploys our Dockerized services.
- From Rebuilding Auth to a Shared Identity Layer — Deeper strategies for structuring robust login services across modern web applications.