← Back to list

Java Scenario-Based Interview Question 4: Design Food Delivery System

In the fast-paced world of food delivery, customers demand flexibility, personalization, and reliability. Building a system that supports…

Raju Methuku · 2025-03-03 03:32 · 5 claps · 3.9 min read
#java-interview-questions #java-interview #code-kata #oops-concepts #java-best-practices
Open on Medium ↗
Wiki topics: CRM · Email & CRM 🍳 · Food & Cooking

Java Scenario-Based Interview Question 4: Design Food Delivery System

In the fast-paced world of food delivery, customers demand flexibility, personalization, and reliability. Building a system that supports multiple payment methods, dynamic discounts, and split payments while maintaining scalability and simplicity is no small feat. In this blog, we’ll explore how to design such a system using best practices and design patterns.

The Problem Statement

Imagine a food delivery application where customers can order food from restaurants. The system processes the order, applies discounts, and handles payments. However, the requirements are far from simple:

  1. Multiple Payment Methods: Customers can pay using wallets, credit cards, or cash. They can also split payments across these methods.
  2. Dynamic Discounts:
  • 10% discount on weekends.
  • Additional 5% discount for customers over 50 years old.
  • Another 5% discount if the order is placed on the customer’s birthday.
  1. Error Handling: The system must validate payment details and gracefully handle payment failures.

  2. Scalability: The solution must be modular, maintainable, and scalable.

The Approach

To tackle this problem, we’ll use a combination of design patterns and best practices:

  1. Strategy Pattern: To handle multiple payment methods.
  2. Decorator Pattern: To apply dynamic discounts.
  3. Factory Pattern: To create payment processors and discount decorators.

We’ll break the solution into three main components:

  1. Order Service: Manages the order lifecycle, including discount calculation and payment processing.
  2. Discount System: Applies dynamic discounts based on customer and order details.
  3. Payment Processor: Handles multiple payment methods and split payments.

Step-by-Step Solution

Let’s dive into the implementation of each component and explain how the code is production-grade.

1. Order Service

The DefaultOrderService is the entry point for placing an order. It is responsible for:

  • Calculating Discounts: Applies all applicable discounts to the order.
  • Validating Payments: Ensures that the split payment amounts match the total order amount.
  • Processing Payments: Delegates payment handling to the PaymentProcessor.

Key Features of Production-Grade Code:

  1. Separation of Concerns: The service delegates discount calculation and payment processing to specialized components, ensuring modularity.
  2. Error Handling: The service gracefully handles exceptions and provides meaningful error messages.
  3. Scalability: The design allows for easy addition of new discounts or payment methods without modifying existing code.

Here’s the implementation:

public class DefaultOrderService implements OrderService {
    private final List<PriceDecorator> discountDecorators;
    private final PaymentProcessor paymentProcessor;
    public DefaultOrderService(PaymentProcessor paymentProcessor, List<PriceDecorator> discountDecorators) {
        this.paymentProcessor = paymentProcessor;
        this.discountDecorators = discountDecorators;
    }
    @Override
    public DiscountedOrder placeOrder(Order order) {
        try {
            // Step 1: Calculate final price with discounts
            List<String> discountBreakdown = new ArrayList<>();
            Float discountedPrice = calculateFinalPrice(order.getTotalPrice(), order, LocalDate.now(), discountBreakdown);
            // Step 2: Validate payment details
            validatePaymentDetails(order.getPaymentDetails(), discountedPrice);
            // Step 3: Process payments
            paymentProcessor.processPayments(order.getPaymentDetails(), order.getCustomer(), discountedPrice);
            // Step 4: Return the discounted order
            return new DiscountedOrder(order, discountedPrice, discountBreakdown);
        } catch (Exception e) {
            throw new OrderProcessingException("Error processing order", e);
        }
    }
    private Float calculateFinalPrice(Float basePrice, Order order, LocalDate orderDate, List<String> discountBreakdown) {
        Float finalPrice = basePrice;
        for (PriceDecorator decorator : discountDecorators) {
            Float newPrice = decorator.calculatePrice(finalPrice, order.getCustomer(), orderDate);
            if (!newPrice.equals(finalPrice)) {
                discountBreakdown.add(decorator.getClass().getSimpleName() + " applied: " + (finalPrice - newPrice));
            }
            finalPrice = newPrice;
        }
        return finalPrice;
    }
    private void validatePaymentDetails(PaymentDetails paymentDetails, Float orderTotal) {
        if (!paymentDetails.getTotalAmount().equals(orderTotal)) {
            throw new OrderProcessingException("Split payment amounts do not match the order total.");
        }
    }
}

