Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building a Scalable Food Delivery and Errand Management Platform

Tech Apr 23 10

Building a Scalable Food Delivery and Errand Management Platform

In today's fast-paced digital landscape, food delivery and errand management systems have become essential infrastructure for urban living. Creating a user-friendly platform requires careful consideration of both technical architecture and user experience. This article explores the technical implementation of a robust delivery management system, focusing on microservices architecture, responsive front-ends, and scalable back-end services.

Requirements Analysis and System Architecture

Functional Requirements

A comprehensive delivery platform must address the needs of multiple stakeholders:

  • Customer Application: User authentication, menu browsing, order placement, payment processing, real-time order tracking, and rating system
  • Merchant Dashboard: Inventory management, order fulfillment, promotional campaign tools, and revenue analytics
  • Delivery Personnel App: Order assignment, navigation assistance, status updates, and performance metrics
  • Admin Console: User management, merchant onboarding, order oversight, and platform analytics

Architectural Considerations

The platform employs a microservices architecture to ensure scalability, maintainability, and independent deployment of components. Key services include:

  • User Authentication Service
  • Menu and Catalog Service
  • Order Processing Service
  • Payment Gateway Integration
  • Real-time Notification Service
  • Analytics and Reporting Service

Services communicate through RESTful APIs and asynchronous message queues, enabling loose coupling while maintaining data consistency.

Frontend Implementation

Customer Application with React

The customer-facing application is built using React with Redux for state management. Below is a simplified component for menu browsing and cart management:

import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { addItemToCart } from '../actions/cartActions';

