
Part 1 of our OnTheGoRentals dev diary. In this installment, we break down the modular monolithic architecture, the domain model with immutable entities, and why we chose Builder patterns over mutable state.
What You’ll Learn
-
Modular Monolith Architecture
Why we chose a clean monolithic blueprint over premature microservices.
-
Immutable Domain Entities
How Builder patterns enforce state integrity across booking lifecycle transitions.
-
Layered Spring Boot Architecture
Controllers, Services, Repositories — the standard Spring layout that scales.
Why We Built This
OnTheGoRentals started as an exploration of how far you can push a Spring Boot monolith before needing to split into microservices. We wanted to build a realistic domain — car rental has temporal availability, complex state machines, and concurrent access patterns — while creating something genuinely useful.
The result is a production-deployed platform that’s live and functional. It’s also a reusable template: the booking engine, authentication system, and admin dashboard can be adapted for bicycles, houses, tools, or any rentable asset with minimal changes.

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). The backend follows standard Spring Boot layered architecture: Controllers, Services, Repositories, JPA Entities.
| Layer | Technology | Key Files |
|---|---|---|
| REST Controllers | Spring MVC + Validation | BookingController, CarController, AuthController |
| Service Layer | Spring Service + Transactional | BookingServiceImpl, CarServiceImpl, AuthServiceImpl |
| Repository Layer | Spring Data JPA + JPQL | BookingRepository, CarRepository |
| Domain Entities | JPA + Lombok Builder | Booking, Car, User, Rental, DamageReport |
| Security | Spring Security 6.5 + JWT | JwtAuthenticationFilter, GoogleOAuth2UserService |
| Storage | MinIO + Local Fallback | MinioStorageService, LocalFileStorageService |
The Domain Model: Immutable Entities and Builder Patterns
The foundation of OnTheGoRentals is a carefully designed domain model where entities are immutable once created. Every Booking, Car, and User is built using the Builder pattern, ensuring that once an object enters the system, it cannot be accidentally mutated. This design choice forced us to think deeply about state transitions from the very beginning.
The Booking Entity
The Booking entity is the heart of the system. It tracks who rented what, when, and in what state the reservation currently sits:
@Entity
@Table(name = “booking”)
@Getter
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public class Booking {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false)
private UUID uuid;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = “user_id”, nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = “car_id”, nullable = false)
private Car car;
@Column(nullable = false)
private LocalDateTime startDate;
@Column(nullable = false)
private LocalDateTime endDate;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private BookingStatus status;
@Column(nullable = false)
private BigDecimal totalCost;
@Column(nullable = false)
private boolean deleted;
}
The @Builder(toBuilder = true) annotation is critical. It allows us to create new instances from existing ones without mutation—essential for status transitions. When a booking moves from PENDING to CONFIRMED, we don’t modify the existing object; we build a new one with the updated status.
The Booking Status Lifecycle
Bookings flow through a defined state machine:

This lifecycle is enforced in BookingServiceImpl, where each status transition is validated before execution. You can’t jump from PENDING to RENTAL_COMPLETED—you must go through CONFIRMED and RENTAL_INITIATED first.
The Live Interface
Here’s the live homepage — clean, focused, and ready for customers:

The car listing page showing the fleet with search, filtering, and date-based availability:

And the car detail page with image carousel, specifications, and booking:

Ready for Part 2?
In the next installment, we tackle the hardest problem in rental systems: preventing double-bookings with JPQL interval queries, and building a secure JWT authentication architecture.
Related Reading
- TorqueBooks: Workshop Management System Case Study — Exploring similar modular architectures using PocketBase.
- Vue 3 Composition API: Real-World Patterns — How we structured the Vue 3 frontend for OnTheGoRentals.
- From Rebuilding Auth to a Shared Identity Layer — Deeper strategies for structuring robust login services.