Skip to main content

Design a Coupon / Promo Code Validator Service

Problem Statement

Design an extensible Coupon / Promo Code Validator Service (similar to the promo codes engine inside Uber, Swiggy, or e-commerce platforms). The service must validate promo codes against cart criteria (like expiration, user eligibility, minimum order value), apply different pricing logic strategies (flat discount, percentage discount with a cap limit, or category-specific discounts), and calculate the final discounted cart totals.

Asked In Companies
Uber Swiggy Razorpay

Design Decisions & Patterns Used

Promo code validation logic changes frequently based on business requirements. For example, some campaigns require first-order-only checks, while others apply discounts only to specific product categories. To prevent rule evaluations from cluttering the pricing code, we separate the validation pipeline from the discount calculation logic.

We will utilize the following Design Patterns:

  • Strategy Pattern: Defining interchangeable algorithms (strategies) to calculate discount rates (e.g., Flat vs. Percentage vs. Category-Specific discount strategies).
  • Chain of Responsibility Pattern: Piping the cart through a chain of validation checks (e.g., Expiry check -> Min Order check -> User Eligibility check) that can fail-fast and reject the promo code.

Functional Requirements

  • Support different discount types: Flat discount, Percentage discount with maximum capping, and Category-Specific discount (applied only to matching items).
  • Validate promo codes against minimum order thresholds, coupon expiration, and user eligibility rules.
  • Support chaining validation checks dynamically.
  • Return the calculated discount amount and update the cart's final total.

Objects Required

  • CartItem & Cart (Models tracking products, pricing, and totals)
  • DiscountStrategy (Interface defining discount calculation contracts)
  • FlatDiscountStrategy, PercentageDiscountStrategy, CategoryDiscountStrategy (Concrete strategies)
  • ValidationHandler (Base class of the validation chain)
  • PromoCode (Aggregate container pairing discount strategies, eligibility rules, and metadata)

CartItem & Cart Classes

The CartItem and Cart classes store the products, price information, and final cart totals.


public class CartItem {
    private final String productId;
    private final String category;
    private final double price;
    private final int quantity;

    public CartItem(String productId, String category, double price, int quantity) {
        this.productId = productId;
        this.category = category;
        this.price = price;
        this.quantity = quantity;
    }

    public String getProductId() { return productId; }
    public String getCategory() { return category; }
    public double getPrice() { return price; }
    public int getQuantity() { return quantity; }
}

Let's define the Cart class:


import java.util.ArrayList;
import java.util.List;

public class Cart {
    private final String userId;
    private final List<CartItem> items;
    private final boolean isFirstOrder;

    public Cart(String userId, boolean isFirstOrder) {
        this.userId = userId;
        this.items = new ArrayList<>();
        this.isFirstOrder = isFirstOrder;
    }

    public void addItem(CartItem item) {
        items.add(item);
    }

    public double getTotalValue() {
        return items.stream().mapToDouble(item -> item.getPrice() * item.getQuantity()).sum();
    }

    public String getUserId() { return userId; }
    public List<CartItem> getItems() { return items; }
    public boolean isFirstOrder() { return isFirstOrder; }
}

The getTotalValue() method uses streams to calculate the sum of prices of all items in the cart.


DiscountStrategy Interface & Implementations

The DiscountStrategy interface determines how a discount is calculated. We use the **Strategy Pattern** to keep pricing calculations pluggable.


public interface DiscountStrategy {
    double calculateDiscount(Cart cart);
}

Let's implement the concrete pricing strategies:


public class FlatDiscountStrategy implements DiscountStrategy {
    private final double flatAmount;

    public FlatDiscountStrategy(double flatAmount) {
        this.flatAmount = flatAmount;
    }

    @Override
    public double calculateDiscount(Cart cart) {
        // Return flat amount, capping it to the total cart value to prevent negative totals
        return Math.min(flatAmount, cart.getTotalValue());
    }
}

The FlatDiscountStrategy subtracts a flat discount value, ensuring the discount does not exceed the total cart value.


public class PercentageDiscountStrategy implements DiscountStrategy {
    private final double percentage;
    private final double maxCapLimit;

    public PercentageDiscountStrategy(double percentage, double maxCapLimit) {
        this.percentage = percentage;
        this.maxCapLimit = maxCapLimit;
    }

