Coupon System - mukular/food-delivery-feastfast--architecture GitHub Wiki

Coupon System Design

This document explains the coupon architecture, rules engine, edge-case handling, and design decisions behind the coupon system used in this food delivery platform.

The system is designed to be:

  • Race-condition resistant
  • Scalable
  • Easy to reason about
  • Aligned with real-world platforms (Swiggy / Zomato style)

1. Goals of the Coupon System

The coupon system supports:

  • Restaurant-specific coupons
  • Percentage / Fixed / Free-item discounts
  • Per-user usage limits
  • Global usage limits
  • Category & item-level restrictions
  • Coupon preview during checkout
  • Final validation during order creation

2. Coupon Types Supported

Type Description
percentage % discount with optional max cap
fixed Flat amount discount
free_item Adds a free item to cart

3. Coupon Data Model

ShopCoupon Schema (Key Fields)

{
  code: String,
  shop: ObjectId,

  discountType: "percentage" | "fixed" | "free_item",
  discountValue: Number,

  freeItem: {
    id: ObjectId,
    name: String
  },

  minOrderAmount: Number,
  maxDiscountAmount: Number,

  startDate: Date,
  endDate: Date,

  usageLimitPerUser: Number,
  usageLimitTotal: Number,

  usedCount: Number,

  applicableCategories: [String],
  excludedItems: [{ id, name }],

  userUsed: [{
    userId,
    usedCount,
    lastUsed
  }]
}

4. Coupon Lifecycle

Step-by-step lifecycle:

Coupon Created
   ↓
Coupon Applied (Checkout Preview)
   ↓
Coupon Validated (Create Order)
   ↓
Coupon Consumed

5. Coupon Application (Checkout Preview)

Purpose

  • Show discount to user
  • Validate basic eligibility
  • Does NOT mutate database

Validation Performed

  • Active status
  • Date validity
  • Min order amount
  • Category match
  • Excluded items check
  • User usage limit
  • Total usage limit

Output

{
  "discount": 40,
  "appliedCoupon": {
    "code": "SAVE40",
    "discountType": "percentage"
  }
}

6. Final Coupon Validation (Create Order)

Coupon is re-validated during order creation to prevent:

  • Tampering
  • Race conditions
  • Stale client data

Why re-validate?

Frontend cannot be trusted.


7. Coupon Consumption Strategy (Important)

Decision Taken

Consume coupon during order creation, not during payment webhook.

Why?

Problem Explanation
Race conditions Webhooks can arrive late
Overuse Multiple users can exceed limit
Complexity Reservation systems add bugs
Refund cost Payment gateways charge fees

This mirrors real production systems.


8. Consumption Logic

When coupon is consumed:

  • Wallet payment → immediately
  • COD → immediately
  • Online payment → immediately (before payment)

Database Mutation

coupon.usedCount += 1;

if (userUsed exists) {
  userUsed.usedCount += 1;
} else {
  push new userUsed entry
}

Atomicity is ensured by:

  • MongoDB session (transaction)
  • Single write path

All coupon mutations and order creation occur within the same database transaction, ensuring consistency under concurrent requests.


9. Handling Overuse Scenarios

Problem

Two users apply the last available coupon simultaneously.

Solution

Final check during order creation:

if (coupon.usedCount >= coupon.usageLimitTotal) {
  reject coupon
}

Only one transaction succeeds.


10. Free Item Coupons

How it Works

  • No price discount applied
  • A new item is injected into shopOrderItems
shopOrderItems.push({
  item: freeItem.id,
  name: freeItem.name,
  price: 0,
  quantity: 1
});

Why stored this way?

  • Appears in order summary
  • Included in seller view
  • Included in delivery packing list

11. Category & Excluded Item Rules

Applicable Categories

Coupon applies only if at least one item matches category.

Excluded Items

Coupon is rejected if any excluded item is present.

if (excludedItemFound) {
  return "Coupon not applicable for X item";
}

12. Time-based Validity

Coupons use UTC timestamps:

startDate <= now < endDate

Important: endDate is treated as end-of-day using normalization when needed.


13. Why No Reservation / Locking System?

Considered but rejected because:

Issue Reason
Complexity Hard to reason about
Refund costs Merchant bears loss
UX confusion User pays but coupon invalid
Scale issues Redis locks explode

This project chooses determinism over perfection.


14. Failure Scenarios & Behavior

Scenario Result
Payment fails Coupon remains consumed
Order cancelled Coupon not restored
Webhook delayed No effect
Multiple tabs Safe

This is an intentional tradeoff.


15. Security Measures

  • Coupon validation happens server-side only
  • No trust in frontend discount
  • Idempotent coupon updates
  • Coupon tied to shop

Coupons are scoped to a single shop and are never applied across multiple restaurants within a single order.


16. Performance Optimizations

  • Coupon fetched only for visible shops
  • Batched queries ($in)
  • Lean queries where possible

17. Why This System Is Production-Ready

  • Race-safe
  • Predictable
  • Scales cleanly
  • Real-world aligned
  • Easy to debug