Savings vs Credit-Card Debt
Same card, same income - one number decides everything: pay just under the interest and the debt runs away; pay just over it and the balance marches to zero.
Level: Beginner
Dynamic Behavior Patterns
Explore common system behaviors: exponential growth, goal-seeking decay, overshoot-and-collapse, and S-curve saturation.
Take the QuizThe minimum-payment trap
Savings and credit-card debt both compound every month. Whether the debt shrinks or explodes hinges on one number: the monthly payment. Pay more than the interest and the balance marches to zero; pay less, and the unpaid interest is rolled into the balance, which makes next month's interest larger still — a reinforcing loop that runs away.
The knob is monthly_payment. The tipping point is the monthly interest
debt_rate * balance: pay below it and the debt grows without end.
from tys import probe, progress
Simulate monthly savings and debt balances.
def simulate(cfg: dict):
import simpy
env = simpy.Environment()
savings = cfg["initial_savings"]
debt = cfg["initial_debt"]
savings_rate = cfg["savings_rate"] # monthly interest rate on savings
debt_rate = cfg["debt_rate"] # monthly interest rate on debt
deposit = cfg["monthly_deposit"] # added to savings each month
payment = cfg["monthly_payment"] # paid toward debt each month
months = cfg["months"]
initial_debt = debt
done = env.event()
Apply interest, then deposits and payments, each month.
def cycle():
nonlocal savings, debt
for m in range(months):
savings = savings * (1 + savings_rate) + deposit
debt = max(0, debt * (1 + debt_rate) - payment) # interest first, then the payment
probe("savings_balance", env.now, savings)
probe("debt_balance", env.now, debt)
progress(int(100 * (m + 1) / months))
yield env.timeout(1)
done.succeed({
"final_savings": savings,
"final_debt": debt,
"debt_ran_away": debt > initial_debt,
"debt_paid_off": debt <= 0.01,
})
env.process(cycle())
env.run(until=done)
return done.value
def requirements():
return {
"external": ["simpy==4.1.1"],
}
# trap.yaml — the default: monthly_payment sits below the monthly interest
# (~40/mo at the start), so the debt grows without end.
initial_savings: 1000
initial_debt: 2000
savings_rate: 0.005
debt_rate: 0.02
monthly_deposit: 100
monthly_payment: 25
months: 48
| Metric | Value |
|---|---|
| final_savings | 6680.27 |
| final_debt | 3190.30 |
| debt_ran_away | true |
| debt_paid_off | false |
# paydown.yaml — the contrast: identical except monthly_payment (25 -> 70),
# which clears the monthly interest and drives the balance to zero.
initial_savings: 1000
initial_debt: 2000
savings_rate: 0.005
debt_rate: 0.02
monthly_deposit: 100
monthly_payment: 70
months: 48
| Metric | Value |
|---|---|
| final_savings | 6680.27 |
| final_debt | 0.00 |
| debt_ran_away | false |
| debt_paid_off | true |
- When does the debt start to run away?
- When the monthly payment is less than the interest charged that month (debt_rate * balance). The unpaid interest is added to the balance, so next month's interest is larger still - the minimum-payment trap.
- Both runs use the same card. Why does one debt vanish and the other grow?
- Only monthly_payment differs. At 25 it sits below the ~40/mo starting interest, so the balance climbs; at 70 it clears the interest and chips away at the principal until the balance reaches zero.
- Why does the savings line look the same in both runs?
- The savings side (deposit and interest) is held fixed, so the only thing that changes is the debt payment - savings is the steady reference the debt is measured against.
- What is the real-world lesson?
- Paying only the minimum on a high-interest card can leave you owing more over time, even while you pay every single month. Beating the monthly interest is what turns the balance around.