A high-performance ride pooling backend that groups airport-bound passengers into shared cabs, minimizing travel deviation while respecting seat/luggage constraints.
Built for: <300ms latency Β· 100 RPS Β· 10,000 concurrent users
Tech Stack: Go 1.22 Β· PostgreSQL 16 + PostGIS 3.4 Β· Redis 7 Β· Docker
- Docker & Docker Compose
That's it. No Go, PostgreSQL, or Redis installation needed.
docker-compose up --build -dThis automatically:
- Builds the Go binary in a multi-stage Docker image
- Starts PostgreSQL (PostGIS), Redis, and the application
- Waits for database health checks to pass
- Applies the schema migration
- Starts the server on
http://localhost:8080
curl http://localhost:8080/health{"status":"ok","services":{"postgres":"healthy","redis":"healthy"}}docker-compose down # Keep data
docker-compose down -v # Wipe all data + volumesHintro/
βββ cmd/server/main.go # HTTP entry point, router, graceful shutdown
βββ config/config.go # Viper-based configuration loader
βββ pkg/
β βββ db/postgres.go # PostgreSQL connection pool (pgxpool)
β βββ cache/redis.go # Redis connection pool (go-redis)
β βββ geo/geo.go # Haversine distance, route time estimation
βββ internal/
β βββ model/model.go # Domain models, enums, DTOs
β βββ repository/
β β βββ ride_repository.go # PostGIS spatial queries (matching)
β β βββ booking_repository.go # Transactional booking (FOR UPDATE)
β β βββ pricing_repository.go # Demand/supply from Redis + PostGIS
β βββ service/
β β βββ matching.go # Greedy heuristic ride matcher
β β βββ booking.go # Booking with pessimistic locking
β β βββ pricing.go # Dynamic fare + surge pricing
β βββ handler/
β βββ handler.go # Match endpoint
β βββ booking_handler.go # Booking endpoint
β βββ pricing_handler.go # Fare estimation endpoint
βββ migrations/
β βββ 001_create_schema.up.sql # Full schema (PostGIS, indexes, triggers)
β βββ 001_create_schema.down.sql # Rollback script
βββ docs/
β βββ HIGH_LEVEL_ARCHITECTURE.md # HLD: system diagram, deployment
β βββ LOW_LEVEL_DESIGN.md # LLD: class diagram, patterns
β βββ ASSIGNMENT_VERIFICATION.md # Requirement checklist
β βββ openapi.yaml # OpenAPI 3.0 spec
β βββ Hintro.postman_collection.json
βββ Dockerfile # Multi-stage build (builder β alpine)
βββ docker-compose.yml # One-command orchestration
βββ entrypoint.sh # Auto-migration on startup
| Artifact | Location | Usage |
|---|---|---|
| High-Level Architecture | docs/HIGH_LEVEL_ARCHITECTURE.md |
System diagram, components, scaling |
| Low-Level Design | docs/LOW_LEVEL_DESIGN.md |
Class diagram, patterns, data flow |
| OpenAPI 3.0 | docs/openapi.yaml |
Import into Swagger Editor, Swagger UI, or code generators |
| Postman | docs/Hintro.postman_collection.json |
Import into Postman via File β Import |
View OpenAPI in Swagger UI: Open editor.swagger.io and paste the contents of docs/openapi.yaml, or use File β Import file.
Health check for all dependencies.
curl http://localhost:8080/healthResponse 200 OK:
{
"status": "ok",
"services": {
"postgres": "healthy",
"redis": "healthy"
}
}Find a compatible existing trip for a pending ride request.
curl -X POST http://localhost:8080/api/v1/match/2Response 200 OK β Match found:
{
"trip_id": 1,
"cab_id": 1,
"added_detour_minutes": 0
}Response 404 β No match:
{
"error": "no_match",
"message": "No compatible trip found. A new trip should be created."
}| Status | Meaning |
|---|---|
200 |
Match found |
400 |
Invalid request_id |
404 |
Request not found / no match |
409 |
Request already matched |
Book a ride β finds a match (or creates a new trip) and reserves the seat atomically.
curl -X POST http://localhost:8080/api/v1/book/2Response 200 OK:
{
"trip_id": 1,
"cab_id": 1,
"request_id": 2,
"seats_booked": 1,
"remaining_seats": 2,
"luggage_booked": 1,
"remaining_luggage": 2
}Luggage constraints: Both seats and luggage are enforced. A request with 3 bags will only match/book cabs with β₯3 luggage capacity. luggage_count (0β8 per request) and luggage_capacity (0β10 per cab) are validated at creation and enforced in matching/booking.
| Status | Meaning |
|---|---|
200 |
Booking successful |
400 |
Invalid request_id |
404 |
Request not found / no cab nearby |
408 |
Timeout (lock contention) |
409 |
Request not in pending state |
422 |
Cab full / cab unavailable |
Cancel a ride request (real-time cancellations).
curl -X POST http://localhost:8080/api/v1/cancel/2Response 200 OK β PENDING request cancelled:
{
"request_id": 2
}Response 200 OK β MATCHED request cancelled (freed capacity):
{
"request_id": 2,
"previous_trip_id": 1,
"trip_cancelled": true,
"cab_freed": true
}State transitions:
- PENDING β CANCELLED: Request removed from matching pool. No trip/cab impact.
- MATCHED β CANCELLED: Trip passenger count decremented; trip cleared if last passenger; cab set back to available.
| Status | Meaning |
|---|---|
200 |
Cancellation successful |
400 |
Invalid request_id |
404 |
Ride request not found |
409 |
Already cancelled or in non-cancellable state (confirmed/completed) |
Calculate the fare with dynamic surge pricing.
curl -X POST http://localhost:8080/api/v1/fare/estimate \
-H "Content-Type: application/json" \
-d '{
"origin_lat": 28.7041,
"origin_lon": 77.1025,
"dest_lat": 28.5562,
"dest_lon": 77.0889
}'Response 200 OK:
{
"base_fare_cents": 5000,
"distance_fare_cents": 19799,
"time_fare_cents": 6600,
"subtotal_cents": 31399,
"surge_multiplier": 1.5,
"total_fare_cents": 47099,
"distance_km": 16.5,
"estimated_minutes": 33,
"demand": 6,
"supply": 2,
"demand_supply_ratio": 3.0
}Pricing Formula:
Price = (BaseFare + Distance Γ PerKmRate + Time Γ PerMinRate) Γ SurgeMultiplier
Surge Tiers:
| Demand/Supply Ratio | Multiplier |
|---|---|
| R β€ 1.5 | 1.0Γ (normal) |
| R > 1.5 | 1.2Γ (moderate) |
| R > 2.0 | 1.5Γ (high) |
| Component | Choice | Assumption |
|---|---|---|
| Language | Go 1.22 | Single binary, good concurrency |
| Database | PostgreSQL 16 + PostGIS 3.4 | Spatial indexing for proximity |
| Cache | Redis 7 | Surge pricing demand/supply cache |
| Container | Docker + Compose | Local dev and deployment |
| Router | Gorilla Mux | Simple HTTP routing |
Assumptions:
- Passengers go to/from a single airport; direction is
to_airportorfrom_airport - Haversine for distance/time (no OSRM/Maps API); 30 km/h average speed
- Greedy matching suffices (no optimal TSP); 4β6 passengers per trip
- Pessimistic locking preferred over optimistic for booking correctness
- Surge cache 30s TTL acceptable; graceful fallback to PostGIS if Redis down
The core challenge is spatial β "find nearby passengers going to the airport." PostGIS provides:
GEOMETRY(Point, 4326)β stores GPS coordinates in the WGS-84 standardST_DWithin()β finds points within a real-world distance (meters, not degrees)- GIST Indexes β spatial tree indexes that turn O(N) full-table scans into O(log N) lookups
Without PostGIS, finding "all pending requests within 2km" would require scanning every row and computing distance in application code. With GIST indexes, PostgreSQL does this in <1ms even with millions of rows.
The critical scenario: two users book the last seat at the exact same millisecond.
We use PostgreSQL's SELECT ... FOR UPDATE inside a ReadCommitted transaction:
User A: BEGIN β SELECT cab FOR UPDATE β (row LOCKED)
User B: BEGIN β SELECT cab FOR UPDATE β β³ BLOCKS (waiting)
User A: seats OK β UPDATE β COMMIT β lock released
User B: (unblocked) β re-reads β NO SEATS β ROLLBACK β 422 error
Why not Optimistic Locking? Optimistic locking (version columns + retry loops) adds application complexity and can cause retry storms under high contention. Pessimistic locking is simpler, deterministic, and PostgreSQL handles the queuing natively.
Timeout safety: A 5-second context deadline prevents deadlock starvation β if a lock wait exceeds this, the transaction aborts with a 408 Timeout error.
Demand/supply counts change rapidly. Querying PostGIS on every fare estimate would add ~5ms of latency. Redis provides:
- <1ms lookups for cached demand/supply counts
- 30-second TTL β stale data is acceptable for surge (it's an estimate)
- Graceful degradation β if Redis is down, the service falls back to PostGIS directly
Total per request: O(log N + C Γ SΒ²)
| Component | Complexity | Explanation |
|---|---|---|
| PostGIS fetch | O(log N) | GIST index scan on ride_requests(origin) |
| Candidate loop | O(C) | C β€ 20 candidates (capped by LIMIT) |
| Insertion scoring | O(SΒ²) | S β€ 6 stops per trip (try each insertion point) |
| Haversine distance | O(1) | Constant-time trigonometry |
In practice: With C=20 and S=6, the inner loop executes 720 Haversine calculations β microseconds in Go. The GIST index handles millions of records. Total latency: <5ms per request, well within the 300ms constraint.
The Travelling Salesman Problem is NP-hard. For airport pooling, the greedy heuristic works because:
- One endpoint is fixed (the airport) β this isn't general VRP
- Trips have β€ 4-6 passengers β the solution space is tiny
- Detour tolerance acts as a hard filter β bad candidates are pruned early
- The difference between greedy and optimal for 4-6 stops is negligible
| Table | Purpose | PostGIS Columns |
|---|---|---|
users |
Passengers and drivers | β |
cabs |
Vehicles with capacity | current_location (Point) |
ride_requests |
Pickup/dropoff requests | origin, destination (Point) |
trips |
Grouped rides | route_path (LineString) |
| Index | Type | Purpose |
|---|---|---|
idx_ride_requests_origin_gist |
GIST | Core spatial matching query |
idx_ride_requests_status_created |
B-tree | FIFO queue for pending requests |
idx_cabs_location_gist |
GIST | Find nearest available cab |
idx_cabs_status_created |
B-tree | Available cab lookup |
Seed test data:
# Load test data (Delhi locations around IGI Airport)
Get-Content migrations/test_seed.sql -Raw | docker exec -i hintro-postgres psql -U hintro -d hintro_db
# Test matching
curl -X POST http://localhost:8080/api/v1/match/2
# Test booking
curl -X POST http://localhost:8080/api/v1/book/2
# Test fare estimate (Connaught Place β IGI Airport)
curl -X POST http://localhost:8080/api/v1/fare/estimate \
-H "Content-Type: application/json" \
-d '{"origin_lat":28.7041,"origin_lon":77.1025,"dest_lat":28.5562,"dest_lon":77.0889}'
# Test cancellation (request 2 must exist and be pending or matched)
curl -X POST http://localhost:8080/api/v1/cancel/2Concurrency race test seed: migrations/test_concurrency_seed.sql
# 1. Go unit tests (no server needed)
go test ./...
# 2. Ensure the system is running (use --build after code changes)
docker-compose up --build -d
# 3. Run integration tests (functional, match, cancel, fare, race, latency)
pip install requests # if not installed
python test_suite.pyTest coverage:
go test ./...β geo (Haversine, route, insertion), modelpython test_suite.pyβ health, book, match, cancel, fare, race condition, P95 latency
This project was built as a backend systems design assignment.