Â
Â
1. LEARNING OBJECTIVES
By the end of this expansive, 20+ page lesson, you will be able to:
-
Understand the fundamental business metrics of subscription-based FinTech: MRR (Monthly Recurring Revenue), ARR (Annual Recurring Revenue), and Churn Rate.
-
Differentiate between “One-time transactions” and “Recurring billing” in a payment gateway’s architecture.
-
Explain the mathematical and business logic of Proration—calculating exact charges when a customer upgrades or downgrades their subscription mid-billing cycle.
-
Design the core architecture of a Subscription Billing Engine that tracks the state of thousands of active subscriptions.
-
Understand the Dunning Process (failed payment recovery) and why automated retries with exponential backoff are critical for retaining customers.
-
Build a complete, beginner-friendly Python simulation of a Subscription Billing Engine that handles sign-ups, upgrades (with proration), and downgrades.
-
Write a complete Python script to simulate a Dunning Engine that retries failed payments on a smart schedule (Day 1, Day 3, Day 7) and cancels subscriptions after persistent failure.
2. THE ECONOMICS OF SUBSCRIPTIONS IN FINTECH
2.1 The Shift from One-Off to Recurring
In the early days of software, you bought a license for $100 and owned it forever. Today, almost every FinTech product (from trading platforms like Robinhood to accounting software like QuickBooks) operates on a Subscription Model.
Why? Because predictable recurring revenue allows a company to project their cash flow years into the future, making it significantly easier to secure investment and scale operations.
2.2 The Holy Trinity of Subscription Metrics
To build a billing engine, you must understand the three metrics that drive the business:
-
MRR (Monthly Recurring Revenue):Â The predictable, normalized monthly revenue generated by active subscriptions.
If you have 1,000 customers paying $10/month, your MRR is $10,000. -
ARR (Annual Recurring Revenue):Â Calculated simply asÂ
MRR * 12. A SaaS FinTech company with an ARR of $1 million is valued very differently than one with an ARR of $100,000. -
Churn Rate:Â The percentage of customers who cancel their subscription within a given period (usually monthly).
If 100 customers start the month, and 5 cancel, your Monthly Churn Rate is 5%.
The Critical Math: A 5% monthly churn rate does not mean you lose 60% a year. Because churn compounds, a 5% monthly churn rate results in an annual churn of roughly 46%. Reducing churn by even 1% translates to massive increases in overall company valuation.
3. THE ARCHITECTURE OF A SUBSCRIPTION BILLING ENGINE
When a user signs up for a recurring subscription, the Payment Gateway does not just process a one-time charge. Instead, the Gateway stores a “Subscription Object” in its database.
This object contains three critical pieces of data:
-
The Token:Â The secure payment token (from Lesson 1) representing the customer’s saved card.
-
The Interval:Â How often to charge (e.g.,Â
monthly,Âyearly, orÂcustom). -
The Next Billing Date:Â The exact timestamp when the next payment should be taken.
3.1 The Scheduling Architecture (The “Cron” Job)
A billing engine does not actively check the clock every millisecond. It relies on a background worker (often triggered by a scheduled Cron Job—a Unix task scheduler).
Every single day at a specific hour (e.g., 2:00 AM), a script runs and queries the database:
“Select all active subscriptions where today’s date is greater than or equal to next_billing_date.”
The script pulls these subscriptions, processes the charges via the payment gateway’s API, and upon success, increments the next_billing_date by 30 days.
4. THE MATHEMATICS OF PRORATION (UPGRADES AND DOWNGRADES)
4.1 The Problem of Mid-Cycle Changes
Imagine a customer buys a subscription for a basic FinTech trading account at $10/month on January 1st. On January 15th (halfway through the month), they decide to upgrade to the Premium account, which costs $20/month.
If you simply charge them $20 on the 15th, you have overcharged them (they already paid for the first 15 days of the Basic plan). If you wait until February 1st, they get 15 days of Premium features for free.
4.2 The Proration Math (The Industry Standard)
To fix this, billing engines use a mathematical process called Proration. The engine calculates the exact difference in cost for the remaining days of the current billing cycle.
Let’s break it down:
-
Billing Cycle:Â 30 days (January 1st to January 31st).
-
Days remaining in cycle:Â 15 days.
-
Daily Rate of Basic Plan:Â $10 / 30 days = `$0.33` per day.
-
Daily Rate of Premium Plan:Â $20 / 30 days = `$0.67` per day.
-
The Math:
-
Credit for unused Basic plan:Â 15 days * $0.33 = `$5.00`
-
Charge for new Premium plan:Â 15 days * $0.67 = `$10.00`
-
Net charge on January 15th:Â $10.00 (New cost) – $5.00 (Old credit) = **$5.00**.
On January 15th, the user is charged exactly $5.00. On February 1st, the system charges the full $20.00 for the new month. This is mathematically fair.
-
4.3 Downgrades
What if the customer downgrades from Premium to Basic on the 15th?
The math is exactly the same, but the net charge is negative (a credit).
-
Credit for unused Premium plan:Â 15 days * $0.67 = `$10.00`
-
Charge for new Basic plan:Â 15 days * $0.33 = `$5.00`
-
Net result:Â The customer gets a **$5.00 credit** applied to their account. This credit is usually applied to their next month’s bill on February 1st, reducing it from $10 to $5.
5. DUNNING: THE ART OF RECOVERING FAILED PAYMENTS
5.1 Why do payments fail?
Even with a saved card, recurring transactions fail roughly 3% to 5% of the time. The reasons include:
-
Expired Card:Â The physical card expired, but the expiration date in the database is old.
-
Insufficient Funds:Â The customer’s bank account doesn’t have the money.
-
Bank Decline:Â The issuing bank’s fraud detection system incorrectly flagged the recurring charge as suspicious.
5.2 The Dunning Process
If a payment fails, you cannot simply cancel the customer’s account immediately. You must enter the Dunning Process. Dunning is the systematic process of automatically reattempting the charge and notifying the customer to update their payment details.
5.3 The Smart Retry Schedule (Exponential Backoff)
If you retry the charge every single hour, your gateway will charge you massive fees for the attempts, and the customer will get angry because their bank will block the repeated requests.
Instead, we implement an Exponential Backoff (a mathematically spaced retry schedule).
-
Attempt 1 (Day 0):Â The initial payment fails. The subscription entersÂ
dunning state. The system emails the customer: “Your payment failed, please update your card.” -
Attempt 2 (Day 3):Â Three days later, the engine retries the charge. If it fails, it does nothing.
-
Attempt 3 (Day 7):Â Seven days after the initial failure, the engine retries the charge. If it fails, it sends a second, more urgent email.
-
Attempt 4 (Day 14): Fourteen days later, the engine retries the charge. If it fails, the subscription is automatically cancelled, and the customer is marked asÂ
inactive in the database.
6. BEGINNER HANDS-ON LAB PART 1: BUILDING A BILLING ENGINE WITH PRORATION LOGIC
We will now build a Python simulation of a Subscription Billing Engine. It will allow a user to sign up, upgrade, downgrade, and automatically calculate the prorated charges.
(Note: To keep this simple, we will use fixed month lengths of 30 days for the math demonstration.)
import datetime from datetime import timedelta # --- STEP 1: DEFINE THE PLANS AND PRICES --- # We define the available subscription tiers and their monthly costs. PLANS = { "basic": {"price": 10.00, "name": "Basic"}, "premium": {"price": 20.00, "name": "Premium"} } # --- STEP 2: THE SUBSCRIPTION CLASS --- class Subscription: def __init__(self, customer_name, plan_name, start_date): self.customer = customer_name self.plan = plan_name self.price = PLANS[plan_name]["price"] self.billing_cycle_length_days = 30 # Simplified to 30 days self.start_date = start_date self.next_billing_date = start_date + timedelta(days=self.billing_cycle_length_days) self.status = "active" self.transaction_history = [] def charge_next_billing(self): """Simulates charging the card at the next billing date.""" # In production, this sends an API request to Stripe. print(f"[BILLING] Charging {self.customer} ${self.price:.2f} for {self.plan} plan.") self.transaction_history.append({ "date": datetime.datetime.now(), "amount": self.price, "type": "recurring_charge" }) # Move the next billing date forward by 30 days self.next_billing_date = self.next_billing_date + timedelta(days=self.billing_cycle_length_days) def change_plan(self, new_plan_name, change_date): """ Calculates proration and executes the plan change. """ if new_plan_name == self.plan: print("Already on this plan.") return new_price = PLANS[new_plan_name]["price"] old_price = self.price # --- THE PRORATION MATH --- # 1. Calculate days remaining in the current billing cycle # Difference between the change_date and the next_billing_date days_remaining = (self.next_billing_date - change_date).days # Safety check: If days_remaining is 0 or negative, we are at the start of a new cycle. if days_remaining <= 0: print(f"[BILLING] Date is past the billing cycle. Immediately applying new plan price.") self.price = new_price self.plan = new_plan_name return # 2. Calculate daily rates daily_old = old_price / self.billing_cycle_length_days daily_new = new_price / self.billing_cycle_length_days # 3. Calculate the Credit (for unused old plan) and the Charge (for new plan) credit_for_old = daily_old * days_remaining charge_for_new = daily_new * days_remaining # 4. Calculate Net Prorated Amount prorated_charge = charge_for_new - credit_for_old # --- EXECUTING THE CHANGE --- if prorated_charge > 0: print(f"[BILLING] Upgrade from {self.plan} to {new_plan_name}.") print(f" Prorated Charge today: ${prorated_charge:.2f} (Days remaining: {days_remaining})") # In production, we would charge the card `prorated_charge` here. self.transaction_history.append({ "date": datetime.datetime.now(), "amount": prorated_charge, "type": "prorated_upgrade" }) elif prorated_charge < 0: # A negative charge means we owe the customer a credit credit_amount = abs(prorated_charge) print(f"[BILLING] Downgrade from {self.plan} to {new_plan_name}.") print(f" Credit applied to account: ${credit_amount:.2f} (Applied to next month's bill)") self.transaction_history.append({ "date": datetime.datetime.now(), "amount": -credit_amount, # Stored as a negative "type": "prorated_downgrade_credit" }) else: print(f"[BILLING] Plan change with $0 proration (neither party owes anything).") # 5. Update the subscription state self.plan = new_plan_name self.price = new_price # --- STEP 3: RUNNING THE SIMULATION --- # We create a subscription starting Jan 1st. start_date = datetime.date(2024, 1, 1) sub = Subscription("Alice", "basic", start_date) print("--- SIMULATION: SUBSCRIPTION LIFECYCLE ---") sub.charge_next_billing() # Alice pays $10 on Jan 1st. Next billing is Feb 1st. # On Jan 15th, Alice wants to upgrade to Premium. # We calculate the proration based on Jan 15th. upgrade_date = datetime.date(2024, 1, 15) sub.change_plan("premium", upgrade_date) # On Feb 1st, the next billing runs. # Because her daily rate is now $20/month, she pays the full $20. print("\n--- NEXT BILLING CYCLE (FEBRUARY 1ST) ---") sub.charge_next_billing() print("\n--- TRANSACTION HISTORY ---") for tx in sub.transaction_history: print(tx)
Interpretation of the Lab:
If you run this code, you will see a beautiful mathematical breakdown:
-
The initial charge is $10.
-
On the 15th, the engine calculates 15 days remaining. The daily difference is $0.34. It correctly charges Alice **$5.00** on the upgrade date.
-
On Feb 1st, the engine charges the full $20.00.
This automated proration math is the exact code that runs behind platforms like Netflix, Spotify, and Stripe Billing when you switch your plan mid-month.
7. BEGINNER HANDS-ON LAB PART 2: BUILDING THE DUNNING ENGINE (RETRY SCHEDULER)
We will now build a Dunning Engine. We will simulate a customer whose card fails, and we will schedule automatic retries on specific days (Day 3, Day 7, Day 14) with smart exponential delays, ultimately cancelling the account if the attempts fail.
import datetime from datetime import timedelta import time # --- STEP 1: MOCK PAYMENT GATEWAY --- # This simulates a real payment gateway. We will make it fail 3 times, then succeed on the 4th. class MockPaymentGateway: def __init__(self): self.attempt_counter = 0 def charge_card(self, token, amount): self.attempt_counter += 1 # Simulate: Fail for the first 3 attempts, succeed on the 4th. if self.attempt_counter <= 3: return {"status": "failed", "error": "insufficient_funds"} else: return {"status": "succeeded", "transaction_id": "txn_12345"} # --- STEP 2: THE DUNNING ENGINE --- class DunningEngine: def __init__(self, payment_gateway): self.gateway = payment_gateway self.dunning_queue = [] # List of subscriptions currently in the dunning process def add_to_dunning(self, subscription, last_attempt_date): """Adds a subscription to the dunning queue after an initial failure.""" # We schedule the first retry 3 days after the last attempt. self.dunning_queue.append({ "subscription": subscription, "last_attempt_date": last_attempt_date, "attempt_count": 1 # We are on the 1st retry attempt }) print(f"[DUNNING] Subscription for {subscription.customer} entered dunning queue.") def process_dunning_queue(self, current_date): """Scans the queue and processes any subscriptions that are ready for a retry.""" print(f"\n[DUNNING ENGINE] Scanning queue on {current_date.strftime('%Y-%m-%d')}...") # We iterate over a copy of the list to allow modifications for item in list(self.dunning_queue): sub = item["subscription"] last_attempt = item["last_attempt_date"] attempt_count = item["attempt_count"] # --- THE SMART RETRY SCHEDULE (EXPONENTIAL BACKOFF) --- # Attempt 1: 3 days after last attempt. # Attempt 2: 7 days after last attempt. # Attempt 3: 14 days after last attempt. schedule = {1: 3, 2: 7, 3: 14} days_until_next_retry = schedule.get(attempt_count) # Check if enough days have passed since the last attempt if (current_date - last_attempt).days >= days_until_next_retry: print(f" -> RETRYING CHARGE for {sub.customer} (Attempt #{attempt_count})") # Attempt the charge via the gateway result = self.gateway.charge_card("token_123", sub.price) if result["status"] == "succeeded": print(f" -> SUCCESS! Payment recovered for {sub.customer}.") # Remove them from dunning, set status back to active, move the billing date forward. self.dunning_queue.remove(item) sub.status = "active" sub.next_billing_date = current_date + timedelta(days=30) else: # Payment failed again print(f" -> FAILED. Reason: {result['error']}") # Increment attempt count and update the last attempt date item["attempt_count"] += 1 item["last_attempt_date"] = current_date # Check if we have exceeded maximum retries (3 attempts) if item["attempt_count"] > 3: print(f" -> CRITICAL: Max retries exhausted for {sub.customer}.") print(f" -> SUBSCRIPTION CANCELLED.") # Cancel the subscription, remove from queue sub.status = "cancelled" self.dunning_queue.remove(item) # --- STEP 3: RUNNING THE DUNNING SIMULATION --- # We use the Subscription class from our previous lab. # We create a subscription for Bob. start_date = datetime.date(2024, 1, 1) bob_sub = Subscription("Bob", "basic", start_date) # Simulate the initial payment failing print("--- DUNNING SIMULATION: INITIAL PAYMENT FAILS ---") print(f"[BILLING] Initial charge for {bob_sub.customer} FAILED.") # Initialize the Mock Gateway and Dunning Engine gateway = MockPaymentGateway() # This will fail 3 times, succeed on the 4th. engine = DunningEngine(gateway) # Add Bob's subscription to the dunning queue engine.add_to_dunning(bob_sub, datetime.date(2024, 1, 1)) # We will fast-forward time and check the queue on specific days test_dates = [ datetime.date(2024, 1, 4), # Day 3 datetime.date(2024, 1, 8), # Day 7 datetime.date(2024, 1, 15), # Day 14 ] for date in test_dates: engine.process_dunning_queue(date) print(f"\n--- FINAL STATUS ---") print(f"Bob's Subscription Status: {bob_sub.status}")
Interpretation of the Lab:
When you run this code, you will watch the lifecycle of a dunning process.
-
On Jan 4th, the engine retries, but the Mock Gateway fails.
-
On Jan 8th, it retries again, the Gateway fails.
-
On Jan 15th, it retries for the 3rd time, the Gateway succeeds.
The engine catches the success, removes Bob from the dunning queue, reactivates his subscription, and schedules his next billing for Jan 15th + 30 days.
This automated logic saves FinTech companies millions of dollars in lost revenue every year. In a real bank, the “Mock Gateway” would be an actual API call to Stripe, and the “engine” would be running as a background script on a server.
8. SUMMARY FOR THE FINANCE PRACTITIONER
Subscription billing is the heartbeat of modern FinTech SaaS.
-
Proration is mandatory for fairness. If you upgrade or downgrade a user and don’t prorate the charge, you will alienate your customer base. Implement the daily-rate calculation method.
-
Dunning is a profit center, not a cost center. Most customers want to pay you, but their cards expire. An automated dunning engine with exponential backoff (Day 3, 7, 14) recovers up to 60% of failed payments, dramatically improving your MRR.
-
Billing is stateful. Unlike a one-time payment, a subscription has a state (
active,Âdunning,Âcancelled) and aÂnext_billing_date. Your database must be indexed perfectly on these fields to allow your background worker to efficiently scan millions of rows each night.