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]
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
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;
}
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
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
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 }
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:
- APR (includes fees in the effective rate)
- Total interest paid over the life of the loan
- Total cost (principal + interest + fees)
- 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)