Rule Based Discount Engine
In real systems, pricing logic is never:
if total > 100:
discount = 10
Real systems involve:
- Multiple conditions
- Overlapping rules
- Priority ordering
- Conflict resolution
- Extensibility
This project teaches you how to design a deterministic rule engine.
Concept Overview
While watching, focus on:
- Rule ordering importance
- Conflict resolution strategies
- Deterministic evaluation
- Mapping-based decision models
- Scalability of rule systems
Step 1 - Define Requirements
Our discount engine must:
- Accept cart total
- Accept customer type
- Accept coupon code
- Apply correct discount
- Prevent double-discount errors
- Be extensible for future rules
Rule systems must be deterministic. Same input → same output.
Step 2 - Define Basic Rules
Rules:
- VIP customers → 20% discount
- Orders above 100 → 10% discount
- Coupon "SAVE5" → 5% discount
- Employee → 30% discount
- Default → No discount
Step 3 - Naive Implementation (Incorrect Design)
if customer_type == "VIP":
discount = 0.20
elif total > 100:
discount = 0.10
elif coupon == "SAVE5":
discount = 0.05
Problem:
- Only one rule applied
- No priority control
- Hard to extend
- Order-sensitive
- Branch explosion risk
Step 4 - Deterministic Priority Model
Define rule priority:
- Employee
- VIP
- Coupon
- Cart Total
Explicit ordering matters.
Step 5 - Structured Implementation
def calculate_discount(total, customer_type, coupon):
if customer_type == "Employee":
return 0.30
if customer_type == "VIP":
return 0.20
if coupon == "SAVE5":
return 0.05
if total > 100:
return 0.10
return 0.0
This ensures deterministic order.
Step 6 - Apply Discount
def apply_discount(total, discount_rate):
return total * (1 - discount_rate)
Usage:
total = 150
discount = calculate_discount(total, "VIP", None)
final_price = apply_discount(total, discount)
print(f"Final price: {final_price}")
Step 7 - Prevent Overlapping Rule Conflicts
What if:
- VIP
- Coupon applied
- Total > 100
Should discounts stack?
If stacking allowed:
def calculate_discount(total, customer_type, coupon):
discount = 0
if customer_type == "Employee":
discount += 0.30
elif customer_type == "VIP":
discount += 0.20
if coupon == "SAVE5":
discount += 0.05
if total > 100:
discount += 0.10
return min(discount, 0.50) # Cap at 50%
Now we control maximum exposure.
Stacking without limits creates financial risk.
Step 8 - Convert to Rule Mapping (Scalable Model)
Avoid long if chains.
Use rule table.
RULES = [
("Employee", 0.30),
("VIP", 0.20),
]
def get_customer_discount(customer_type):
for rule_type, rate in RULES:
if customer_type == rule_type:
return rate
return 0
Extendable.
Add new roles easily.
Step 9 - Advanced Rule Engine (Data-Driven)
Define rules as functions.
def employee_rule(context):
if context["customer_type"] == "Employee":
return 0.30
return 0
def vip_rule(context):
if context["customer_type"] == "VIP":
return 0.20
return 0
def coupon_rule(context):
if context["coupon"] == "SAVE5":
return 0.05
return 0
def cart_rule(context):
if context["total"] > 100:
return 0.10
return 0
Engine:
RULE_FUNCTIONS = [
employee_rule,
vip_rule,
coupon_rule,
cart_rule,
]
def calculate_discount(context):
discount = 0
for rule in RULE_FUNCTIONS:
discount += rule(context)
return min(discount, 0.50)
Usage:
context = {
"total": 150,
"customer_type": "VIP",
"coupon": "SAVE5"
}
discount = calculate_discount(context)
final_price = apply_discount(context["total"], discount)
Now rule system is modular.
Step 10 - Decision Tree Visualization
Decision order:
Customer Type ↓ Coupon ↓ Cart Threshold ↓ Cap Check
Structured flow reduces ambiguity.
Step 11 - Add Early Exit Optimization
If stacking disabled:
for rule in RULE_FUNCTIONS:
discount = rule(context)
if discount > 0:
return discount
Stops evaluation early.
Performance-aware design.
Cheap checks should run before expensive checks.
Step 12 - Detect Invalid Coupons
Add validation layer.
VALID_COUPONS = {"SAVE5"}
def coupon_rule(context):
coupon = context["coupon"]
if coupon and coupon not in VALID_COUPONS:
raise ValueError("Invalid coupon code")
if coupon == "SAVE5":
return 0.05
return 0
Fail fast.
Step 13 - Prevent Branch Explosion
Instead of:
if role == "VIP" and region == "EU":
Use mapping:
REGIONAL_DISCOUNTS = {
("VIP", "EU"): 0.25,
("VIP", "US"): 0.20,
}
Then:
rate = REGIONAL_DISCOUNTS.get((customer_type, region), 0)
Data-driven branching scales better.
Step 14 - Complexity Analysis
Each additional rule increases:
- Execution paths
- Risk of overlap
- Testing combinations
- Maintenance cost
Rule engines must be:
- Ordered
- Explicit
- Bounded
- Testable
Step 15 - Testing Strategy
Test cases:
- VIP + coupon
- Employee + coupon
- Invalid coupon
- High total no coupon
- Edge case total = 100
- Discount cap exceeded
Control flow systems must be exhaustively tested.
Common Mistakes
❌ Random rule ordering
❌ Hidden stacking
❌ Missing caps
❌ Deep nested if-elif chains
❌ No fallback
❌ Overlapping ambiguous conditions
Engineering Reflection
Before deployment, ask:
- Is rule order deterministic?
- Can new rules be added safely?
- Are discount caps enforced?
- Are conflicts handled?
- Is branching readable?
- Is evaluation cheap-to-expensive?
Rule systems grow.
Architecture must anticipate growth.
Final Insight
This project teaches:
- Rule ordering
- Decision-tree modeling
- Conflict resolution
- Data-driven branching
- Guard-based validation
- Performance-aware evaluation
Real e-commerce systems are rule engines.
Real pricing systems are decision trees.
Control flow is not about syntax.
It is about business logic architecture.
You just built one.
