Every team I have worked on has eventually shipped a finance feature that involved projecting a balance forward in time. Every single one of those features also produced, at some point, an answer that did not match the number the finance reviewer had on their spreadsheet. The bug is almost never the math itself — the math is a one-liner in any language. The bug is almost always the model: which compounding period you used, which day count convention you assumed, which month the first deposit lands in, and whether "annual rate of 5%" really means what your code thinks it means.
This article is a checklist-driven walk through the engineering traps that show up the moment compound interest leaves the textbook and enters a product. It is not a tutorial on the formula, and it is not a pitch for any one tool. It is the post I wish I had been handed before my first pull request on a savings projection endpoint was bounced back with the comment "your number is $47 off for a 30-year horizon."
If you want a clean walkthrough of the underlying math (including how CD math differs from savings-account math), the step-by-step guide on calculating compound interest on a CD is a good reference. For the rest of this article, I assume you know the formula and want to make sure your implementation is right.
The Five Things Your Formula Knows That Your Code Probably Doesn't
The headline formula — A = P * (1 + r/n)^(n*t) — is famously short. The five assumptions baked into it are not obvious until one of them is wrong:
-
The rate is a nominal annual rate, and
nis the compounding frequency. If the marketing copy says "5% APY" and your code applies 5% asr, you have quietly doubled the effective rate on a daily-compounding product. APY already includes compounding; APR-style rates do not. The Wikipedia entry on annual percentage rate is a stable summary of the distinction. -
tis measured in the same unit asn. Daily compounding over 30 years isn = 365,t = 30. Daily compounding over 30 years and 47 days is not — you have to either truncate to whole periods or model the tail separately. - Contributions land at the end of each period. A deposit on day 1 of month 1 does not earn interest for month 1 in a standard end-of-period model. If your UI says "deposit today and start earning" but your backend uses an ordinary annuity assumption, the user will be confused.
-
Fractional periods exist and they matter. A 30-year CD with a 7-year term has 23 whole compounding periods plus a stub. Naively multiplying
t = 30blows past the maturity event. - Taxes and fees are not in the formula. "What will I have?" and "what will I have after the IRS takes its cut?" are different questions, and conflating them is the single most common source of reviewer pushback.
A Debugging Workflow You Can Actually Run
When the number on screen disagrees with the number in the spreadsheet, work through this list in order. Skipping ahead is how teams burn an afternoon.
- Reproduce with the smallest possible input. Three deposits, one compounding frequency, one term. If it is wrong at that size, the formula is wrong and the model is irrelevant.
- Compare against a known-good reference. A trusted external calculator run with the same five inputs is the fastest way to localize the bug. This is the point at which a purpose-built tool earns its keep — you are not using it because you cannot do the math, you are using it as an oracle to diff against.
- Log every input and every derived value. Not just the inputs the user typed, but the nominal rate, the effective rate, the period count, the stub period, and the contribution timing. If your log line for a 30-year projection does not include a period count, you cannot tell whether you have a formula bug or a units bug.
- Round at the end, not at every step. Rounding the balance to two decimals after every compounding iteration is a classic source of penny drift over long horizons. Accumulate in a high-precision type and format only on display.
- Test the boundary cases explicitly. Zero rate, one period, deposit on the last day of the term, leap-day start date, negative nominal rate during a promotional intro period. Each one has shipped broken somewhere.
The Bugs I Have Personally Seen in Production
Naming no companies, here are the patterns. Treat them as a checklist when reviewing any new savings-projection code.
-
APY-as-APR double compounding. The product page says "5.00% APY, compounded daily." The code uses
r = 0.05andn = 365, which produces an effective rate of about 5.13%. Users see numbers slightly higher than the marketing promised, and legal gets a letter. - Day-count drift on monthly contributions. The code divides the annual rate by 12 and assumes every month is the same length. Over 360 months, that assumption is roughly accurate; over 30 years with a start date of January 31, it produces a handful of phantom late-month deposits.
- Stub-period loss. A 7-year CD inside a 30-year savings projection. The maturity event triggers at year 7, but the code happily compounds for another 23 years and reports a balance that does not exist in any real account.
- Annuity-due vs ordinary annuity confusion. "Deposit on the first of each month" sounds identical to "deposit at the start of each period," but the latter earns one extra period of interest and the difference over a 30-year horizon is material.
- Off-by-one on the contribution schedule. A "5-year, $100/month" plan that produces 60 contributions but models only 59 compounding events. The bug is invisible in the first year and embarrassing in year six.
The Production Trade-Offs Nobody Puts in the Spec
Once the math is correct, you still have to ship it. A few constraints that come up in code review:
Precision vs determinism. Double is fine for display; it is not fine when downstream systems compare two projections to decide whether a product is in scope for a regulator. For anything that is persisted, hashed, or compared, use a decimal type and document the rounding rule. The IEEE 754 summary on MDN's Number page is a fair starting point for understanding where Number quietly loses precision.
Latency vs coverage. A projection endpoint that recomputes the full schedule on every keystroke will be fine for a 30-year horizon and miserable for a 50-year horizon with monthly contributions and tax drag. Either precompute a lookup table at common horizons, or debounce the input on the client. Recomputing 600 monthly periods is not slow; recomputing 600 monthly periods inside a tax-drag loop with a per-bracket lookup, on every render, is.
Versioning the formula. The day a regulator changes the APY definition, or your team changes the contribution-timing assumption, you need to be able to show which version of the math produced which historical projection. Storing the model version alongside the result is boring and worth every line of code.
Time zones and calendars. If your product is used in more than one country, "30 years from today" depends on which calendar and which time zone you anchor to. For most retail finance, treating dates as civil dates in the user's locale is fine; for anything that crosses a daylight-saving boundary in the middle of a compounding period, decide explicitly.
A Pre-Ship Checklist for Savings Projection Code
Before the pull request goes up, the author should be able to tick every box:
- [ ] Nominal rate, effective rate, period count, and stub period are all logged.
- [ ] Contribution timing assumption is documented in code comments and surfaced in the UI.
- [ ] APY inputs are not re-compounded; APR inputs are.
- [ ] Maturity events truncate the schedule, not just the balance.
- [ ] Rounding happens once, at the display boundary.
- [ ] Boundary tests exist for zero rate, one period, and a leap-day start.
- [ ] The output of the code matches an independent oracle to the cent for at least one canonical test case.
- [ ] The model version is stored with every persisted projection.
If your team is missing one of those, the bug is not "if," it is "when."
Frequently asked questions
What is the single most common bug in compound interest code?
Treating an APY as if it were a nominal APR and then applying compounding on top of it. The result is a projected balance that is slightly higher than the product's marketed yield, which is both a regulatory and a trust problem.
How do I tell whether my off-by-one is a period bug or a calendar bug?
If the error grows linearly with the term, it is almost always a calendar bug — a stub period or a wrong month length. If the error grows roughly exponentially, it is a compounding-frequency bug, because compounding magnifies small rate errors over time.
Should I write my own projection engine or call a library?
For a single, well-defined formula, writing it yourself is fine and arguably preferable — you control the rounding, the timing assumptions, and the logging. For anything that involves tax drag, inflation adjustment, or multi-leg contributions, an audited library or a cross-checked external tool will save you a code-review cycle.
How often should I re-validate against an external reference?
Every time the formula changes, every time the inputs change shape (new contribution type, new compounding frequency), and at least once a quarter regardless. Models rot quietly; oracles do not.
This article was drafted with AI assistance and reviewed for technical accuracy before publishing.
Top comments (0)