Building a Scalable Food Delivery and Errand Management Platform
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.