Money is a Long, and Double fails my build
0.1 + 0.2 != 0.3. In a finance app that is not a curiosity, it is a bug you ship and find months later.
Compiled from the repository’s decision records and changelog rather than written as a first-person account. The reasoning is the project’s; the summarising is editorial. The record itself is linked at the end.
0.1 + 0.2 does not equal 0.3 in binary floating point. That is a party trick until the number is a rupee balance, and then it is a bug that ships quietly. In Rekhon, money is a Long count of paise inside a Money value class. This entry is about the part that is easy to skip: deciding exactly what the build should refuse.
A rule nobody enforced
The ban on Double for money was already written in the project's CLAUDE.md, next to three others: no wall-clock reads in domain code, no GlobalScope, no hardcoded user-visible strings. A governance audit on 25 July 2026 found that none of them was enforced by anything. There was no lint.xml and no custom lint module, and lintDebug ran with the Android Gradle Plugin's defaults, which know nothing about this project.
The audit called that its systemic finding. A rule documented as a gate but implemented as nothing gives false confidence to human reviewers and to AI agents alike, because both treat CLAUDE.md as ground truth.
What counts as money
The detector cannot ask the type system whether a Double is money. If the type could say so, Money would not need to exist. So it checks two things together. The declared type is floating point (Double, Float or BigDecimal), and the name contains a monetary word. Both have to be true.
- val amountPaise: Double fails. Floating point, and the name holds a money word.
- val emiPrice: BigDecimal fails too. BigDecimal is in the floating-point set, so an exact decimal type still trips the rule when the name is monetary.
- val scrollFraction: Double passes. Floating point, but nothing in the name is money.
- val amountPaise: Long passes. That is the representation the codebase wants.
Names are split into whole camelCase words before matching, never searched as substrings, so in the ADR's own example expand does not match pan. The money words live in MoneyDoubleDetector.MONEY_WORDS: amount, price, balance, cost, paise, rupee, money, fee, salary, income, expense, spend, budget, emi, premium, minor, payment, principal, interest, cashflow and networth, with their plurals.
What it lets through on purpose
The heuristic will miss money kept under a bad name, and it can flag a Double that only sounds monetary. The ADR accepts both. A rule that blocks valid work gets disabled, and a disabled rule protects nothing. For a genuine false positive there are two ways out: rename the variable, which is usually the right fix anyway, or @Suppress it with a comment saying why.
Two stricter designs were turned down. Flagging every Double would block the floating point the app legitimately needs for animation fractions, ratios and progress bars.
A blanket ban is the rule that gets turned off in week two.
An @MonetaryAmount annotation was rejected as self-defeating. It only fires when a developer remembers to annotate, which is the exact moment they would also have remembered to use Money.
Making sure it actually runs
Two details decide whether the rule fires at all. Lint does not run on a pure-Kotlin module unless com.android.lint is applied to it, and :core:model, where Money itself lives, is one of those. Without that one line it would have been the only module the money rule never checked.
The other is version drift. lintVersion in the catalog has to move in lockstep with the Android Gradle Plugin, lint version being the AGP version plus 23, or the detectors compile against a different API from the one that runs them.
The proof that it worked was never the detector file existing. Each rule was checked by seeding a real violation in a real module and watching ./gradlew lint go red.