After exploring the theory behind microservices in Day 0, it’s time to define what we’re actually building and why.
What Are We Building?
A ride-sharing platform — think Uber or Lyft. At its core, the problem is simple: a rider wants to go somewhere, and a driver can take them there. But the real complexity comes from making this work in real time.
The key user flows are:
- Rider: pick a location on a map, preview the trip/fare, request a ride, get matched with a driver, and pay
- Driver: register with a car package, see incoming trip requests, accept or decline, and navigate to the rider
What makes this interesting from a systems perspective is that nearly every interaction is time-sensitive and event-driven. When a rider requests a trip, we need to notify available drivers immediately. When a driver accepts, the rider needs to know right away. Polling or REST request-response alone won’t cut it.
Why Microservices?
This project is a natural fit for microservices because the domain breaks cleanly into independent responsibilities:
- Trip Service — manages the trip lifecycle (creation, driver assignment, completion)
- Driver Service — handles driver registration, availability, and location tracking
- Payment Service — processes payments via Stripe
- API Gateway — the single entry point that bridges HTTP/WebSocket clients to the backend
Each of these can be developed, deployed, and scaled independently. The Trip Service doesn’t need to know how payments work — it just emits a trip.event.completed event, and the Payment Service reacts to it.
This is the hybrid approach from Day 0 in action: the API Gateway handles synchronous HTTP requests from the frontend, while backend services communicate asynchronously through RabbitMQ.
Choosing the Communication Pattern
From what I covered in Day 0, the hybrid approach made the most sense here:
- Client-to-Gateway: Synchronous HTTP for things like creating a trip or fetching driver info, plus WebSockets for real-time updates (driver location, trip status changes)
- Service-to-Service: Asynchronous messaging via RabbitMQ using a topic exchange
I settled on a routing key convention to keep things organized:
| Pattern | Description |
|---|---|
trip.event.* | Trip lifecycle events (created, driver_assigned, no_drivers_found) |
driver.cmd.* | Driver commands (trip_request, trip_accept, trip_decline, location) |
payment.event.* | Payment events (session_created, success, failed, cancelled) |
payment.cmd.* | Payment commands (create_session) |
The distinction between event and cmd is intentional — events describe something that happened (past tense, broadcast), while commands request an action (imperative, targeted). This keeps the message flow predictable.
Structuring the Services
One thing I wanted to get right early was the internal structure of each service. I went with Clean Architecture, where each service follows this layout:
services/<name>-service/
├── cmd/ # Entry point
├── internal/
│ ├── domain/ # Interfaces & business models
│ ├── service/ # Business logic
│ └── infrastructure/ # Adapters (events, gRPC, repository)
└── pkg/types/ # Public types
The key idea: domain logic has zero dependencies on infrastructure. The domain layer defines interfaces (e.g., TripRepository), and the infrastructure layer provides concrete implementations (e.g., a MongoDB adapter). This makes it easy to swap out a database or message broker without touching business logic.
I also built a small scaffolding tool (go run tools/create_service.go -name <service>) so spinning up a new service with this structure takes seconds instead of manually creating directories and boilerplate.
The Trip Scheduling Flow
Here’s the full flow I designed for trip scheduling — from the rider requesting a ride to a driver being assigned:
What I Learned
- Start with the domain, not the tech. It was tempting to jump straight into setting up RabbitMQ and Kubernetes, but defining the business flows first made every technical decision easier — the architecture fell out naturally from the problem.
- Events vs. commands is a useful mental model. Separating “something happened” from “please do something” made the message flow much cleaner and easier to reason about.
- Clean Architecture pays off early. Even in a small project, having clear boundaries between domain logic and infrastructure made it easy to iterate without breaking things.