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