    @Override
    public double calculateDiscount(Cart cart) {
        double calculated = cart.getTotalValue() * (percentage / 100.0);
        return Math.min(calculated, maxCapLimit); // Enforce maximum discount limit
    }
}

The PercentageDiscountStrategy calculates the percentage discount and caps it to the maximum limit (e.g., 10% off up to $50).


public class CategoryDiscountStrategy implements DiscountStrategy {
    private final String targetCategory;
    private final double percentage;

    public CategoryDiscountStrategy(String targetCategory, double percentage) {
        this.targetCategory = targetCategory;
        this.percentage = percentage;
    }

    @Override
    public double calculateDiscount(Cart cart) {
        double categoryTotal = cart.getItems().stream()
                .filter(item -> item.getCategory().equalsIgnoreCase(targetCategory))
                .mapToDouble(item -> item.getPrice() * item.getQuantity())
                .sum();
        return categoryTotal * (percentage / 100.0);
    }
}

The CategoryDiscountStrategy calculates the discount based only on the items in the cart that match the target category (e.g., Electronics).


Validation Chain (Chain of Responsibility)

We use the **Chain of Responsibility Pattern** to validate promo codes. Each validation check is represented as a handler in the chain.


public abstract class ValidationHandler {
    protected ValidationHandler next;

    public void setNext(ValidationHandler next) {
        this.next = next;
    }

    public abstract void validate(Cart cart, PromoCode promo);

    protected void checkNext(Cart cart, PromoCode promo) {
        if (next != null) {
            next.validate(cart, promo);
        }
    }
}

Let's implement concrete validation handlers: ExpiryValidator, MinOrderValidator, and EligibilityValidator.


public class ExpiryValidator extends ValidationHandler {
    @Override
    public void validate(Cart cart, PromoCode promo) {
        if (System.currentTimeMillis() > promo.getExpiryTime()) {
            throw new IllegalArgumentException("Promo code '" + promo.getCode() + "' has expired.");
        }
        checkNext(cart, promo);
    }
}

public class MinOrderValidator extends ValidationHandler {
    @Override
    public void validate(Cart cart, PromoCode promo) {
        if (cart.getTotalValue() < promo.getMinOrderValue()) {
            throw new IllegalArgumentException("Cart total $" + cart.getTotalValue() + 
                    " is less than the minimum order requirement $" + promo.getMinOrderValue());
        }
        checkNext(cart, promo);
    }
}

public class EligibilityValidator extends ValidationHandler {
    @Override
    public void validate(Cart cart, PromoCode promo) {
        if (promo.isFirstOrderOnly() && !cart.isFirstOrder()) {
            throw new IllegalArgumentException("Promo code '" + promo.getCode() + "' is valid for first-time orders only.");
        }
        checkNext(cart, promo);
    }
}

Each handler performs its specific validation check. If the check passes, it calls checkNext() to pass control to the next handler; if the check fails, it throws an exception to abort the validation process immediately.


PromoCode Class

The PromoCode class stores the metadata, validation criteria, and the discount strategy of the promo code.


public class PromoCode {
    private final String code;
    private final long expiryTime;
    private final double minOrderValue;
    private final boolean firstOrderOnly;
    private final DiscountStrategy discountStrategy;

    public PromoCode(String code, long expiryTime, double minOrderValue, boolean firstOrderOnly, DiscountStrategy discountStrategy) {
        this.code = code;
        this.expiryTime = expiryTime;
        this.minOrderValue = minOrderValue;
        this.firstOrderOnly = firstOrderOnly;
        this.discountStrategy = discountStrategy;
    }

    public String getCode() { return code; }
    public long getExpiryTime() { return expiryTime; }
    public double getMinOrderValue() { return minOrderValue; }
    public boolean isFirstOrderOnly() { return firstOrderOnly; }
    public DiscountStrategy getDiscountStrategy() { return discountStrategy; }
}

The constructor configures the validation criteria and discount strategy, which are evaluated by the validation handlers.


Main Driver Class

This class tests our promo code validation engine. It configures the validation chain, applies promo codes to carts, and verifies that invalid codes are rejected.


