DEV Community

Michael Lip
Michael Lip

Posted on • Originally published at zovo.one

The Amortization Formula Every Developer Should Know

I have built loan calculators for three different fintech products. Each time, the product team assumed the monthly payment calculation was straightforward. Each time, edge cases in the amortization formula caused bugs that took days to trace. Here is the math, the implementation, and the traps.

The standard amortization formula

The fixed monthly payment for a fully amortizing loan:

M = P * [r(1+r)^n] / [(1+r)^n - 1]
Enter fullscreen mode Exit fullscreen mode

Where:

  • M = monthly payment
  • P = principal (loan amount)
  • r = monthly interest rate (annual rate / 12)
  • n = total number of payments

In JavaScript:

function monthlyPayment(principal, annualRate, years) {
  const r = annualRate / 100 / 12;
  const n = years * 12;

  if (r === 0) return principal / n;

  return principal * (r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);
}

// $250,000 loan at 6.5% for 30 years
monthlyPayment(250000, 6.5, 30); // $1,580.17
Enter fullscreen mode Exit fullscreen mode

That zero-rate check on line 4 is not theoretical. Promotional 0% financing exists, and without that guard, your formula divides by zero.

Building an amortization schedule

The monthly payment stays fixed, but the split between principal and interest changes every month. Early payments are mostly interest. Late payments are mostly principal.

function amortizationSchedule(principal, annualRate, years) {
  const r = annualRate / 100 / 12;
  const n = years * 12;
  const payment = monthlyPayment(principal, annualRate, years);

  let balance = principal;
  const schedule = [];

  for (let month = 1; month <= n; month++) {
    const interest = balance * r;
    const principalPaid = payment - interest;
    balance -= principalPaid;

    schedule.push({
      month,
      payment: payment.toFixed(2),
      principal: principalPaid.toFixed(2),
      interest: interest.toFixed(2),
      balance: Math.max(0, balance).toFixed(2)
    });
  }

  return schedule;
}
Enter fullscreen mode Exit fullscreen mode

The Math.max(0, balance) on the last line handles a floating-point edge case. After 360 payments, the balance should be exactly zero, but accumulated rounding errors might make it -0.0000001. Displaying a negative balance to a user is a bug.

The rounding problem

In production financial software, you cannot simply round at the end. Each payment must be rounded to the penny, and the rounding errors must be tracked and corrected.

The standard approach: calculate the exact monthly payment, round it up to the nearest cent, then make the last payment slightly different to zero out the balance exactly.

const exactPayment = monthlyPayment(250000, 6.5, 30);
const roundedPayment = Math.ceil(exactPayment * 100) / 100;
// Last payment = remaining balance + last month's interest
Enter fullscreen mode Exit fullscreen mode

This is why your last mortgage payment is often a few cents or dollars different from every other payment. It is not an error. It is the rounding correction.

Total interest paid

The total cost of a loan is eye-opening:

function totalInterest(principal, annualRate, years) {
  const payment = monthlyPayment(principal, annualRate, years);
  const totalPaid = payment * years * 12;
  return totalPaid - principal;
}

totalInterest(250000, 6.5, 30); // $318,861
Enter fullscreen mode Exit fullscreen mode

On a $250,000 loan at 6.5% over 30 years, you pay $318,861 in interest. The total cost is $568,861. You pay more in interest than the original loan amount. This is not unusual for 30-year mortgages at moderate rates.

The impact of extra payments

Adding even a small extra payment each month has an outsized effect because it reduces the principal that accrues interest for every remaining month.

An extra $100/month on the loan above:

function payoffWithExtra(principal, annualRate, years, extraMonthly) {
  const r = annualRate / 100 / 12;
  const basePayment = monthlyPayment(principal, annualRate, years);
  const totalPayment = basePayment + extraMonthly;

  let balance = principal;
  let months = 0;
  let totalInterestPaid = 0;

  while (balance > 0) {
    const interest = balance * r;
    totalInterestPaid += interest;
    const principalPaid = Math.min(totalPayment - interest, balance);
    balance -= principalPaid;
    months++;
  }

  return { months, totalInterest: totalInterestPaid };
}

payoffWithExtra(250000, 6.5, 30, 100);
// { months: 313, totalInterest: ~$271,000 }
Enter fullscreen mode Exit fullscreen mode

An extra $100/month saves approximately $47,000 in interest and pays off the loan 47 months (nearly 4 years) early. That is a remarkable return on $100/month.

Comparing loan offers

When comparing loans, monthly payment alone is misleading. A longer term has a lower monthly payment but higher total cost. The metrics that matter:

  1. APR (includes fees in the effective rate)
  2. Total interest paid over the life of the loan
  3. Total cost (principal + interest + fees)
  4. Monthly payment (for budgeting, not for comparison)

I keep a loan calculator at zovo.one/free-tools/loan-calculator that shows all four metrics for any loan configuration. It also generates a full amortization schedule so you can see exactly how each payment splits between principal and interest. Understanding this math is the difference between choosing a loan rationally and choosing one by monthly payment alone.


I'm Michael Lip. I build free developer tools at zovo.one. 500+ tools, all private, all free.

Top comments (0)