Apex Decimal Arithmetic Precision
Activate when an Apex calculation produces a value that is wrong by a cent, by a fraction, or — more dangerously — looks correct in unit tests but disagrees with a hand-totalled spreadsheet by enough to fail an audit. The skill resolves the calculation by giving Decimal an explicit scale and rounding mode at every step where rounding can occur, separating the "calculate" path from the "store" path, and matching Apex's behavior to the platform's currency-field behavior.
Before Starting
Gather this context before working on anything in this domain:
- The calculation in plain language: what's the formula, and what is the expected output for one or two specific input rows. "Tax = subtotal × rate, rounded to 2 places, banker's rounding" is enough.
- The Decimal destination: a currency field with 2 decimal places, a percent field with 4, a number(18,6), or a transient variable. The destination dictates the final scale; intermediates should generally carry more.
- Whether any of the operands could be a literal
1 or 100 (Integer) rather than 1.0 or 100.0 (Decimal). This is the most common source of "the result truncated to 0" reports.
Core Concepts
Apex Decimal is fixed-precision, scale-tracking
Apex's Decimal type is a fixed-point decimal — internally a Java BigDecimal. Every Decimal value carries a scale (number of digits after the decimal point). Scale is preserved through addition, subtraction, and multiplication. Division is the operation where Apex requires you to declare what scale the result should have, because most divisions don't terminate in finite digits.
A Decimal literal in Apex source picks up its scale from how you wrote it:
1.5 — scale 1
1.50 — scale 2
1 — Integer (no scale, not Decimal)
1.0 — Decimal, scale 1
Once the value is in a Decimal variable, scale is "sticky" across arithmetic in predictable ways:
Decimal a = 1.50; Decimal b = 1.5; Decimal c = a + b; → c is 3.00 (scale 2, the larger of the two)
Decimal d = 1.25 * 1.0; → d is 1.250 (scale = sum of operand scales for multiply)
Integer e = 10 / 4; → e == 2 (Integer / Integer truncates). The compiler does not warn.
Division requires divide(divisor, scale, roundingMode) for non-terminating results
Decimal.divide(Decimal divisor) (single-argument) throws System.MathException: Division does not result in an exact result, set scale and rounding mode for an inexact result when the result has more digits than the result's natural scale can hold. 1.0 / 3 blows up; 1.0 / 4 == 0.25 works.
Always prefer the three-argument form for any production calculation:
Decimal result = numerator.divide(denominator, 4, System.RoundingMode.HALF_UP);
The two-argument divide(divisor, scale) exists but uses HALF_EVEN (banker's rounding) silently — fine if that's what you want, dangerous if you assumed HALF_UP.
Rounding modes that matter
| Mode |
Behavior |
Use for |
HALF_UP |
0.5 rounds away from zero |
Retail prices, most user-facing money |
HALF_EVEN (banker's) |
0.5 rounds to even neighbor |
Tax, GAAP/IFRS reporting, statistics |
HALF_DOWN |
0.5 rounds toward zero |
Rare; specific contractual rounding |
UP / DOWN |
Always away / toward zero |
Truncation; commission caps |
CEILING / FLOOR |
Always up / down (signed) |
Inventory ceiling, never-overcharge |
UNNECESSARY |
Throws if rounding actually needed |
Use to prove no rounding loss occurred |
The platform's currency-field UI uses HALF_EVEN for display rounding. If your Apex uses HALF_UP and writes to a Currency field, the value persists as you wrote it but a downstream formula that re-rounds via the platform may shift it back. Match the rounding mode end-to-end or accept the difference.
Scale of currency, percent, and number fields
- Standard
Currency fields: scale 2 (or more in multi-currency orgs after Dated Exchange Rate conversion — the stored value retains source scale).
- Custom
Number(length, decimal): scale = decimal.
- Custom
Percent(length, decimal): scale = decimal. Stored as the displayed value (5.25% → 5.25, not 0.0525).
When you write a higher-scale Decimal into a field with smaller scale, the platform truncates without an explicit rounding mode at the DML boundary. To control rounding, call setScale(fieldScale, RoundingMode.X) before assignment.
Common Patterns
Calculating a line-item total with tax
public class LineItemCalc {
public static Decimal totalWithTax(Decimal unitPrice, Integer qty, Decimal taxRatePercent) {
// Force Decimal arithmetic — qty alone is Integer.
Decimal subtotal = unitPrice * qty;
Decimal tax = (subtotal * taxRatePercent).divide(100, 6, System.RoundingMode.HALF_EVEN);
Decimal total = (subtotal + tax).setScale(2, System.RoundingMode.HALF_EVEN);
return total;
}
}
Notes: intermediate tax carries scale 6 to avoid double-rounding; the outer setScale(2, HALF_EVEN) aligns to the currency-field display behavior; qty (Integer) is implicitly promoted by unitPrice * qty because unitPrice is already Decimal.
Splitting a total across allocations without rounding drift
The classic trap: total / N rounded to 2 places, then summed back, drifts by 0.01 × (rounding-direction × N). Fix by allocating cumulatively and assigning the residual to the last item:
public static List<Decimal> allocate(Decimal total, Integer parts) {
List<Decimal> out = new List<Decimal>();
Decimal each = total.divide(parts, 2, System.RoundingMode.HALF_DOWN);
Decimal running = 0;
for (Integer i = 0; i < parts - 1; i++) {
out.add(each);
running += each;
}
out.add((total - running).setScale(2, System.RoundingMode.HALF_UP));
return out;
}
The list always sums exactly to total. Useful for revenue allocation, invoice splits, and percentage-of-total reporting.
Decision Guidance
| Situation |
Choice |
Rationale |
| Storing into a currency field |
setScale(2, HALF_EVEN) before DML |
Matches platform display rounding |
| User-visible price (retail) |
setScale(2, HALF_UP) |
Matches consumer expectation |
| Tax calculation |
HALF_EVEN |
Required by most tax authorities; defensible in audit |
| Cumulative average / weighted avg |
Carry scale 6+ in intermediates, round at end |
Avoid double-rounding error compounding |
1 / 3-style division |
Three-arg divide(d, scale, mode) |
One-arg form throws on non-terminating |
| Equality compare |
Round both sides to same scale first |
1.50 == 1.5 is true in Apex but Decimal.valueOf('1.50').equals(1.5) is false |
Recommended Workflow
- Identify every division in the calculation. For each, decide the result scale and rounding mode based on the destination field — write them down before writing code.
- Identify every Integer literal (
1, 100, qty) being divided or multiplied. If the operation could produce a fraction, change the literal to a Decimal (1.0, 100.0) or wrap with Decimal.valueOf().
- Carry intermediates at scale ≥ 4 even when the final answer is scale 2. Round only at the boundary (UI display, DML write, contract output).
- Replace any one-argument
divide(d) with the three-argument form. Search the codebase for \.divide\([^,]+\) to find single-arg divides.
- Add tests for the boundary cases: zero divisor (expect
MathException), exact-terminating divide (1.00 / 4), repeating divide (1.00 / 3), half-way value with both HALF_UP and HALF_EVEN to confirm the chosen mode.
- If the calculation feeds a Currency field, write a final
setScale(2, HALF_EVEN) and confirm the stored value matches a hand calculation.
- Where the calculation is reused, lift it into a
LineItemCalc-style stateless utility class with a single rounding-mode parameter so the policy is one place, not seven.
Review Checklist
- Every
divide() call uses the three-argument form (or there is an inline comment explaining why one-arg is safe — e.g. divisor is a literal power of 2).
- Every Integer-typed operand in a fraction-producing operation is either deliberately Integer (count, index) or has been promoted to Decimal.
- Final scale matches the destination field's scale; intermediate scale is at least 2 higher than final.
- The chosen rounding mode is
HALF_EVEN for tax/financial reporting, HALF_UP for consumer-visible prices, or has a comment explaining the deviation.
- Splitting / allocation logic uses the residual-on-last-item pattern; tests assert sum-equals-total to the cent.
- Equality comparisons round both sides to the same scale before comparing, OR use
== (value-equal) deliberately rather than .equals() (scale-equal).
Salesforce-Specific Gotchas
| Gotcha |
Behavior |
| Currency fields in multi-currency orgs |
Decimal reads from a Currency field carry the record's currency, not the user's. Apex doesn't auto-convert; use UserInfo.getDefaultCurrency() and the DatedConversionRate SObject if conversion is needed. |
Decimal.valueOf(Double) |
Passes through Java double's binary float, reintroducing 0.1 + 0.2 = 0.30000000000000004 errors. Use Decimal.valueOf(String) or a Decimal literal. |
| Aggregate query types |
SUM(Amount__c) returns Decimal in Apex but AVG() always returns scale 6 regardless of source field. Round explicitly when consuming. |
| Formula-field evaluation |
Cross-object formulas evaluate at platform-level with HALF_EVEN; if Apex pre-computed the same value with HALF_UP, expect a 1-cent shift. |
== vs .equals() |
Apex == on Decimal is value-equal (1.50 == 1.5 is true); .equals() is scale-strict. Use == for "same number" semantics. |
Output Artifacts
- Corrected Apex utility class with explicit
setScale(scale, RoundingMode) calls at every rounding point.
- Apex test class with parameterized test for: terminating divide, repeating divide, half-way value with chosen mode, zero divisor, large-N allocation summing exactly to total.
- One-paragraph design note: "this calculation uses HALF_EVEN at scale 2 to match the platform's currency-field display behavior" — kept with the class so the next developer doesn't switch modes accidentally.
Related Skills
apex/apex-aggregate-queries — SUM and AVG return type quirks.
data/multi-currency-and-advanced-currency-management — Dated Exchange Rates and per-record currency.
apex/fsc-financial-calculations — domain-specific (TWR/IRR) financial math built on top of the Decimal primitives covered here.
flow/flow-formula-and-expression-patterns — Flow's formula engine has its own rounding rules; the gap between Flow and Apex causes the most "Apex says X, Flow says Y" tickets.
1---2name: apex-decimal-arithmetic-precision3description: Use when Apex code performs arithmetic on currency, tax, percentage, or quantity values and the result is wrong by a cent, a penny, or a fraction — Decimal scale collapses to 0 after divide, totals don't match the sum-of-line-items, currency-field rounding differs from in-app calculation, or `divide()` throws on a non-existent rounding mode. Triggers: 'apex decimal divide rounded wrong', 'currency calculation off by a penny', 'apex setScale rounding mode', 'integer divided by integer truncated to zero', 'apex Decimal precision lost'. NOT for dated exchange rates — use data/multi-currency-and-advanced-currency-management. NOT for Flow formula rounding — use flow/flow-formula-and-expression-patterns. NOT for SOQL aggregate queries — use apex/apex-aggregate-queries.4---56# Apex Decimal Arithmetic Precision78Activate when an Apex calculation produces a value that is wrong by a cent, by a fraction, or — more dangerously — looks correct in unit tests but disagrees with a hand-totalled spreadsheet by enough to fail an audit. The skill resolves the calculation by giving Decimal an explicit scale and rounding mode at every step where rounding can occur, separating the "calculate" path from the "store" path, and matching Apex's behavior to the platform's currency-field behavior.910---1112## Before Starting1314Gather this context before working on anything in this domain:1516- The calculation in plain language: what's the formula, and what is the expected output for one or two specific input rows. "Tax = subtotal × rate, rounded to 2 places, banker's rounding" is enough.17- The Decimal *destination*: a currency field with 2 decimal places, a percent field with 4, a number(18,6), or a transient variable. The destination dictates the final scale; intermediates should generally carry more.18- Whether any of the operands could be a literal `1` or `100` (Integer) rather than `1.0` or `100.0` (Decimal). This is the most common source of "the result truncated to 0" reports.1920---2122## Core Concepts2324### Apex Decimal is fixed-precision, scale-tracking2526Apex's `Decimal` type is a fixed-point decimal — internally a Java `BigDecimal`. Every Decimal value carries a *scale* (number of digits after the decimal point). Scale is preserved through addition, subtraction, and multiplication. Division is the operation where Apex *requires* you to declare what scale the result should have, because most divisions don't terminate in finite digits.2728A Decimal literal in Apex source picks up its scale from how you wrote it:2930- `1.5` — scale 131- `1.50` — scale 232- `1` — Integer (no scale, not Decimal)33- `1.0` — Decimal, scale 13435Once the value is in a Decimal variable, scale is "sticky" across arithmetic in predictable ways:3637- `Decimal a = 1.50; Decimal b = 1.5; Decimal c = a + b;` → `c` is `3.00` (scale 2, the larger of the two)38- `Decimal d = 1.25 * 1.0;` → `d` is `1.250` (scale = sum of operand scales for multiply)39- `Integer e = 10 / 4;` → `e == 2` (Integer / Integer truncates). The compiler does not warn.4041### Division requires `divide(divisor, scale, roundingMode)` for non-terminating results4243`Decimal.divide(Decimal divisor)` (single-argument) throws `System.MathException: Division does not result in an exact result, set scale and rounding mode for an inexact result` when the result has more digits than the result's natural scale can hold. `1.0 / 3` blows up; `1.0 / 4 == 0.25` works.4445Always prefer the three-argument form for any production calculation:4647```apex48Decimal result = numerator.divide(denominator, 4, System.RoundingMode.HALF_UP);49```5051The two-argument `divide(divisor, scale)` exists but uses `HALF_EVEN` (banker's rounding) silently — fine if that's what you want, dangerous if you assumed `HALF_UP`.5253### Rounding modes that matter5455| Mode | Behavior | Use for |56|---|---|---|57| `HALF_UP` | 0.5 rounds away from zero | Retail prices, most user-facing money |58| `HALF_EVEN` (banker's) | 0.5 rounds to even neighbor | Tax, GAAP/IFRS reporting, statistics |59| `HALF_DOWN` | 0.5 rounds toward zero | Rare; specific contractual rounding |60| `UP` / `DOWN` | Always away / toward zero | Truncation; commission caps |61| `CEILING` / `FLOOR` | Always up / down (signed) | Inventory ceiling, never-overcharge |62| `UNNECESSARY` | Throws if rounding actually needed | Use to prove no rounding loss occurred |6364The platform's currency-field UI uses `HALF_EVEN` for display rounding. If your Apex uses `HALF_UP` and writes to a Currency field, the value persists as you wrote it but a downstream formula that re-rounds via the platform may shift it back. Match the rounding mode end-to-end or accept the difference.6566### Scale of currency, percent, and number fields6768- Standard `Currency` fields: scale 2 (or more in multi-currency orgs after Dated Exchange Rate conversion — the stored value retains source scale).69- Custom `Number(length, decimal)`: scale = `decimal`.70- Custom `Percent(length, decimal)`: scale = `decimal`. Stored as the displayed value (5.25% → `5.25`, not `0.0525`).7172When you write a higher-scale Decimal into a field with smaller scale, the platform truncates **without an explicit rounding mode** at the DML boundary. To control rounding, call `setScale(fieldScale, RoundingMode.X)` *before* assignment.7374---7576## Common Patterns7778### Calculating a line-item total with tax7980```apex81public class LineItemCalc {82 public static Decimal totalWithTax(Decimal unitPrice, Integer qty, Decimal taxRatePercent) {83 // Force Decimal arithmetic — qty alone is Integer.84 Decimal subtotal = unitPrice * qty;85 Decimal tax = (subtotal * taxRatePercent).divide(100, 6, System.RoundingMode.HALF_EVEN);86 Decimal total = (subtotal + tax).setScale(2, System.RoundingMode.HALF_EVEN);87 return total;88 }89}90```9192Notes: intermediate `tax` carries scale 6 to avoid double-rounding; the outer `setScale(2, HALF_EVEN)` aligns to the currency-field display behavior; `qty` (Integer) is implicitly promoted by `unitPrice * qty` because `unitPrice` is already Decimal.9394### Splitting a total across allocations without rounding drift9596The classic trap: `total / N` rounded to 2 places, then summed back, drifts by `0.01 × (rounding-direction × N)`. Fix by allocating cumulatively and assigning the residual to the last item:9798```apex99public static List<Decimal> allocate(Decimal total, Integer parts) {100 List<Decimal> out = new List<Decimal>();101 Decimal each = total.divide(parts, 2, System.RoundingMode.HALF_DOWN);102 Decimal running = 0;103 for (Integer i = 0; i < parts - 1; i++) {104 out.add(each);105 running += each;106 }107 out.add((total - running).setScale(2, System.RoundingMode.HALF_UP));108 return out;109}110```111112The list always sums *exactly* to `total`. Useful for revenue allocation, invoice splits, and percentage-of-total reporting.113114---115116## Decision Guidance117118| Situation | Choice | Rationale |119|---|---|---|120| Storing into a currency field | `setScale(2, HALF_EVEN)` before DML | Matches platform display rounding |121| User-visible price (retail) | `setScale(2, HALF_UP)` | Matches consumer expectation |122| Tax calculation | `HALF_EVEN` | Required by most tax authorities; defensible in audit |123| Cumulative average / weighted avg | Carry scale 6+ in intermediates, round at end | Avoid double-rounding error compounding |124| `1 / 3`-style division | Three-arg `divide(d, scale, mode)` | One-arg form throws on non-terminating |125| Equality compare | Round both sides to same scale first | `1.50 == 1.5` is **true** in Apex but `Decimal.valueOf('1.50').equals(1.5)` is **false** |126127---128129## Recommended Workflow1301311. Identify every division in the calculation. For each, decide the result scale and rounding mode based on the destination field — write them down before writing code.1322. Identify every Integer literal (`1`, `100`, `qty`) being divided or multiplied. If the operation could produce a fraction, change the literal to a Decimal (`1.0`, `100.0`) or wrap with `Decimal.valueOf()`.1333. Carry intermediates at scale ≥ 4 even when the final answer is scale 2. Round only at the boundary (UI display, DML write, contract output).1344. Replace any one-argument `divide(d)` with the three-argument form. Search the codebase for `\.divide\([^,]+\)` to find single-arg divides.1355. Add tests for the boundary cases: zero divisor (expect `MathException`), exact-terminating divide (`1.00 / 4`), repeating divide (`1.00 / 3`), half-way value with both `HALF_UP` and `HALF_EVEN` to confirm the chosen mode.1366. If the calculation feeds a Currency field, write a final `setScale(2, HALF_EVEN)` and confirm the stored value matches a hand calculation.1377. Where the calculation is reused, lift it into a `LineItemCalc`-style stateless utility class with a single rounding-mode parameter so the policy is one place, not seven.138139---140141## Review Checklist142143- Every `divide()` call uses the three-argument form (or there is an inline comment explaining why one-arg is safe — e.g. divisor is a literal power of 2).144- Every Integer-typed operand in a fraction-producing operation is either deliberately Integer (count, index) or has been promoted to Decimal.145- Final scale matches the destination field's scale; intermediate scale is at least 2 higher than final.146- The chosen rounding mode is `HALF_EVEN` for tax/financial reporting, `HALF_UP` for consumer-visible prices, or has a comment explaining the deviation.147- Splitting / allocation logic uses the residual-on-last-item pattern; tests assert sum-equals-total to the cent.148- Equality comparisons round both sides to the same scale before comparing, OR use `==` (value-equal) deliberately rather than `.equals()` (scale-equal).149150---151152## Salesforce-Specific Gotchas153154| Gotcha | Behavior |155|---|---|156| Currency fields in multi-currency orgs | `Decimal` reads from a Currency field carry the *record's* currency, not the user's. Apex doesn't auto-convert; use `UserInfo.getDefaultCurrency()` and the `DatedConversionRate` SObject if conversion is needed. |157| `Decimal.valueOf(Double)` | Passes through Java double's binary float, reintroducing `0.1 + 0.2 = 0.30000000000000004` errors. Use `Decimal.valueOf(String)` or a Decimal literal. |158| Aggregate query types | `SUM(Amount__c)` returns Decimal in Apex but `AVG()` always returns scale 6 regardless of source field. Round explicitly when consuming. |159| Formula-field evaluation | Cross-object formulas evaluate at platform-level with `HALF_EVEN`; if Apex pre-computed the same value with `HALF_UP`, expect a 1-cent shift. |160| `==` vs `.equals()` | Apex `==` on Decimal is value-equal (`1.50 == 1.5` is `true`); `.equals()` is scale-strict. Use `==` for "same number" semantics. |161162---163164## Output Artifacts165166- Corrected Apex utility class with explicit `setScale(scale, RoundingMode)` calls at every rounding point.167- Apex test class with parameterized test for: terminating divide, repeating divide, half-way value with chosen mode, zero divisor, large-N allocation summing exactly to total.168- One-paragraph design note: "this calculation uses HALF_EVEN at scale 2 to match the platform's currency-field display behavior" — kept with the class so the next developer doesn't switch modes accidentally.169170---171172## Related Skills173174- `apex/apex-aggregate-queries` — `SUM` and `AVG` return type quirks.175- `data/multi-currency-and-advanced-currency-management` — Dated Exchange Rates and per-record currency.176- `apex/fsc-financial-calculations` — domain-specific (TWR/IRR) financial math built on top of the Decimal primitives covered here.177- `flow/flow-formula-and-expression-patterns` — Flow's formula engine has its own rounding rules; the gap between Flow and Apex causes the most "Apex says X, Flow says Y" tickets.