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.
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.
Comments
Post a Comment