Over Engineering on Purpose
For the longest time, i’ve built applications as a monoliths.And honestly? They work.A single Spring Boot app , a PostgreSQL database, deploy it somewhere, done.There is nothing wrong with that.
But here's the thing — in today's job market, "I can build a monolith" doesn't turn heads the way it used to. People want to hear about service discovery, distributed tracing, message queues, API gateways. They want microservices experience. And I didn't have it.
So I decide to build it. Not a toy project with two services that share a Todo list. A real platform, with real architectural decisions, real tradeoffs, and - I’ll be upfront about this- alot of deliberate over-engineering.
This series is me documenting that journey.Every decision, every mistake, every moment where i chose the “wrong” tool on purpose because i wanted to understand how it worked.
The idea
I needed a project complex enough to justify a microservice architecture. Something with multiple bounded contexts, different scaling requirements, and enough moving parts to make the infrastructure interesting.
Here's what I landed on.
If you've ever tried to rent heavy machinery — a cement mixer, an excavator, a tractor — you know the process is messy. Either you go through a big company with big-company pricing, or you rely on word of mouth. You know John, who knows Brian, who knows Jane's brother-in-law who owns a tractor and might rent it out.
RentItUp is a platform where machine owners list their equipment with pricing (per hour, per day, per week), and customers can search, book, and pay — all in one place. Think of it as an Airbnb for machinery.
Now, does a machinery rental platform need microservices? Absolutely not. A well-structured monolith would handle this just fine. But that's not the point. The point is learning, and a rental platform gives me enough domain complexity to make the architecture meaningful:
- •
Users -with different roles (owners, customers, admins) and verification requirements
- •
A catalog -of machines with categories, images, pricing models, and location data
- •
Bookings - with availability checks, payments, and scheduling
- •
Notifications - triggered by events across the system
- •
Maintenance records -and condition tracking
Each of these maps naturally to a service boundary, which is exactly what I need.
The Architecture
Here's the system I designed. Some of these choices are practical, some are aspirational, and some are purely because I want to learn the technology. I'll be honest about which is which.
The Services
I'm starting with four microservices:
User Service — handles authentication, user profiles, and KYC verification. This is the source of truth for who you are in the system.
Catalog Service — manages machines, categories, images, and maintenance records. This is the heaviest service in terms of data and the one customers interact with most when browsing.
Booking Service — manages rental transactions, payments, and reviews. This service needs to talk to both the Catalog Service (is the machine available?) and the User Service (who's booking this?).
Notification Service — sends emails, SMS, and push notifications. This one is stateless. It doesn't own a database. It just listens for events and acts on them.
The Tech Stack
Spring Boot is the core. I work in the Spring ecosystem professionally, so this is where I'm most productive. Every service is a Spring Boot application.
gRPC for inter-service communication. This is where the deliberate over-engineering starts. REST would be simpler. But gRPC gives me an excuse to learn Protocol Buffers, think about contract-first design, and understand how binary serialization differs from JSON. The performance benefits are a bonus, not the motivation.
PostgreSQL with schema-based separation. Instead of a database per service (which is the textbook recommendation), I'm using a single PostgreSQL instance with separate schemas — users, catalog, booking. Each service only has access to its own schema. This gives me logical separation without the operational overhead of managing multiple database instances during development. In production, you'd likely split these out.
RabbitMQ for async messaging. The Notification Service doesn't receive direct gRPC calls. Instead, the other services publish events (booking.created, payment.received, user.verified) and the Notification Service consumes them. This is a natural fit — notifications don't need to be synchronous.
Redis for caching. The Catalog Service will cache machine listings and search results. The API Gateway will use it for rate limiting. Redis is one of those tools I've used superficially but never configured properly in a distributed context.
Spring Cloud Eureka for service discovery. Services register themselves on startup and discover each other at runtime. Is this necessary when I'm running everything locally? No. But it's the pattern used in production, and I want to understand the registration, heartbeat, and deregistration lifecycle.
Spring Cloud Config Server for centralized configuration. All service configurations live in a Git repository. Services pull their config on startup. This means I can change a setting without redeploying a service.
Zipkin for distributed tracing. When a booking request flows through the API Gateway → Booking Service → Catalog Service → User Service, I want to see that entire chain in one view with timing data.
Resilience4j for fault tolerance. Circuit breakers, retries, rate limiting — the tools you need when one service going down shouldn't take the whole system with it.
The Flow
Here's what a typical request looks like:
Client sends: GET /api/v1/machines?category=excavator
API Gateway (BFF):
- Validates the JWT
- Converts the JSON request to a Protobuf message
- Makes a gRPC call to the Catalog Service
Catalog Service:
- Queries the catalog schema in PostgreSQL
- Returns a Protobuf response with the machine listings
API Gateway:
- Converts the Protobuf response back to JSON
- Returns it to the clientFor operations that span services — like creating a booking that needs to check machine availability — the Booking Service makes its own gRPC call to the Catalog Service. The user's JWT gets forwarded through the chain so every service knows who initiated the request.

How I’m learning This
A quick note on approach, because this shapes how the series will read.
I'm using AI — mostly Claude — as a place where i can bounce my Ideas of from.Not to write my code, but to help me think through architectural decisions. "Should I use a database per service or schemas?" "How does gRPC handle load balancing with Eureka?" I get a direction, then I go read about it, then I implement it.
There's a tension in learning something new between going deep and going wide. Recently I was reading about RabbitMQ. You start with the high-level concepts: exchanges, bindings, queues, routing keys. Then you hit AMQP — the protocol that defines how messages are formatted and transmitted. You could stop there and start building, or you could go deeper. How does the queuing protocol actually work internally? How are protocols designed? My rule of thumb: go wide first. Understand enough to build. Go deep when you hit a wall. That wall will come — it always does — and when it does, you'll have enough context to know exactly what you need to learn next. This series follows that pattern. I'll explain what I understood, what I built, what broke, and what I had to go back and learn properly. It won't be a polished tutorial where everything works on the first try. It will be honest.
What’s Next
In the next post, I'll set up the monorepo with Gradle, define the gRPC contracts in Protocol Buffers, and explain why contract-first design forces you to think about your domain boundaries before writing a single line of service code.