public class Main {
    public static void main(String[] args) {
        // Set up the validation chain
        ValidationHandler expiry = new ExpiryValidator();
        ValidationHandler minOrder = new MinOrderValidator();
        ValidationHandler eligibility = new EligibilityValidator();

        expiry.setNext(minOrder);
        minOrder.setNext(eligibility);

        // Define promo codes
        long futureTime = System.currentTimeMillis() + 100000;
        PromoCode flatPromo = new PromoCode("FLAT_50", futureTime, 100.0, false, new FlatDiscountStrategy(50.0));
        PromoCode firstOrderPromo = new PromoCode("WELCOME_10", futureTime, 0.0, true, new PercentageDiscountStrategy(10, 100));

        System.out.println("==========================================");
        System.out.println("Scenario 1: Applying Flat Discount to Cart");
        System.out.println("==========================================");

        Cart cart1 = new Cart("user-1", false);
        cart1.addItem(new CartItem("item-1", "Electronics", 80.0, 1));
        cart1.addItem(new CartItem("item-2", "Clothing", 40.0, 1)); // Total: $120.00

        try {
            // Run validation checks
            expiry.validate(cart1, flatPromo);
            double discount = flatPromo.getDiscountStrategy().calculateDiscount(cart1);
            System.out.println("Cart Total: $" + cart1.getTotalValue());
            System.out.println("Discount Applied: $" + discount);
            System.out.println("Final Cart Total: $" + (cart1.getTotalValue() - discount));
        } catch (Exception e) {
            System.err.println("Validation failed: " + e.getMessage());
        }

        System.out.println("\n==========================================");
        System.out.println("Scenario 2: Rejecting First-Order Promo Code");
        System.out.println("==========================================");

        Cart cart2 = new Cart("user-1", false); // Not a first-time order
        cart2.addItem(new CartItem("item-3", "Grocery", 30.0, 1));

        try {
            expiry.validate(cart2, firstOrderPromo);
        } catch (Exception e) {
            System.out.println("Caught Expected Exception: " + e.getMessage());
        }
    }
}

The main() driver configures the validation chain, schedules promo codes, applies discounts to carts, and asserts that invalid promo codes are blocked.


Also See

Comments

Popular posts from this blog

Designing a Parking Lot - Low Level Design

Problem Statement Design a parking lot that can handle vehicles entering and leaving while managing parking across multiple floors. Each vehicle should be assigned a suitable parking spot based on its type, and the spot should be freed once the vehicle exits. The design should also support generating a ticket at entry and optionally calculating the parking fee based on the duration of stay. Asked In Companies Amazon Google Microsoft Uber Walmart Flipkart Meta PayPal Oracle Salesforce Adobe Apple Intuit LinkedIn Atlassian Functional Requirements The design should support multiple vehicle types such as bikes, cars, and trucks A vehicle must be assigned a parking spot compatible with its type A parking spot cannot be assigned to more than one vehicle at a time The parking lot should support multiple levels (floors) The design should search and allocate an availa...

Most Frequently Asked Low Level Design(LLD) Interview Questions

Below are the curated list of most commonly asked Low Level Design (LLD) interview problems. Each problem includes a short description and a link to the complete solution with code and class diagrams. Design Parking Lot System The system should handle parking for different vehicle types such as bikes, cars, and trucks. It should manage slot allocation, availability tracking, and entry/exit flow. The design also ensures efficient usage of parking space under varying load conditions. View Solution Design Elevator / Lift System The system should support multiple elevators operating across floors with request handling logic. It focuses on scheduling algorithms to minimize wait time and optimize movement. It also manages direction control and concurrent floor requests. View Solution Design Movie Ticket Booking System The system should allow users to browse movies, select shows, and book seats. It handles seat ...

Software Design Patterns for LLD Interviews: A Complete Guide

Software Design Patterns for LLD Interviews: A Complete Guide In Software Development Engineer (SDE) interviews—especially for mid-level and senior roles—low-level design (LLD) rounds assess your ability to write clean, reusable, maintainable, and extensible code. The foundation of resolving these architectural challenges lies in the standard Gang of Four (GoF) Design Patterns. Rather than memorizing theoretical definitions, interviewers expect you to apply these patterns to real-world scenarios, identifying the trade-offs of each. Below is a comprehensive guide to the 12 most frequently asked design patterns in LLD interviews, categorized by their classification (Creational, Structural, and Behavioral). Each pattern contains a concrete, real-world Java implementation and a detailed breakdown of design decisions. Creational Design Patterns Creational design patterns deal with object creation mechanisms. They abstract the instantiation process, making a system independent of how...