Design Discount Coupon Engine | Low-Level Design (LLD)
Building a flexible, scalable, and thread-safe Discount Coupon Engine (plug-and-play middleware) commonly integrated into inventory management systems (like Zepto, Blinkit) and food delivery platforms (like Swiggy, Zomato).
1. Why Do We Need a Plug-and-Play Coupon Engine?
In modern e-commerce and quick-commerce applications, coupons drive user engagement. However, hardcoding discount rules inside controllers or monolithic order services leads to unmaintainable code.
- The Problem: Applications need to handle diverse discounts (bank offers, seasonal promotions, loyalty rewards, bulk purchase perks) that apply at either the cart level or product level. Some coupons can be chained together, while others are strictly exclusive.
- The Solution: A decoupled, highly modular LLD engine using clean design patterns (Strategy, Chain of Responsibility, Template Method, and Singleton) that can be plugged into any host application (e.g., Zomato clone, inventory apps) without modifying core business logic.
2. Core Mental Model & Real-Life Analogy
Real-Life Analogy: The Department Store Checkout Line & Stamp Cards
Think of applying coupons like handing a stack of discount vouchers to a store cashier:
- The Cashier (Coupon Manager): Collects your vouchers and organizes them.
- The Order of Vouchers (Chain of Responsibility): You hand over a store-wide festive voucher first, then a bank card discount voucher. Some vouchers say "Valid only if used alone" (non-cumberable), stopping further vouchers from being scanned.
- The Discount Rule (Strategy Pattern): Each voucher has specific arithmetic—some give a flat $20 off, some give 20% off, and some give 60% off up to a maximum cap of $120.
3. Functional Requirements & Scope
- Run-Time Coupon Registration: Ability to register new coupons dynamically at runtime.
- Dual-Level Discounts: Support for both Cart-Level discounts (applied to total bill) and Product-Level discounts (applied only to specific matching categories, e.g., Electronics).
- Diverse Coupon Types: Seasonal Offers, Loyalty Discounts, Bulk Purchase Discounts, and Banking Discounts.
- Pluggable Discount Strategies: Flat discount, Percentage discount, and Percentage discount with an upper cap (e.g., 60% off up to $120).
- Coupon Chaining & Exclusivity: Ability to chain multiple compatible coupons or restrict stacking when exclusivity rules apply.
- Thread Safety: Multithreaded lock mechanisms to handle high concurrency (e.g., flash sales with limited coupon quantities).
4. Step-by-Step Architectural Evolution
We use a Bottom-Up Design Approach:
- Model Classes:
Product,CartItem,Cart - Strategy Hierarchy:
DiscountStrategyabstract interface with concrete implementations (FlatDiscountStrategy,PercentageDiscountStrategy,PercentageWithCapStrategy) - Strategy Factory/Manager:
DiscountStrategyManager(Singleton) - Coupon Hierarchy:
Couponabstract base class (using Chain of Responsibility) with concrete types (SeasonalOffer,LoyaltyDiscount,BulkPurchaseCoupon,BankingCoupon) - Orchestrator:
CouponManager(Singleton) coordinating the entire discount pipeline.
5. Detailed Class Design & Specifications
A. Model Classes (Product, CartItem, Cart)
Product
- name: String
- category: String
- price: double
CartItem
- product: Product (Aggregation)
- quantity: int
- getItemTotal(): double
Cart
- items: List<CartItem> (Composition)
- originalTotal: double
- currentTotal: double
- isLoyaltyMember: boolean
- paymentBank: String
- addProduct(Product p, int qty): void
- applyDiscount(double amount): void
B. Strategy Design Pattern for Discount Calculations
Instead of writing nested if-else blocks for discount logic, we encapsulate algorithms inside separate strategy classes.
class DiscountStrategy {
public:
virtual double calculate(double baseAmount) = 0;
virtual ~DiscountStrategy() = default;
};
C. Chain of Responsibility & Template Method for Coupons
The Coupon base class acts as the handler in a Chain of Responsibility and uses the Template Method Pattern to standardize execution flow.
Template Method Execution Flow (applyDiscount):
6. Complete Implementation Code (C++)
Here is the robust, production-grade implementation reflecting the entire architecture:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <mutex>
#include <memory>
using namespace std;
// ==================== 1. STRATEGY PATTERN ====================
class DiscountStrategy {
public:
virtual double calculate(double baseAmount) = 0;
virtual ~DiscountStrategy() = default;
};
class FlatDiscountStrategy : public DiscountStrategy {
double discountAmount;
public:
FlatDiscountStrategy(double amount) : discountAmount(amount) {}
double calculate(double baseAmount) override {
return min(baseAmount, discountAmount);
}
};
class PercentageDiscountStrategy : public DiscountStrategy {
double percentage;
public:
PercentageDiscountStrategy(double pct) : percentage(pct) {}
double calculate(double baseAmount) override {
return (percentage / 100.0) * baseAmount;
}
};
class PercentageWithCapStrategy : public DiscountStrategy {
double percentage;
double capAmount;
public:
PercentageWithCapStrategy(double pct, double cap) : percentage(pct), capAmount(cap) {}
double calculate(double baseAmount) override {
double discount = (percentage / 100.0) * baseAmount;
return min(discount, capAmount);
}
};
// ==================== 2. STRATEGY TYPE ENUM & MANAGER (FACTORY) ====================
enum class StrategyType {
FLAT,
PERCENT,
PERCENT_WITH_CAP
};
class DiscountStrategyManager {
static DiscountStrategyManager* instance;
static mutex mtx;
DiscountStrategyManager() {}
public:
static DiscountStrategyManager* getInstance() {
lock_guard<mutex> lock(mtx);
if (!instance) {
instance = new DiscountStrategyManager();
}
return instance;
}
shared_ptr<DiscountStrategy> getStrategy(StrategyType type, double param1, double param2 = 0.0) {
if (type == StrategyType::FLAT) {
return make_shared<FlatDiscountStrategy>(param1);
} else if (type == StrategyType::PERCENT) {
return make_shared<PercentageDiscountStrategy>(param1);
} else if (type == StrategyType::PERCENT_WITH_CAP) {
return make_shared<PercentageWithCapStrategy>(param1, param2);
}
return nullptr;
}
};
DiscountStrategyManager* DiscountStrategyManager::instance = nullptr;
mutex DiscountStrategyManager::mtx;
// ==================== 3. MODEL CLASSES ====================
class Product {
string name;
string category;
double price;
public:
Product(string n, string cat, double p) : name(n), category(cat), price(p) {}
string getCategory() const { return category; }
double getPrice() const { return price; }
string getName() const { return name; }
};
class CartItem {
Product product;
int quantity;
public:
CartItem(Product p, int qty) : product(p), quantity(qty) {}
double getItemTotal() const { return product.getPrice() * quantity; }
Product getProduct() const { return product; }
int getQuantity() const { return quantity; }
};
class Cart {
vector<CartItem> items;
double originalTotal;
double currentTotal;
bool isLoyaltyMemberFlag;
string paymentBank;
public:
Cart(bool loyalty = false, string bank = "")
: originalTotal(0.0), currentTotal(0.0), isLoyaltyMemberFlag(loyalty), paymentBank(bank) {}
void addProduct(Product p, int qty = 1) {
items.push_back(CartItem(p, qty));
double itemVal = p.getPrice() * qty;
originalTotal += itemVal;
currentTotal += itemVal;
}
void applyDiscount(double amount) {
currentTotal = max(0.0, currentTotal - amount);
}
double getOriginalTotal() const { return originalTotal; }
double getCurrentTotal() const { return currentTotal; }
bool isLoyaltyMember() const { return isLoyaltyMemberFlag; }
string getPaymentBank() const { return paymentBank; }
const vector<CartItem>& getItems() const { return items; }
};
// ==================== 4. COUPON HIERARCHY (CHAIN OF RESPONSIBILITY) ====================
class Coupon {
protected:
Coupon* next;
shared_ptr<DiscountStrategy> strategy;
public:
Coupon() : next(nullptr) {}
virtual ~Coupon() = default;
void setNext(Coupon* n) { next = n; }
Coupon* getNext() const { return next; }
virtual bool isApplicable(Cart* cart) = 0;
virtual bool isCombinable(Cart* cart) { return true; }
virtual double getDiscount(Cart* cart) = 0;
virtual string getName() = 0;
// Template Method Pattern
void applyDiscount(Cart* cart) {
if (isApplicable(cart)) {
double discount = getDiscount(cart);
cart->applyDiscount(discount);
cout << "[Applied Coupon]: " << getName() << " | Discount: $" << discount << endl;
if (!isCombinable(cart)) {
cout << "[Notice]: Coupon " << getName() << " is not combinable. Stopping chain." << endl;
return;
}
}
if (next != nullptr) {
next->applyDiscount(cart);
}
}
};
// Concrete Coupon 1: Seasonal Offer (Product/Category Level)
class SeasonalOffer : public Coupon {
string targetCategory;
public:
SeasonalOffer(string cat, double pct) : targetCategory(cat) {
strategy = DiscountStrategyManager::getInstance()->getStrategy(StrategyType::PERCENT, pct);
}
bool isApplicable(Cart* cart) override {
for (const auto& item : cart->getItems()) {
if (item.getProduct().getCategory() == targetCategory) return true;
}
return false;
}
double getDiscount(Cart* cart) override {
double subtotal = 0.0;
for (const auto& item : cart->getItems()) {
if (item.getProduct().getCategory() == targetCategory) {
subtotal += item.getItemTotal();
}
}
return strategy->calculate(subtotal);
}
string getName() override { return "Seasonal Offer (" + targetCategory + " 10% Off)"; }
};
// Concrete Coupon 2: Loyalty Discount (Cart Level)
class LoyaltyDiscount : public Coupon {
public:
LoyaltyDiscount(double pct) {
strategy = DiscountStrategyManager::getInstance()->getStrategy(StrategyType::PERCENT, pct);
}
bool isApplicable(Cart* cart) override {
return cart->isLoyaltyMember();
}
double getDiscount(Cart* cart) override {
return strategy->calculate(cart->getCurrentTotal());
}
string getName() override { return "Loyalty Member Discount"; }
};
// Concrete Coupon 3: Bulk Purchase Coupon (Cart Level with Threshold)
class BulkPurchaseCoupon : public Coupon {
double threshold;
public:
BulkPurchaseCoupon(double thresh, double flatOff) : threshold(thresh) {
strategy = DiscountStrategyManager::getInstance()->getStrategy(StrategyType::FLAT, flatOff);
}
bool isApplicable(Cart* cart) override {
return cart->getOriginalTotal() >= threshold;
}
double getDiscount(Cart* cart) override {
return strategy->calculate(cart->getCurrentTotal());
}
string getName() override { return "Bulk Purchase Flat Discount"; }
};
// Concrete Coupon 4: Banking Coupon (Cart Level with Cap)
class BankingCoupon : public Coupon {
string targetBank;
double minSpend;
public:
BankingCoupon(string bank, double minSpd, double pct, double cap)
: targetBank(bank), minSpend(minSpd) {
strategy = DiscountStrategyManager::getInstance()->getStrategy(StrategyType::PERCENT_WITH_CAP, pct, cap);
}
bool isApplicable(Cart* cart) override {
return (cart->getPaymentBank() == targetBank) && (cart->getOriginalTotal() >= minSpend);
}
double getDiscount(Cart* cart) override {
return strategy->calculate(cart->getCurrentTotal());
}
string getName() override { return "Banking Offer (" + targetBank + ")"; }
};
// ==================== 5. COUPON MANAGER (ORCHESTRATOR) ====================
class CouponManager {
Coupon* head;
mutex mtx;
public:
CouponManager() : head(nullptr) {}
void registerCoupon(Coupon* c) {
lock_guard<mutex> lock(mtx);
if (!head) {
head = c;
} else {
Coupon* temp = head;
while (temp->getNext() != nullptr) {
temp = temp->getNext();
}
temp->setNext(c);
}
}
vector<string> getApplicableCoupons(Cart* cart) {
lock_guard<mutex> lock(mtx);
vector<string> applicable;
Coupon* temp = head;
while (temp != nullptr) {
if (temp->isApplicable(cart)) {
applicable.push_back(temp->getName());
}
temp = temp->getNext();
}
return applicable;
}
void applyAllCoupons(Cart* cart) {
lock_guard<mutex> lock(mtx);
if (head != nullptr) {
head->applyDiscount(cart);
}
}
};
// ==================== 6. CLIENT DRIVER CODE ====================
int main() {
// 1. Create Products
Product p1("Winter Jacket", "Clothing", 1000.0);
Product p2("Smartphone", "Electronics", 20000.0);
Product p3("Denim Jeans", "Clothing", 1500.0);
Product p4("Wireless Earbuds", "Electronics", 2500.0);
// 2. Setup Cart
Cart cart(true, "HDFC"); // Loyalty member = true, Payment Bank = HDFC
cart.addProduct(p1, 1); // 1000
cart.addProduct(p2, 1); // 20000
cart.addProduct(p3, 1); // 1500
cart.addProduct(p4, 1); // 2500
// Total Original = 25000.0
cout << "Initial Cart Total: $" << cart.getOriginalTotal() << "\n\n";
// 3. Setup Coupon Manager & Register Coupons
CouponManager manager;
manager.registerCoupon(new SeasonalOffer("Clothing", 10.0)); // 10% off on Clothing
manager.registerCoupon(new LoyaltyDiscount(5.0)); // 5% off for loyalty members
manager.registerCoupon(new BulkPurchaseCoupon(5000.0, 200.0));// $200 off if total >= 5000
manager.registerCoupon(new BankingCoupon("HDFC", 2000.0, 15.0, 500.0)); // HDFC offer: 15% up to $500 cap
// 4. Inspect Applicable Coupons
cout << "--- Applicable Coupons ---" << endl;
for (const string& name : manager.getApplicableCoupons(&cart)) {
cout << " -> " << name << endl;
}
cout << "--------------------------\n\n";
// 5. Apply All Coupons Pipeline
cout << "--- Applying Discount Pipeline ---" << endl;
manager.applyAllCoupons(&cart);
cout << "----------------------------------\n";
// 6. Final Result
cout << "\nFinal Payable Amount: $" << cart.getCurrentTotal() << endl;
return 0;
}
7. Real-World Practical Pitfalls & Debugging Mindset
- Order of Execution in Chain of Responsibility: The order in which coupons are registered matters immensely. If a percentage discount runs before a flat deduction versus after, the final total changes. Always align with business stakeholders on execution priority.
- Floating-Point Precision Errors: When compounding percentage calculations over cart items, floating-point round-off issues can occur. Use
std::roundor currency-handling data types (cents as integers) in production financial systems. - Concurrency & Race Conditions: In flash sales where only 4 coupons are available, multiple threads attempting to decrement the coupon count simultaneously can cause overselling. The
std::mutexlocks implemented inCouponManagerprevent race conditions.
8. Summary and Key Takeaways
- Decoupled Architecture: Using the Strategy Pattern separates discount math from business rules, allowing new discount structures (e.g., buy-one-get-one) to be added without modifying existing code.
- Template Method Flexibility: The Chain of Responsibility allows coupons to evaluate applicability, calculate discounts, and pass execution down the pipeline cleanly.
- Singletons for Managers:
DiscountStrategyManagerandCouponManagermaintain centralized lifecycles while enforcing thread safety. - Extensibility: New coupon types can be introduced effortlessly by extending the
Couponbase class and registering them via the manager.