TL;DR
- Prepare for Java interviews by implementing and explaining a small complete feature, including its input contract and failure cases.
- The order-total exercise below uses Java 21, records and BigDecimal, with explicit quantity, price and collection limits.
- Records do not automatically make referenced objects deeply immutable. Distinguish final fields, unmodifiable collections and ownership of mutable data.
- Test boundary values, nulls, decimal scale, input preservation and repeated calls. A correct total is one component of checkout, not proof of authorization or payment correctness.
Start Java interview preparation with a concrete contract
An interviewer asks you to calculate the subtotal of a shopping basket. Before choosing a stream or a loop, establish the rules. In this original exercise, the basket has at most 1,000 lines, each quantity is between 1 and 1,000, and each unit price is between zero and one million in a single currency with two decimal places.
Prices must already come from a trusted catalog or another authorized pricing source. This helper does not accept browser prices as trustworthy, apply tax, reserve inventory or charge a payment method. Repeated product codes are allowed because two basket lines may represent separate selections; whether to combine them is outside this exercise.
The code targets Java 21 and uses no framework or external library. It is practice material, not a claimed employer assessment. The complete class was compiled and exercised locally with a Java 21 runtime.
1. How would you model a basket line?
A record is a useful fit for a small data value with a fixed set of components. Java supplies component accessors and standard value methods, while a compact constructor can enforce invariants. Oracle's record-class guide describes the generated members and constructor rules.
For this case, the components are a product code, unit price and quantity. Validate them at construction so the total function does not need to rediscover the same line-level problems on every call. Decide which invariants belong to this value and which require a database or an authenticated request.
An existing product code cannot be verified merely by checking that a string is nonblank. This class enforces a small structural contract; the application still needs to resolve the code against its catalog. Keeping that distinction explicit prevents a tidy Java type from being mistaken for a complete business authorization boundary.
2. Why use BigDecimal, and what does the scale rule mean?
BigDecimal supports decimal arithmetic with explicit scale and rounding choices. Construct a value from the intended decimal representation, such as a string, rather than assuming a binary floating-point value exactly represents the entered price. Oracle documents the distinction in the BigDecimal API.
Our rule is to accept values representable exactly at two decimal places. 1.230 can become 1.23 without changing its value. 1.231 cannot, so the constructor rejects it rather than silently rounding. This is a deliberate exercise policy, not a universal currency rule.
State where a different rule would go. If tax or discounts produce additional decimal places, the business must choose when and how rounding happens. Rounding every line and rounding only the final total can produce different results. The subtotal function should not guess which policy finance intended.
3. Can you implement the complete class?
Save the following code as OrderTotals.java. Its result always has scale two. Null inputs fail explicitly, and an empty basket produces zero.
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Objects;
public final class OrderTotals {
private OrderTotals() {}
public record Line(String sku, BigDecimal unitPrice, int quantity) {
public Line {
Objects.requireNonNull(sku, "sku");
Objects.requireNonNull(unitPrice, "unitPrice");
if (sku.isBlank() || sku.length() > 64) {
throw new IllegalArgumentException("Invalid product code");
}
if (quantity < 1 || quantity > 1000) {
throw new IllegalArgumentException("Quantity out of range");
}
if (unitPrice.signum() < 0 ||
unitPrice.compareTo(new BigDecimal("1000000.00")) > 0) {
throw new IllegalArgumentException("Price out of range");
}
unitPrice = unitPrice.setScale(2, RoundingMode.UNNECESSARY);
}
}
public static BigDecimal total(List<Line> lines) {
Objects.requireNonNull(lines, "lines");
if (lines.size() > 1000) {
throw new IllegalArgumentException("Too many basket lines");
}
List<Line> snapshot = List.copyOf(lines);
BigDecimal result = new BigDecimal("0.00");
for (Line line : snapshot) {
result = result.add(line.unitPrice()
.multiply(BigDecimal.valueOf(line.quantity())));
}
return result;
}
}For two units at 19.95 and three units at 0.10, the result is 40.20. The loop is intentionally direct: it makes the calculation and absence of side effects easy to inspect. A stream-based version is also possible, but it should preserve the same contract and tests.
The input boundary for a real HTTP service must additionally limit request size before parsing. A helper that receives an already constructed BigDecimal does not protect an endpoint from arbitrarily large incoming strings. Separate that integration concern from what this class actually demonstrates.
4. Does List.copyOf make the whole operation thread-safe?
No. List.copyOf produces an unmodifiable list and rejects null elements, as described in the List API. It does not make an arbitrary source collection safe to mutate concurrently while the copy is being taken.
The caller must provide a stable list during this method call. In this example, each Line contains a String, a BigDecimal and an integer, so it does not expose a mutable nested collection. A record containing an ArrayList, however, would require a separate ownership or copying decision.
Explain three separate ideas: a final field cannot be reassigned after construction; an unmodifiable list rejects structural updates through that list; deeply immutable data does not expose mutable state through its components. Combining those ideas without checking the actual types is a common source of incorrect interview answers. If the interviewer extends the question to race conditions or the Java Memory Model, our guide to concurrency interview questions for software engineers covers both.
5. How do equals and compareTo differ for decimals?
For BigDecimal, numerical comparison and object equality have different scale behavior. 2.0 and 2.00 compare numerically as equal, while equals considers their scales. The BigDecimal reference documents this distinction.
Our constructor normalizes accepted prices to scale two, making the representation consistent inside a Line. In a test of the total, comparing against new BigDecimal("40.20") checks both the numerical result and that representation. If a test only cares about numerical equality, it should say so and use the appropriate comparison.
Consider the consequences for collections. If decimal values are used as keys or set members, understand whether the collection relies on equality and hashing or on an ordering relation. Do not substitute one semantic for another because both appear to work with the first two test values.
6. Which tests would you write first?
Start with a small table of observable outcomes. These are cases for the displayed class, not a general checkout certification.
| Case | Expected result |
|---|---|
| Empty basket | 0.00 |
| Two items at 19.95 and three at 0.10 | 40.20 |
| Zero-price line with a valid quantity | Accepted and contributes zero |
| Price 1.230 | Accepted as 1.23 |
| Price 1.231 | Arithmetic exception because rounding would be required |
| Negative price or quantity zero | Rejected |
| Quantity 1,000 and price 1,000,000.00 | Accepted boundary values |
| Null line within a basket | Rejected |
| More than 1,000 lines | Rejected |
| Same basket in a different order | Same subtotal |
Also check that a mutable input list retains its contents after calculation, an unmodifiable input is accepted, repeated calls return the same value, and separate lines with the same product code both contribute. Exercise maximum values using exact decimal arithmetic rather than an overflowing integer intermediate. Naming cases like these before you write code is a habit you can build in a timed mock coding interview, where edge-case discipline is part of the grading.
The local verification compiles the exact displayed class and runs both fixed boundary cases and generated baskets against an independent integer-cents oracle. It does not exercise an HTTP controller, a database transaction or a payment provider.
7. How would exceptions and service integration change the design?
The class exposes failures through exceptions that fit its local contract. A service layer should translate expected validation failures into an appropriate client response without leaking internal stack traces. Unexpected failures still need useful diagnostic context and an operator-visible signal.
Suppose the catalog price changes between basket display and checkout. Decide whether checkout uses the current price, a time-limited quote or another explicit policy. Calculate from the authorized price snapshot, and make any changed total clear before proceeding according to the product's flow.
Similarly, calling total twice should not charge the customer twice because this method does not charge anything. The later payment step needs its own idempotency and recovery design. If a payment request times out after being accepted, a correct arithmetic helper cannot tell you whether to retry that external action. For that separate design question, this backend engineer interview guide explains idempotency keys, retries and effectively-once processing.
8. What should you practise after this exercise?
Ask for one change at a time. Add a discount rule, support multiple currencies, attach a quoted-price version, or return both line subtotals and a basket subtotal. Before editing code, write the new invariant and the counterexample that would reveal a mistake.
For a multi-currency request, returning one unlabeled sum is not enough. For a discount, specify whether it applies before tax and where rounding occurs. For a new nested list in the result, revisit ownership and immutability instead of assuming the record keyword solves it.
Use PhantomCodeAI alongside direct Java practice to rehearse the reasoning. A strong explanation connects each type, validation and test to a requirement, and identifies the parts of a production workflow that remain outside the example.