const RestaurantMenu = ({ restaurantId, addItemToCart }) => {
    const [menuItems, setMenuItems] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        const fetchMenu = async () => {
            try {
                const response = await fetch(`/api/restaurants/${restaurantId}/menu`);
                const data = await response.json();
                setMenuItems(data);
                setLoading(false);
            } catch (error) {
                console.error('Error fetching menu:', error);
                setLoading(false);
            }
        };

        fetchMenu();
    }, [restaurantId]);

    if (loading) return <div>Loading menu...</div>;

    return (
        <div classname="restaurant-menu">
            <h2>Restaurant Menu</h2>
            <div classname="menu-grid">
                {menuItems.map(item => (
                    <div classname="menu-item" key="{item.id}">
                        <img alt="{item.name}" classname="menu-item-image" src="{item.imageUrl}"></img>
                        <div classname="menu-item-details">
                            <h3>{item.name}</h3>
                            <p classname="menu-item-description">{item.description}</p>
                            <div classname="menu-item-footer">
                                <span classname="menu-item-price">${item.price.toFixed(2)}</span>
                                <button classname="add-to-cart-btn" onclick="{()"> addItemToCart(item)}
                                >
                                    Add to Cart
                                </button>
                            </div>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    );
};

const mapStateToProps = (state) => ({
    cartItems: state.cart.items
});

export default connect(mapStateToProps, { addItemToCart })(RestaurantMenu);

Merchant Dashboard with Vue.js

The merchant interface is developed using Vue.js with Vuex for state management. This example demonstrates a simplified menu management component:

<template>
  <div class="menu-management">
    <h1>Menu Management</h1>
    
    <div class="add-item-form">
      <h2>Add New Menu Item</h2>
      <form @submit.prevent="createMenuItem">
        <div class="form-group">
          <label for="item-name">Item Name</label>
          <input 
            type="text" 
            id="item-name" 
            v-model="newItem.name" 
            required
            placeholder="Enter item name"
          >
        </div>
        
        <div class="form-group">
          <label for="item-price">Price ($)</label>
          <input 
            type="number" 
            id="item-price" 
            v-model.number="newItem.price" 
            required
            min="0"
            step="0.01"
            placeholder="0.00"
          >
        </div>
        
        <div class="form-group">
          <label for="item-category">Category</label>
          <select id="item-category" v-model="newItem.category" required>
            <option value="">Select a category</option>
            <option v-for="category in categories" :key="category" :value="category">
              {{ category }}
            </option>
          </select>
        </div>
        
        <div class="form-group">
          <label for="item-description">Description</label>
          <textarea 
            id="item-description" 
            v-model="newItem.description" 
            required
            placeholder="Enter item description"
          ></textarea>
        </div>
        
        <button type="submit" class="submit-btn">Add Item</button>
      </form>
    </div>
    
    <div class="menu-items-list">
      <h2>Current Menu Items</h2>
      <div v-if="menuItems.length === 0" class="no-items">
        No menu items available
      </div>
      <div v-else class="menu-items-grid">
        <div v-for="item in menuItems" :key="item.id" class="menu-item-card">
          <div class="item-header">
            <h3>{{ item.name }}</h3>
            <span class="item-price">${{ item.price.toFixed(2) }}</span>
          </div>
          <p class="item-category">{{ item.category }}</p>
          <p class="item-description">{{ item.description }}</p>
          <div class="item-actions">
            <button @click="editItem(item)" class="edit-btn">Edit</button>
            <button @click="removeItem(item.id)" class="delete-btn">Delete</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'MenuManagement',
  data() {
    return {
      newItem: {
        name: '',
        price: 0,
        category: '',
        description: ''
      },
      categories: ['Appetizers', 'Main Courses', 'Desserts', 'Beverages'],
      menuItems: []
    };
  },
  methods: {
    async fetchMenu() {
      try {
        const response = await fetch(`/api/merchant/menu`);
        const data = await response.json();
        this.menuItems = data;
      } catch (error) {
        console.error('Error fetching menu:', error);
      }
    },
    async createMenuItem() {
      try {
        const response = await fetch('/api/merchant/menu', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(this.newItem)
        });
        
        if (response.ok) {
          this.newItem = {
            name: '',
            price: 0,
            category: '',
            description: ''
          };
          await this.fetchMenu();
        } else {
          console.error('Error creating menu item');
        }
      } catch (error) {
        console.error('Error creating menu item:', error);
      }
    },
    async removeItem(itemId) {
      if (confirm('Are you sure you want to delete this menu item?')) {
        try {
          const response = await fetch(`/api/merchant/menu/${itemId}`, {
            method: 'DELETE'
          });
          
          if (response.ok) {
            await this.fetchMenu();
          } else {
            console.error('Error deleting menu item');
          }
        } catch (error) {
          console.error('Error deleting menu item:', error);
        }
      }
    }
  },
  mounted() {
    this.fetchMenu();
  }
};
</script>

Backend Services Implementation

Authentication Service with Node.js and Express

The authentication service handles user registration, login, and token validation. It uses JWT for session management and bcrypt for password hashing:

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { User } = require('../models');

const router = express.Router();
router.use(bodyParser.json());
router.use(cors());

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const SALT_ROUNDS = 10;

// User registration endpoint
router.post('/register', async (req, res) => {
    try {
        const { username, email, password, role } = req.body;
        
        // Check if user already exists
        const existingUser = await User.findOne({ 
            $or: [{ email }, { username }] 
        });
        
        if (existingUser) {
            return res.status(409).json({ 
                message: 'User with this email or username already exists' 
            });
        }
        
        // Hash password
        const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
        
        // Create new user
        const newUser = new User({
            username,
            email,
            password: hashedPassword,
            role: role || 'customer'
        });
        
        await newUser.save();
        
        // Generate JWT
        const token = jwt.sign(
            { userId: newUser._id, role: newUser.role }, 
            JWT_SECRET, 
            { expiresIn: '24h' }
        );
        
        res.status(201).json({
            message: 'User registered successfully',
            token,
            user: {
                id: newUser._id,
                username: newUser.username,
                email: newUser.email,
                role: newUser.role
            }
        });
    } catch (error) {
        console.error('Registration error:', error);
        res.status(500).json({ message: 'Internal server error' });
    }
});

