food-delivery

Food Delivery Platform

Restaurant & Customer Order Management

ReactTypeScriptJavaScriptMaterial-UINode.jsRuby on Rails
3
Portals Built
Rails → Node
Migration
Real-time
Order Tracking

The Problem

A food delivery platform needed three interconnected applications: a customer app for ordering food, a vendor portal for restaurants to manage menus and orders, and a backend API to orchestrate everything. The technical twist: the existing backend was built in Ruby on Rails with production authentication bugs, requiring a mid-project migration to Node.js while maintaining zero downtime for live orders.

What Made It Hard

Ruby on Rails to Node.js Migration

The existing Rails backend had:

  • OTP authentication endpoints with race conditions
  • Session management causing intermittent login failures
  • Email verification flows breaking under load
  • Production incidents every week

Challenge: Migrate authentication/OTP endpoints to Node.js without breaking existing features or requiring frontend changes.

Migration Constraints:

  • Frontend already deployed (can't change API contracts)
  • Database schema can't change (shared with Rails)
  • Must maintain session compatibility
  • Zero downtime requirement

Location-Based Ordering with Google Maps

Customers order from restaurants based on proximity:

  • User enters delivery address
  • System shows only restaurants within delivery radius
  • Distance calculations for delivery fees
  • Restaurant search with map view
  • Geolocation for "near me" searches

Complexity:

  • Haversine distance calculations
  • Radius filtering in real-time
  • Handling inaccurate GPS coordinates
  • Displaying 100+ restaurants on map without lag

Discount Scheduling Engine

Restaurants needed time-based discount automation:

  • "20% off Monday-Friday 2-4 PM"
  • "Buy 2 get 1 free on weekends"
  • "Free delivery after 10 PM"
  • Promo codes with usage limits

Challenge: Building a scheduling system that evaluates discounts in real-time as user adds items to cart, respecting timezone, date ranges, and business rules.

My Solution

Rails-to-Node Migration Strategy

Implemented strangler fig pattern (incremental replacement):

Phase 1: Identify Problem Endpoints

  • Audited production logs
  • Found race conditions in OTP generation/verification
  • Session token issues causing 401 errors

Phase 2: Build Node.js Equivalent

// New Node.js endpoint
app.post('/api/auth/send-otp', async (req, res) => {
  const { phone } = req.body;
  
  // Generate OTP with atomic transaction
  const otp = await db.transaction(async (t) => {
    // Invalidate previous OTPs
    await OTP.update(
      { status: 'expired' },
      { where: { phone, status: 'active' }, transaction: t }
    );
    
    // Create new OTP
    return await OTP.create({
      phone,
      code: generateOTP(),
      expiresAt: Date.now() + 5 * 60 * 1000,
      status: 'active'
    }, { transaction: t });
  });
  
  // Send SMS
  await sendSMS(phone, otp.code);
  res.json({ success: true });
});

Phase 3: Dual-Run Testing

  • Node endpoints ran in parallel with Rails
  • Logged differences in responses
  • Fixed edge cases before cutover

Phase 4: Traffic Switchover

  • Nginx routing rules to direct auth traffic to Node
  • Rails endpoints remained for other features
  • Monitored error rates (dropped by 95%)

Result: Zero production incidents after migration, authentication bugs resolved.

Location-Based Restaurant Discovery

Built geolocation-powered ordering system:

1. Address Input with Autocomplete

  • Google Places API for address suggestions
  • Geocoding to get lat/lng coordinates
  • User can also drag map pin for precision

2. Radius-Based Filtering

function getRestaurantsInRadius(
  userLat: number,
  userLng: number,
  radiusKm: number = 5
): Restaurant[] {
  // Haversine formula for distance
  const restaurants = await db.query(`
    SELECT *, 
      (6371 * acos(
        cos(radians(:userLat)) * cos(radians(latitude)) *
        cos(radians(longitude) - radians(:userLng)) +
        sin(radians(:userLat)) * sin(radians(latitude))
      )) AS distance
    FROM restaurants
    HAVING distance < :radiusKm
    ORDER BY distance ASC
  `, { userLat, userLng, radiusKm });
  
  return restaurants;
}

3. Map View with Clustering

  • Google Maps integration
  • Cluster markers when zoomed out (performance)
  • Click marker to see restaurant details
  • Smooth animations for pin placement

4. Delivery Fee Calculation

  • Based on distance from restaurant
  • Dynamic pricing (0-3km = free, 3-5km = $2, 5km+ = $4)
  • Shown before checkout

Discount Scheduling Engine

Built rule-based discount system:

interface DiscountRule {
  name: string;
  type: 'percentage' | 'fixed' | 'bogo';
  value: number;
  schedule: {
    daysOfWeek: number[];      // [1,2,3,4,5] = Mon-Fri
    startTime: string;          // "14:00"
    endTime: string;            // "16:00"
  };
  conditions: {
    minOrderValue?: number;
    maxUsagePerUser?: number;
    validUntil?: Date;
  };
}

function evaluateDiscounts(
  cart: CartItem[],
  user: User,
  restaurant: Restaurant,
  orderTime: Date
): Discount[] {
  const applicableDiscounts = restaurant.discounts.filter(discount => {
    // Check day of week
    const dayOfWeek = orderTime.getDay();
    if (!discount.schedule.daysOfWeek.includes(dayOfWeek)) {
      return false;
    }
    
    // Check time range
    const currentTime = orderTime.getHours() * 60 + orderTime.getMinutes();
    const [startH, startM] = discount.schedule.startTime.split(':').map(Number);
    const [endH, endM] = discount.schedule.endTime.split(':').map(Number);
    const startMinutes = startH * 60 + startM;
    const endMinutes = endH * 60 + endM;
    
    if (currentTime < startMinutes || currentTime > endMinutes) {
      return false;
    }
    
    // Check min order value
    const orderTotal = cart.reduce((sum, item) => sum + item.price, 0);
    if (discount.conditions.minOrderValue && orderTotal < discount.conditions.minOrderValue) {
      return false;
    }
    
    // Check usage limit
    if (discount.conditions.maxUsagePerUser) {
      const usageCount = await getUserDiscountUsage(user.id, discount.id);
      if (usageCount >= discount.conditions.maxUsagePerUser) {
        return false;
      }
    }
    
    return true;
  });
  
  return applicableDiscounts;
}

UI Features:

  • Real-time discount badge on cart ("20% off applied!")
  • Strikethrough original price
  • Countdown timer for time-limited discounts
  • Promo code input with validation

Branch & Menu Management for Vendors

Built vendor portal with:

  • Branch Management: Multiple locations per restaurant
  • Menu CRUD: Categories, items, modifiers (extra cheese, no onions)
  • Availability Toggles: Mark items out of stock
  • Pricing: Base price + modifier pricing
  • Image Uploads: Item photos with preview

Challenge: Keeping menus in sync across branches

  • Solution: Template system (copy menu to new branch)
  • Branch-specific overrides (price adjustments)

Technical Decisions

Why Node.js over Keeping Rails?

  • Team expertise (JavaScript full-stack)
  • Better concurrency for real-time features
  • Shared language with frontend (code reuse)
  • Rails OTP race conditions couldn't be fixed (async issues)

Why Material-UI over Custom Components?

  • Faster development (pre-built components)
  • Consistent design language
  • Accessibility out-of-the-box
  • Customizable theming

Why Google Maps over Mapbox?

  • Better Places API (address autocomplete)
  • Familiar UX for customers
  • Accurate distance calculations
  • Client already had Google Cloud account

Why Real-Time Polling over WebSockets?

  • Simpler implementation
  • Better for mobile (WebSocket drops on network switch)
  • 10-second polling acceptable for order tracking
  • Less server overhead (no persistent connections)

Impact

Business Metrics

  • 3 portals delivered (customer, vendor, backend)
  • 95% reduction in authentication errors post-migration
  • Location-based ordering enabled for 100+ restaurants
  • Real-time discount engine increased promotion usage by 40%

Technical Achievements

  • Zero-downtime migration from Rails to Node.js
  • Sub-500ms geolocation queries with Haversine
  • Google Maps integration with clustering for performance
  • Discount evaluation runs in under 50ms per cart update

Developer Experience

  • Consolidated codebase (JavaScript frontend + backend)
  • Reusable geolocation utilities across projects
  • Automated tests prevent discount calculation bugs
  • Vendor portal reduces customer support tickets by 30%

What I'd Do Differently

Complete Rails Migration

Would migrate entire backend to Node.js:

  • Current hybrid (Node for auth, Rails for orders) causes deployment complexity
  • Maintaining two backends doubles infrastructure cost
  • Team context-switching between languages
  • Should have allocated time for full migration

PostGIS for Geolocation

Would use PostgreSQL PostGIS extension:

  • Current Haversine in application code is slow
  • PostGIS indexes spatial queries (10x faster)
  • Built-in radius queries and polygon support
  • Better for map-heavy features

Redis for Discount Caching

Would cache discount evaluations in Redis:

  • Current approach recalculates on every cart update
  • Same discounts evaluated repeatedly
  • Cache with TTL = time until discount expires
  • Huge performance gain

Automated Migration Testing

Would implement shadow traffic testing:

  • Send production traffic to both Rails and Node
  • Compare responses automatically
  • Catch regressions before cutover
  • Current manual testing was time-consuming

Key Takeaways

  1. Strangler fig pattern works for incremental migrations - replace one feature at a time
  2. Location features need spatial databases - application-level distance calculations don't scale
  3. Time-based scheduling is hard - timezone, daylight saving, and edge cases multiply fast
  4. Vendor portals reduce support load - self-service beats manual menu updates
  5. Migration without downtime is possible - dual-run, compare, cutover, rollback plan