DEV Community

levent çelik
levent çelik

Posted on

Why I always model loans before signing them: a 30-line debt simulator

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 };
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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) });
}
Enter fullscreen mode Exit fullscreen mode

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)