I have signed three loans in my life. I regretted two of them within a year. After the third, I made a rule: never sign a loan without modeling it in code first. The math is so small that a junior dev can write it in an afternoon, but the discipline of running it before the dotted line has saved me real money since.
This post is the simulator I use, in roughly thirty lines, plus the three questions I ask it before agreeing to anything.
The amortization formula, in code
A fixed-rate loan is just a stream of payments where each one pays this month's interest first and the rest of it goes to principal. So you only need two helpers:
function monthlyPayment({ principal, annualRate, years }) {
const r = annualRate / 12;
const n = years * 12;
return (principal * r) / (1 - Math.pow(1 + r, -n));
}
function amortize({ principal, annualRate, years }) {
const r = annualRate / 12;
const payment = monthlyPayment({ principal, annualRate, years });
let balance = principal;
const rows = [];
for (let month = 1; month <= years * 12; month++) {
const interest = balance * r;
const principalPaid = payment - interest;
balance -= principalPaid;
rows.push({ month, payment, interest, principalPaid, balance: Math.max(0, balance) });
}
return { payment, rows };
}
That is it. Two functions, no dependencies, runs in a browser console. Everything else is just slicing the rows array and asking questions.
Question 1: how much will I have paid in total?
The total of all payments is the part lenders never lead with:
const { payment, rows } = amortize({
principal: 300000,
annualRate: 0.065,
years: 30,
});
const totalPaid = rows.reduce((sum, r) => sum + r.payment, 0);
const totalInterest = totalPaid - 300000;
console.log({ payment, totalPaid, totalInterest });
// payment: 1896.20, totalPaid: 682632, totalInterest: 382632
A $300,000 mortgage at 6.5% for 30 years costs you $682,632 in cash. The interest alone is $382,632, more than the original price of the house. Seeing those three numbers next to each other is a different experience than reading "6.5% APR" on a flyer.
When I want to show this to someone without making them install Node, I send the Mortgage Calculator on Equation Solver which produces the same totals plus a chart of how much of each payment is interest versus principal. That chart is great for explaining to a first-time buyer why early-payoff strategies work so well.
Question 2: what does an extra payment a year do?
This is the question that converted me to mortgage pre-payment. Add one extra principal payment per year and re-run the simulation:
function amortizeWithExtra({ principal, annualRate, years, extraPerYear }) {
const r = annualRate / 12;
const payment = monthlyPayment({ principal, annualRate, years });
let balance = principal;
let month = 0;
let totalPaid = 0;
while (balance > 0 && month < years * 12 + 12) {
month++;
const interest = balance * r;
let principalPaid = payment - interest;
if (month % 12 === 0) principalPaid += extraPerYear;
balance -= principalPaid;
totalPaid += payment + (month % 12 === 0 ? extraPerYear : 0);
if (balance < 0) {
totalPaid += balance;
balance = 0;
}
}
return { months: month, totalPaid, totalInterest: totalPaid - principal };
}
amortizeWithExtra({
principal: 300000,
annualRate: 0.065,
years: 30,
extraPerYear: 2000,
});
// months: 295, totalPaid: ~595k, totalInterest: ~295k
One extra $2,000 payment per year shaves about 5 years off the loan and saves around $87,000 in interest. The lender's monthly payment table never tells you that, because they make their money from the years you do not pay early.
Question 3: how does this change with rate?
Last question, also the simplest. Loop the rate:
for (const rate of [0.05, 0.055, 0.06, 0.065, 0.07]) {
const { payment } = amortize({ principal: 300000, annualRate: rate, years: 30 });
const totalInterest = payment * 360 - 300000;
console.log({ rate, payment: payment.toFixed(2), totalInterest: totalInterest.toFixed(0) });
}
The gap between 5% and 7% on the same $300k principal is well over $130,000 in lifetime interest. That gap is bigger than a college tuition. It is the most important number in the conversation, and yet most rate shopping happens "feel-based."
For a quick refresher on the simpler version of this without the amortization plumbing, the Loan Calculator on Equation Solver is the one I open when somebody asks about a personal loan, a student loan, or any non-mortgage installment debt.
Bonus: comparing payoff strategies
If you are juggling multiple debts, the same code generalizes nicely. Avalanche (highest rate first) versus snowball (smallest balance first) is just a matter of which loan gets the extra payment each month. I will not paste the snippet here, but it is roughly the same loop wrapped in a debts.map.
The takeaway is mundane and unsexy: write the model, run the model, then sign the paperwork. Loans are the kind of long-tail decision where a thirty-line simulator can pay you back a thousand times over.
Top comments (0)