Precision starts at input
If a contract supplies text "0.1", read it directly as BigDecimal when the domain requires the exact decimal. Converting to double before constructing BigDecimal preserves the binary approximation already introduced. Increasing precision after conversion does not recover lost information.
Also define accepted format. A decimal point in a technical contract does not mean a local comma or whitespace is automatically accepted. Validate application-appropriate magnitude and scale limits rather than assuming arbitrary precision means unlimited cost is acceptable. A numerically valid value may still fall outside the domain.
BigDecimal is immutable. Calling add or setScale returns a result that must be used. If amount.add(fee) is ignored, amount retains its previous value. When reviewing a calculation, trace both the value and the variable receiving it. This identifies control-flow mistakes that are not rounding errors.
Numeric value and representation
Texts "2.0" and "2.00" describe the same numeric value with different scales. compareTo answers numeric comparison; equals also considers representation. Before comparing results, decide which question matches the requirement. A value test should not fail solely over irrelevant scale, while a contract requiring two places must validate that scale.
This choice affects keys and sets. If the domain treats 2.0 and 2.00 as the same numeric identifier, do not casually insert both as keys without a coherent policy. Define intended equality and the representation used at the input boundary. Do not change every value’s scale merely to hide a poorly defined test.
Precision and scale also differ: limiting significant digits does not mean requiring a fixed number of decimal places. A useful specification states what to measure, when to round and how to compare results. Record those decisions beside the calculation to avoid differing interpretations in production, export and assessment.
Rounding is a domain decision
Exact division of one by three does not terminate in a finite decimal expansion. The exact divide overload throws ArithmeticException; the application must provide a policy permitting approximation. The API should not silently choose how many places the business accepts.
At a tie, HALF_EVEN selects the neighbor with an even last retained digit. To two places, 1.245 becomes 1.24 and 1.255 becomes 1.26. HALF_UP resolves ties away from zero; it does not mean always increasing the algebraic number. UNNECESSARY asserts that no information is lost and can fail when reducing scale would require rounding.
Calculation stage matters as much as mode. Three contributions of 0.045 sum exactly to 0.135. Rounding each before adding can produce 0.15; rounding only the total with HALF_UP produces 0.14. The case defines the required rule. Do not extrapolate this example into a universal tax, monetary or contractual rule.
Choose what time means
A birth date and a message timestamp do not need the same representation. LocalDate retains year, month and day without time or zone. Instant identifies a point in time. LocalDateTime describes local fields; reaching an instant requires knowing the applicable offset. ZonedDateTime associates those fields with zone rules and a resolved offset.
If a message already includes Z, retain that information when converting to Instant. Turning the timestamp into local fields and discarding its offset loses part of the contract. A screen may present one instant in several zones without creating several events. Conversely, two messages with the same local time can correspond to different instants.
The temporal types used here are immutable. day.plusDays(2) does not change day. When logic seems to ignore a date update, first check whether the result was retained. This avoids blaming zones or calendars for a reassignment problem entirely local to the code.
Missing intervals and repeated times
Zone rules can yield zero, one or two valid offsets for local date-time fields. When clocks advance from 02:00 to 03:00, 02:30 never occurs on that local timeline. ZonedDateTime.of default resolution moves the time by the gap length, to 03:30 here. If the domain requires rejecting nonexistent times, it must make that policy explicit.
When a time repeats, automatically choosing an offset produces a usable value but does not establish which occurrence generated a message. If 02:30 admits +02:00 and +01:00, candidates are 00:30Z and 01:30Z. Without offset, sequence or other evidence, ingestion cannot recover the original instant with certainty.
Adding one local day also differs from adding 24 elapsed hours. With a one-hour forward change between two noons, plusDays(1) retains noon after 23 hours; plusHours(24) reaches 13:00. Define whether the requirement is a calendar recurrence or a duration. Examples supply the necessary transitions and require no memorization of clock-change dates.
Boundaries and an explicit time source
A window including start and excluding end can be written as !t.isBefore(start) && t.isBefore(end). The next window can start exactly at end without counting the event twice. Conceptually check the start, end, one interior instant and both exteriors. Do not replace the condition with two strict comparisons when the start must be included.
Apply the same care to expiry: if the contract says expired at the boundary instant, now equal to the boundary must mean expired. That domain requirement should appear in the condition and examples. Avoid obtaining now repeatedly in one decision if the requirement intends a single reference point.
A Clock dependency permits real time in production and a fixed instant for local evaluation of the rule. Changing the JVM zone does not freeze time. Waiting a few milliseconds also does not guarantee a test sees the intended instant. Making the time source explicit reduces hidden dependencies without changing rule meaning.
Guided review: an acceptance window
The snippet accepts start and end as Instant plus a Clock dependency. Its contract requires non-null references and start before end. Interval validation prevents empty or reversed configuration from being treated as a normal window. The function reads the clock once and evaluates that instant against both boundaries.
For start=12:00Z and end=13:00Z, Clock.fixed at 12:00Z produces true; at 13:00Z it produces false. Between those limits the decision is true, and before the start or after the end it is false. No formatted-text comparison or display-zone dependency is involved.
The function only decides. It does not guarantee an external action finishes before end or another process uses the same configuration. If acceptance has effects, define when the decision applies and how temporal evidence relates to the outcome. Assessment asks for snippet interpretation and correct-condition selection without executing Java or adjusting clocks.
import java.time.Clock;
import java.time.Instant;
final class AcceptanceWindow {
// Non-null arguments; an inclusive start and exclusive end.
static boolean accepts(Instant start, Instant end, Clock clock) {
if (!start.isBefore(end)) {
throw new IllegalArgumentException("Invalid interval");
}
Instant now = Instant.now(clock);
return !now.isBefore(start) && now.isBefore(end);
}
}
Window [12:00Z, 13:00Z): accepts 12:00Z and rejects 13:00Z. The clock is read once; the function does not perform the accepted action.
Common pitfalls
Constructing decimals from unintended approximations; confusing scale with value; ignoring immutable results; inventing offsets; counting an endpoint in two windows.
Related topics: Types, conversions and validation · Collections, generics and keys · Streams, lambdas and absence · Concurrency, state and cancellation
Precision and time need explicit contracts: representation, rounding, zone, clock source and acceptance boundaries.
Reference: Java 25 API: BigDecimal · Java SE 25; no preview features