[PROJECT_CASE_STUDY_02]

We Built a Ride-Hailing Platform to Rival Uber and Ola — Before AI Made It Easy

87 hand-crafted data models. Real-time dispatch. Aadhaar KYC. GST invoicing. Bulk promo distribution. A fare engine that handles sub-region pricing, night surcharges, and rupee rounding — all from first principles.

SYSTEM_ARCHITECTURE_MAP Utoo System Architecture Diagram
KEY_IMPACT_METRICS
92% Reduction in booking failures
<4s Average driver match time
35% Increase in completed trips/day
4.7★ App store rating at launch

The Brief

At the peak of the Uber-vs-Ola war in India, a regional taxi operator needed more than a dispatch app — they needed a full platform: real-time booking, driver apps, passenger apps, a CRM for call-centre agents, fleet management dashboards, corporate billing, and a reporting engine that could tell them which driver performed best on a Tuesday night.

No AI tools. No LLMs generating boilerplate. No Copilot suggesting the next line. Just engineers who understood the domain deeply enough to model it correctly the first time.

The Scale of the Problem

India's cab market is unforgiving. Passengers expect sub-minute response times. Drivers need clear earnings breakdowns. Operators manage fleets of hundreds of vehicles across multiple cities. Regulators mandate Aadhaar-based KYC. Payment methods range from Paytm wallets to corporate credit accounts.

A single "booking" is actually a coordinated state machine touching multiple client interfaces, payment gateways, geographical boundaries, and background synchronization queues. Building this without scaffolding from AI is a statement about engineering craft.

What Was Built

We engineered a complete, production-grade ecosystem supporting multiple user personas and heavy operational loads.

PLATFORM_DIMENSIONS
Dimension Metric / Count
Ruby source files 690+ files
Lines of Ruby code 12,000+ lines
Data models 87 models
API controllers 39 controllers
Background workers 24 workers
External integrations 8 integrations
User roles 8 roles
MongoDB databases 3 isolated instances
Report types 15+ types

The system coordinates everything from direct consumer-facing apps to business analytics platforms:

  • Passenger-facing: Registration, instant and scheduled bookings, real-time driver tracking, referral structures, and native wallet systems.
  • Driver-facing: KYC document renewal pipelines, shift schedules, ratings, and detailed earnings summaries.
  • Fleet Operations: Yard check-in/check-out logs, fuel consumption, insurance monitoring, and driver-vehicle mapping.
  • Corporate Accounts: Custom pre-negotiated corporate rates, multi-user client portals, and monthly ledger balance sheets.
8_STATE_BOOKING_LIFECYCLE Utoo Booking Flow Diagram

The Broader Ecosystem

Beyond the core booking path, Utoo was built as an integrated suite of companion applications sharing the same backend database layers:

  • Driver Management System (DMS): Manages driver compliance, performance histories, and vehicle allocation.
  • Real World Simulator: Employs bots to model wait times, travel patterns, and supply-demand configurations under synthetic load before deployment to new cities.
  • WAR Room Dashboard: Push-first monitor displaying active positions, SLA limits, and fleet KPIs for control-center dispatchers.
  • Investor Portal: Strategic BI dashboard displaying city-wide revenue metrics and fleet utilization variables.

Quick Facts

Client: Utoo Cabs
Location: Chennai, India
Duration: 6 Months
Team: 5 Engineers

Tech Stack

Ruby on Rails React Native Node.js WebSockets MongoDB Redis PostGIS Sidekiq Aadhaar KYC Paytm & Freecharge Google Maps SDK AWS EC2

Technical Logs & Architecture

A review of deep engineering choices, custom workflows, and background scaling strategies implemented in the Utoo core backend.

database_decision.yml

Decoupled Three-Database Architecture

With 4,000 active drivers pinging coordinates every 3 seconds, the database absorbs 80,000 writes per minute. If location pings reside on the same database node as active booking queries, write locks quickly queue up and destroy read performance.

The Utoo architecture split database writes across three isolated MongoDB instances:

# mongoid.yml — isolated clients
clients:
  default:
    database: taximongo          # operational profiles (drivers, fares, promos)
  atlas:
    database: atlas              # booking records and lifecycle states
  tracker:
    database: tracker            # live telemetry, location history, and GPS pings

This partition keeps the core operational database clean and ensures sub-second query speeds on the dispatch CRM booking listings.

ENTITY_RELATIONSHIPS Utoo Data Model Map

Custom Calculated Fare Engine

Invoicing logic runs inside a background worker `BookingInvoice` after a ride has completed, avoiding any blockages in Web API transaction cycles.

