Why We Built Dineshstack Ride hailing: Choosing a Modular Laravel + Node Microservice Architecture
Dinesh Wijethunga
When I started building this Ride hailing β a ride-booking and fleet-management platform β the first decision I had to make wasn't about databases or frameworks. It was about philosophy: how do you build a system that needs to be reliable, real-time, and scalable all at the same time, without drowning in complexity from day one?
This post is the story of that decision.
The Problem Space
A ride-booking platform is deceptively complex. On the surface it looks like: customer taps "book", driver gets a notification, trip happens, payment settled. Under the hood you are solving three fundamentally different engineering problems simultaneously.
Transactional integrity β a booking must go through strict legal state transitions. Money cannot be charged twice. Offers cannot be sent to a driver who already has one pending. You need a database, strong consistency, and business rules enforced by code.
Real-time communication β a driver needs to receive an offer the moment it is generated, not after a REST poll. A rider needs to see their driver moving on the map. This demands persistent WebSocket connections and millisecond-level message delivery.
Geospatial dispatch intelligence β finding the right driver within a radius, ranking by ETA, avoiding drivers with live offers, respecting surge pricing rules β this is a continuous stream of computation that happens in the background, not inside a request-response cycle.
These three problems have very different characteristics. Mixing them into a single monolith under pressure leads to an architecture where you can't scale one dimension without over-provisioning the others, and where a bug in dispatch intelligence can take down the booking API.
Why Laravel for the Core
Laravel was the right choice for the transactional layer. Not because it is the fastest framework or the most fashionable, but because it has exactly the primitives needed: Eloquent with transactions for atomic state writes, Passport for OAuth2 token auth, Spatie Permission for role-based access control scoped to teams, and nwidart/laravel-modules for keeping feature domains isolated without going full microservice.
The modules structure matters. Each feature β Booking, BookingConfig, User, Notifications, Documents, Gallery β lives in its own directory with its own controllers, models, migrations, routes, DTOs, and tests. They share the same database and runtime, but their code is isolated by boundary. Adding or disabling a module requires one line in modules_statuses.json.
This is intentional. It gives you the deployment simplicity of a monolith while giving you the codebase discipline of microservices.
Why Node.js for Real-Time and Dispatch
Laravel is not the right tool for persistent WebSocket connections or for running a continuous geospatial search loop. Node.js on the other hand is built around an event loop that handles thousands of concurrent connections with a tiny memory footprint.
The decision was to build three dedicated Node services. node-realtime is a Socket.io server that brokers all real-time messages between the Laravel backend and mobile clients. node-demand is a background dispatch engine that listens for booking events and runs the driver search and offer loop. node-gateway is a public API gateway built with Fastify that is the single entry point for all clients.
Each Node service has one job. node-realtime does not know how to dispatch drivers. node-demand does not hold WebSocket connections. node-gateway does not run business logic.
How They Connect
The three layers communicate through two mechanisms.
Kafka handles asynchronous events. When a booking transitions to "Searching", the Laravel state machine publishes a booking.searching event to Kafka. node-demand consumes it and starts the dispatch loop. When a driver accepts, node-demand calls Laravel via HTTP. Laravel transitions state and publishes booking.accepted to Kafka. node-realtime consumes that and pushes the update to both rider and driver over their WebSocket connections.
This decoupling means Laravel never blocks waiting for Node. Node never blocks waiting for Laravel. If one service restarts, Kafka holds the events and delivers them on recovery.
Redis handles shared state. Driver locations are stored in a Redis GEO structure. When node-demand searches for nearby drivers it queries Redis, not MySQL. This is orders of magnitude faster for geospatial proximity queries. Redis also holds a short-lived key booking:offer:{driverId} so the dispatch engine can skip drivers who already have a live offer outstanding.
HTTP handles synchronous handoffs for operations that need an immediate response β sending a driver offer, confirming fare, recovering booking state after a reconnect.
The Gateway as the Public Face
node-gateway is the only port exposed to the internet. Clients never call Laravel or node-realtime directly. The gateway validates every Bearer token via Laravel's introspection endpoint, strips any user-identity headers that clients might try to inject, and injects canonical X-User-Id, X-User-Type, X-Roles headers so downstream services can trust them without re-validating.
This trusted header injection pattern means each upstream service can focus on its job rather than repeating auth logic.
The Monitoring Layer
From the beginning every layer was instrumented with Prometheus metrics. Laravel exposes a scrape endpoint at /api/v1/internal/metrics. Each Node service exposes its own /metrics. Prometheus scrapes all of them every 15 seconds. Grafana provides dashboards for booking funnel health, dispatch success rate, API latency, and infrastructure vitals.
This was not an afterthought. Building metrics in from day one means you catch problems before users report them.
What This Architecture Buys You
No single part of this is clever on its own. The value is in how deliberately each piece owns one responsibility and communicates cleanly with the others. Business logic, state, and payments live in Laravel for strong consistency and transactions. Real-time messaging lives in node-realtime for its event loop handling 10k+ concurrent connections. Dispatch intelligence lives in node-demand as Kafka-driven background work. The public entry point and auth live in node-gateway as a single security surface. Kafka provides a durable, replayable, decoupled event bus. Redis GEO delivers sub-millisecond proximity queries. Prometheus and Grafana provide industry-standard observability with no vendor lock-in.
The next posts in this series go deep on each layer: the pricing engine, the booking state machine, the dispatch algorithm, the gateway security model, and the monitoring stack.
Reviews & Ratings
Sign in to leave a review.
