Cloud
I Rejected My Reference Tools' Assumptions, Then Inherited One
Seihyung Oh Dev.to (EN Zone)
1 views
I built a loan calculator because the ones I was using assumed something wrong about how a borrowing decision happens. I caught that assumption early and designed against it.
Then I built extra-payment handling, tested it, and considered it done. It took running the same input through a second market's convention to see that I'd inherited a different assumption from the same tools — and encoded it as arithmetic.
The assumption I caught
Web calculators don't keep anything. Close the tab and the numbers are gone. Every time I wanted to check the same loan again, I typed it in from scratch. Most of them also asked for more than I needed — three fields filled in, a dozen skipped past.
Both follow from one assumption: that you calculate once, read the number, and leave.
That isn't how a loan decision works. You don't borrow from the first place you walk into. You check a few, narrow it down, and often go back to negotiate. You look again when the rate changes. The decision takes days and covers more than one lender. The tool assumed it took a minute and covered one.
So what I needed wasn't a more accurate calculator. It was one that expected to be opened again — that could save the conditions I entered, recalculate them when something changed, and hold more than one scenario side by side.
The existing tools weren't wrong. They were built for a different question than the one I was asking.
The assumption I didn't catch
Here's the case. $25,000 at 4.26% APR over 36 months. The monthly payment is $740.9946, which the app displays as $740.99 and carries at full precision. Left alone the loan costs $1,675.81 in interest. At month 24 the balance is $8,690.11, and the borrower pays down $5,000.
The schedule has to be rebuilt from there. I rebuilt it the way every calculator I'd used as a reference does: hold the payment, let the term contract. The loan clears in month 30 instead of 36, total interest drops to $1,513.63, and the result screen says saved $162.18, six months early.
Then I ran it under a different convention, where the maturity date holds and the payment is recomputed instead. Same loan, same $5,000, same month. The loan still ends in month 36. The payment falls to $314.65. Total interest $1,559.68.
The interest figures are $46 apart, which is not interesting. What's interesting is that I had nowhere to put the second result.
Nowhere to put it
struct PrepaymentResult {
let monthsSaved: Int
let interestSaved: Decimal
}
The second convention saves zero months. Its entire benefit is $426.34 a month for twelve months, and this model has no field for that — not because I'd rejected it, but because the question "how much does the monthly payment change" had never come up while I was building against references that couldn't ask it.
The fee was the same mistake
I'd modeled the prepayment fee as rate × amount. But a fee prorated by remaining term — twelve months left of thirty-six — computes as 5,000 × 2% × (12 ÷ 36) = $33.33, where the flat version on identical inputs is $100. One rate field, two formulas, triple the difference.
Hold the payment
Hold the maturity date
Loan ends
month 30
month 36
Monthly payment
$740.99
$314.65
Total interest
$1,513.63
$1,559.68
Interest saved
$162.18
$116.13
Prepayment fee
$100.00
$33.33
Net saved
$62.18
$82.80
Net of fees, the option that saved $46 less in interest keeps $20 more. The ranking my result screen produced was an artifact of a fee formula I'd hardcoded without ever deciding to.
How the figures were derived
Monthly rate is APR ÷ 12 = 0.00355. Interest accrues on the outstanding balance each period; the balance is carried at full precision and rounded only for display, which is why the month-24 balance reads $8,690.11 against the rounded payment rather than $8,690.21.
Term-shortening solves for the number of remaining periods at the original payment. Payment-reduction solves the annuity formula again over the twelve periods that remain, against the post-prepayment balance of $3,690.11.
A constant, or a choice
None of this is a story about one market being unusual. It's a story about the difference between a domain constant and a domain choice, and how easily the second disguises itself as the first.
I hadn't chosen term-shortening. I'd inherited it, from tools that were all built for the same market, and then encoded it as how amortization works. The tell is that it never appeared as a parameter anywhere — not in a settings screen, not in a function signature, not in a comment. Things you decide leave marks. Things you absorb don't.
enum PrepaymentMode {
case shortenTerm // hold the payment, contract the schedule
case reducePayment // hold the maturity date, recompute the payment
}
struct PrepaymentResult {
let mode: PrepaymentMode
let interestSaved: Decimal
let monthsSaved: Int // zero under .reducePayment
let paymentReduction: Decimal // zero under .shortenTerm
}
The fix was small — a setting, and two code paths behind it. Finding it wasn't.
Next: a 30-year mortgage, where the same two answers are ten years apart.
Read original: https://dev.to/c00012/i-rejected-my-reference-tools-assumptions-then-inherited-one-g3p
← Previous
Why JavaScript Text-to-Binary Snippets Break on Emoji
Next →
Most of Your Support Tickets Are One Question You Never Answered
Related
E
Everything except the server: what it costs to run one WordPress site for a year
Cloud
0
DEV Community
D
Day 1 — ইন্টারনেট আসলে কীভাবে কাজ করে
Cloud
0
DEV Community
C
Common AWS Free Tier Mistakes Beginners Make
Cloud
0
DEV Community
C
Consuming AWS MSK from Azure Databricks over mTLS
Cloud
1
DEV Community
Comments0
No comments yet — be the first