// User login endpoint
router.post('/login', async (req, res) => {
    try {
        const { username, password } = req.body;
        
        // Find user by username or email
        const user = await User.findOne({ 
            $or: [{ username }, { email: username }] 
        });
        
        if (!user) {
            return res.status(401).json({ 
                message: 'Invalid credentials' 
            });
        }
        
        // Check password
        const isPasswordValid = await bcrypt.compare(password, user.password);
        
        if (!isPasswordValid) {
            return res.status(401).json({ 
                message: 'Invalid credentials' 
            });
        }
        
        // Generate JWT
        const token = jwt.sign(
            { userId: user._id, role: user.role }, 
            JWT_SECRET, 
            { expiresIn: '24h' }
        );
        
        res.json({
            message: 'Login successful',
            token,
            user: {
                id: user._id,
                username: user.username,
                email: user.email,
                role: user.role
            }
        });
    } catch (error) {
        console.error('Login error:', error);
        res.status(500).json({ message: 'Internal server error' });
    }
});

// Middleware to authenticate JWT
const authenticateToken = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    
    if (!token) {
        return res.status(401).json({ message: 'Authentication token required' });
    }
    
    jwt.verify(token, JWT_SECRET, (err, user) => {
        if (err) {
            return res.status(403).json({ message: 'Invalid or expired token' });
        }
        
        req.user = user;
        next();
    });
};

// Protected route example
router.get('/profile', authenticateToken, async (req, res) => {
    try {
        const user = await User.findById(req.user.userId).select('-password');
        res.json(user);
    } catch (error) {
        console.error('Error fetching user profile:', error);
        res.status(500).json({ message: 'Internal server error' });
    }
});

module.exports = router;

Order Processing Service with Spring Boot

The order service manages order creation, updates, and status tracking. It integrates with other services for payment and notifications:

package com.delivery.platform.service;

import com.delivery.platform.model.Order;
import com.delivery.platform.model.OrderStatus;
import com.delivery.platform.repository.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Service
@Transactional
public class OrderService {

    private final OrderRepository orderRepository;
    private final NotificationService notificationService;
    private final PaymentService paymentService;

    @Autowired
    public OrderService(OrderRepository orderRepository, 
                       NotificationService notificationService,
                       PaymentService paymentService) {
        this.orderRepository = orderRepository;
        this.notificationService = notificationService;
        this.paymentService = paymentService;
    }

    /**
     * Creates a new order
     * @param order The order to create
     * @return The created order
     */
    public Order createOrder(Order order) {
        // Set initial order status and timestamps
        order.setStatus(OrderStatus.PENDING);
        order.setCreatedAt(LocalDateTime.now());
        order.setUpdatedAt(LocalDateTime.now());
        
        // Save the order
        Order savedOrder = orderRepository.save(order);
        
        // Send notification to restaurant
        notificationService.sendNotification(
            savedOrder.getRestaurantId(),
            "New order received: #" + savedOrder.getId()
        );
        
        return savedOrder;
    }

    /**
     * Updates an existing order
     * @param orderId The ID of the order to update
     * @param order The updated order data
     * @return The updated order
     */
    public Order updateOrder(Long orderId, Order order) {
        Optional<order> existingOrder = orderRepository.findById(orderId);
        
        if (existingOrder.isPresent()) {
            Order orderToUpdate = existingOrder.get();
            
            // Update fields
            orderToUpdate.setCustomerAddress(order.getCustomerAddress());
            orderToUpdate.setItems(order.getItems());
            orderToUpdate.setTotalAmount(order.getTotalAmount());
            orderToUpdate.setSpecialInstructions(order.getSpecialInstructions());
            orderToUpdate.setStatus(order.getStatus());
            orderToUpdate.setUpdatedAt(LocalDateTime.now());
            
            // Save the updated order
            Order updatedOrder = orderRepository.save(orderToUpdate);
            
            // Send notification about order update
            notificationService.sendOrderUpdateNotification(
                updatedOrder.getCustomerId(),
                updatedOrder.getId(),
                updatedOrder.getStatus()
            );
            
            return updatedOrder;
        } else {
            throw new RuntimeException("Order not found with ID: " + orderId);
        }
    }