2. Discount System

The discount system uses the Decorator Pattern to apply dynamic discounts. Each discount is implemented as a PriceDecorator. This approach allows us to add or remove discounts without modifying the core logic.

Why the Decorator Pattern?

The Decorator Pattern allows us to dynamically add behavior (discounts) to an object (order price) without modifying its structure. Each discount is encapsulated in its own class, making the system modular and easy to extend.

Key Features of Production-Grade Code:

  1. Modularity: Each discount is implemented as a separate class, making it easy to add or remove discounts.
  2. Reusability: Discounts can be reused across different services or contexts.
  3. Extensibility: New discounts can be added without modifying existing code.

Implementation of Discounts

Weekend Discount:

class WeekendDiscount implements PriceDecorator {
    @Override
    public Float calculatePrice(Float basePrice, Customer customer, LocalDate orderDate) {
        if (orderDate.getDayOfWeek() == DayOfWeek.SATURDAY || orderDate.getDayOfWeek() == DayOfWeek.SUNDAY) {
            return basePrice * 0.9f; // 10% discount
        }
        return basePrice;
    }
}

Age Discount:

class AgeDiscount implements PriceDecorator {
    @Override
    public Float calculatePrice(Float basePrice, Customer customer, LocalDate orderDate) {
        if (customer.getAge() > 50) {
            return basePrice * 0.95f; // Additional 5% discount
        }
        return basePrice;
    }
}

Birthday Discount:

class BirthdayDiscount implements PriceDecorator {
    @Override
    public Float calculatePrice(Float basePrice, Customer customer, LocalDate orderDate) {
        if (customer.getBirthday().equals(orderDate)) {
            return basePrice * 0.95f; // Additional 5% discount
        }
        return basePrice;
    }
}

3. Payment Processor

The DefaultPaymentProcessor handles multiple payment methods and validates split payments. It ensures that the total split amount matches the order total and processes each payment method sequentially.

Key Features of Production-Grade Code:

  1. Validation: Ensures that the total split payment matches the order total before processing payments.
  2. Error Handling: Throws meaningful exceptions for invalid or failed payments.
  3. Extensibility: New payment methods can be added without modifying the existing processor.

Here’s the implementation:

public class DefaultPaymentProcessor implements PaymentProcessor {
    @Override
    public void processPayments(PaymentDetails paymentDetails, Customer customer, Float orderTotal) {
        if (!paymentDetails.getTotalAmount().equals(orderTotal)) {
            throw new PaymentProcessingException("Split payment amounts do not match the order total.");
        }
        List<PaymentMethod> methods = paymentDetails.getPaymentMethods();
        List<Float> amounts = paymentDetails.getAmounts();
        for (int i = 0; i < methods.size(); i++) {
            if (!methods.get(i).pay(amounts.get(i), customer)) {
                throw new PaymentProcessingException("Payment failed");
            }
        }
    }
}

You can find full code here: https://github.com/raju4789/code-katas/tree/main/src/main/java/com/raju/codekatas/orderingsystem

👋 Let’s Connect!

If you found this post insightful, here’s how you can help spread the knowledge:

👏 Clap if you enjoyed it — your claps motivate me to keep sharing more Java tricks and insights! 🔗 Share this post with your network so others can learn too. 💬 Ask your questions or share your experiences in the comments. Have you encountered this situation? Let’s discuss!

🚀 Follow me for more deep dives into Java, design patterns, and programming gotchas! Let’s learn and grow together. 🌟


메타데이터
post_id
bb4454abe599
slug
java-scenario-based-interview-question-4-design-food-delivery-system-bb4454abe599
url
https://medium.com/@narasimha4789/java-scenario-based-interview-question-4-design-food-delivery-system-bb4454abe599
canonical_url
https://medium.com/@narasimha4789/java-scenario-based-interview-question-4-design-food-delivery-system-bb4454abe599
author_url
https://medium.com/@narasimha4789
status
ok
fetched_at
2026-08-06 07:53:32