# app/workers/booking_invoice.rb
split_details = {
  base_charge:      motor_model.base_fare,
  distance_fare:    [trip_km - motor_model.min_km, 0].max * motor_model.per_km_rate,
  time_fare:        trip_minutes * motor_model.per_minute_rate,
  additional_fare:  surge_applicable? ? motor_model.additional_fare : 0,
  discount:         calculate_promo_discount(passenger_offer, subtotal),
  tax:              (subtotal * region_tax.percentage / 100).round(2),
  wallet_deduction: [passenger.wallet_balance, net_payable].min,
  gross_amount:     subtotal,
  total_invoice:    (net_after_wallet).ceil   # ceiling round up to nearest rupee
}

Key features implemented here:

  • Minimum distance threshold: Distance calculations only apply after subtracting baseline minimum distances.
  • Predictable Surge: Surge charges are flat model overrides rather than floating multipliers, providing operations with absolute tariff controls.
  • Wallet Safeguards: Capping deductions to `net_payable` ensures balances cannot drop below zero.

Geofenced Boundary Fare Tables

Cities are mapped into polygon-bounded sub-regions using geographic coordinates. Pricing configurations map to `(sub_region, motor_model)` tuples:

Zone (e.g., Chennai Hub)
  ├── Sub-Region A (Airport Geofence)
  │     ├── Compact: Base ₹70, ₹9/km
  │     └── Sedan:   Base ₹95, ₹12/km
  └── Sub-Region B (Central Station Geofence)
        ├── Compact: Base ₹50, ₹8/km
        └── Sedan:   Base ₹75, ₹11/km

When a pickup is requested, coordinates automatically map to a defined `RegionBound` polygon, fetching the correct tariff rules without dispatcher error.

Sidekiq Job Prioritization

24 background workers process asynchronous operational tasks. Prioritization rules prevent bulk batches from blocking active booking lifecycles.

# config/sidekiq.yml
:concurrency: 20   # production
:queues:
  - [critical, 10]        # Booking path (invoices, SMS dispatch, matching)
  - [default, 10]         # Reporting and general email queues
  - [product_detail, 2]   # Throttled bulk writes (Promo CSV imports, migrations)

Restricting `product_detail` queues to a weight of 2 prevents massive DB lockouts during marketing campaign distributions.

Bulk Campaign Distribution

Marketing teams frequently upload lists containing 50,000 passenger contacts for promotions. The importer handles writes asynchronously:

# Importer runs in background, updating file status
def perform(upload_id)
  upload = FileUploadStatus.find(upload_id)
  offers = []
  sms_jobs = []

  CSV.foreach(upload.file_path, headers: true) do |row|
    passenger = Passenger.find_by(phone: row['phone'])
    next unless passenger

    offers << {
      passenger_id:   passenger.id,
      discount_type:   upload.discount_type,
      discount_value:  upload.value,
      valid_to:        upload.expiry_date,
      created_at:      Time.current,
      updated_at:      Time.current
    }
    sms_jobs << [passenger.phone, "Promo code active!"]
  end

  # Perform bulk operations in database and queue
  if offers.any?
    PassengerOffer.collection.insert_many(offers)
    SendSms.perform_bulk(sms_jobs) # Previously push_bulk
  end

  upload.update(status: 'completed')
end

Consolidating writes into a single bulk insert (`insert_many`) and enqueuing Sidekiq jobs in bulk (`perform_bulk`, previously `push_bulk`) avoids high database locking overhead and CPU spikes under heavy load.

Shift Time-Slice Performance Analytics

To analyze fleet metrics, driver schedules are divided into precise operational windows:

TIME_SLICES = {
  morning:   ['08:30', '11:30'],
  afternoon: ['11:30', '12:30'],
  evening:   ['20:30', '23:30'],
  night:     ['23:30', '00:30']  # Spans midnight boundary
}

Batching filters queries in groups of 100 profiles, preventing system OOM (Out Of Memory) states when crunched on production nodes:

total_drivers = Driver.where(active: true).count
(1..(total_drivers / 100.0).ceil).each do |page|
  drivers_batch = Driver.where(active: true).skip((page - 1) * 100).limit(100)
  process_batch_analytics(drivers_batch)
end

India-Market Integrations

Custom verification and payment gateways implemented with low-level security:

  • Aadhaar Biometric Bridge: OTP and fingerprint validation pipelines matching UIDAI government specifications.
  • Freecharge & Paytm Hashing: Secure SHA256 checksum signatures implemented manually without heavy SDK dependencies.
# SHA256 checksum calculation for payment validation
checksum_string = [
  merchant_id,
  transaction_id,
  amount,
  currency,
  payment_mode,
  ENV['freecharge_secret_key']
].join('|')

checksum = Digest::SHA256.hexdigest(checksum_string)

Let's Build Together

Need to build high-scale geospatial platforms, real-time engines, or reliable B2B ecosystems?