    /**
     * Updates the status of an order
     * @param orderId The ID of the order to update
     * @param status The new status
     * @return The updated order
     */
    public Order updateOrderStatus(Long orderId, OrderStatus status) {
        Optional<order> order = orderRepository.findById(orderId);
        
        if (order.isPresent()) {
            Order orderToUpdate = order.get();
            orderToUpdate.setStatus(status);
            orderToUpdate.setUpdatedAt(LocalDateTime.now());
            
            // Save the updated order
            Order updatedOrder = orderRepository.save(orderToUpdate);
            
            // Send notification about status change
            notificationService.sendOrderUpdateNotification(
                updatedOrder.getCustomerId(),
                updatedOrder.getId(),
                status
            );
            
            // If order is confirmed, process payment
            if (status == OrderStatus.CONFIRMED) {
                paymentService.processPayment(updatedOrder);
            }
            
            return updatedOrder;
        } else {
            throw new RuntimeException("Order not found with ID: " + orderId);
        }
    }

    /**
     * Retrieves an order by ID
     * @param orderId The ID of the order to retrieve
     * @return The order
     */
    public Order getOrderById(Long orderId) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found with ID: " + orderId));
    }

    /**
     * Cancels an order
     * @param orderId The ID of the order to cancel
     * @return The cancelled order
     */
    public Order cancelOrder(Long orderId) {
        Optional<order> order = orderRepository.findById(orderId);
        
        if (order.isPresent()) {
            Order orderToCancel = order.get();
            
            // Check if order can be cancelled (not already in preparation or delivery)
            if (orderToCancel.getStatus() == OrderStatus.PENDING || 
                orderToCancel.getStatus() == OrderStatus.CONFIRMED) {
                
                orderToCancel.setStatus(OrderStatus.CANCELLED);
                orderToCancel.setUpdatedAt(LocalDateTime.now());
                
                // Save the cancelled order
                Order cancelledOrder = orderRepository.save(orderToCancel);
                
                // Process refund if payment was made
                if (cancelledOrder.getStatus() == OrderStatus.CONFIRMED) {
                    paymentService.processRefund(cancelledOrder);
                }
                
                // Send notification about cancellation
                notificationService.sendOrderUpdateNotification(
                    cancelledOrder.getCustomerId(),
                    cancelledOrder.getId(),
                    OrderStatus.CANCELLED
                );
                
                return cancelledOrder;
            } else {
                throw new RuntimeException("Order cannot be cancelled at this stage");
            }
        } else {
            throw new RuntimeException("Order not found with ID: " + orderId);
        }
    }
}
</order></order></order>

Testing and Deployment Strategy

Testing Approach

A comprehensive testing strategy ensures the platform's reliability and performance:

  • Unit Testing: Individual components are tested in isolation using frameworks like JUnit for Java and Jest for JavaScript.
  • Integration Testing: Verifies that different services interact correctly through API endpoints and message queues.
  • End-to-End Testing: Simulates complete user workflows across all system components.
  • Performance Testing: Evaluates system behavior under load using tools like JMeter and Gatling.
  • Security Testing: Identifies vulnerabilities through penetration testing and automated security scanning.

Containerization and Deployment

The platform is deployed using containerization technologies:

# Dockerfile for Node.js service
FROM node:16-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

# Dockerfile for Spring Boot service
FROM openjdk:16-jdk-alpine

WORKDIR /app

COPY target/delivery-platform-service.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Containers are orchestrated using Kubernetes, enabling auto-scaling, load balancing, and service discovery. The CI/CD pipeline automates testing and deployment using Jenkins or GitLab CI, ensuring rapid and reliable releases.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.