Skip to content
Ameen Abozaid
PublishedBEG-012-L06

Demo: shipping cost rules and their boundaries

  • Lesson 6 of 6
  • 11 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

This lesson is a guided build. You will take a short written rule for shipping prices, turn it into Java, and then prove it correct with a table of test inputs worked out by hand. Along the way you will make one planned mistake, a boundary on the wrong side, and watch the test table catch it.

You will use everything in this module: if/else for rejecting bad input (L1), an else if chain for weight bands and the boundary habit (L2), a switch expression for zones (L3 and L4), and early exits for validation (L5). The finished program is also the starting point for the shipping cost lab at the end of the module, where you add Scanner input from Module 11.

No new Java is introduced. The new skill is process: write the rules, code them, predict, run, compare.

Learning Objectives

By the end of this lesson you will be able to:

  • Translate a written business rule into conditions, deciding which side of each boundary a word belongs to.
  • Combine a chain, a switch expression, and guard clauses in one small program.
  • Build a test table that covers every boundary and every kind of invalid input.
  • Work out expected results by hand before running the code.
  • Detect and fix a < versus <= boundary bug using the table.
  • Explain why decimals and discounts make hand-calculated expectations worth the effort.

Written Explanation

Step 1: write the rules in plain sentences

Code cannot be more precise than the rule it implements, so write the rule first and settle every ambiguity. Our delivery service says:

  1. Weight must be greater than 0. Otherwise the request is invalid.
  2. Base cost by weight: up to and including 1 kg costs 5; above 1 kg and up to and including 5 kg costs 10; above 5 kg costs 20.
  3. A zone surcharge is added: zone 1 adds 0, zone 2 adds 3, zone 3 adds 8. Any other zone is invalid.
  4. Members get 10 percent off the total (base plus surcharge).

Notice what the sentences settle: "up to and including" tells us to use <=; the discount applies to the surcharge as well; the order is base, then surcharge, then discount. If the requirement had not said, you would ask. These choices are where bugs come from.

Step 2: pick the right tool for each rule

  • Rule 1 and the invalid zone: guard clauses with early exits.
  • Rule 2: ranges, so an else if chain.
  • Rule 3: one exact value with fixed choices, so a switch expression with default for invalid.
  • Rule 4: one more if, applied last.

Two comments on choices. The invalid-weight test comes first, before the ranges, so that zero and negative weights never fall into the "up to 1 kg" band. And the zone switch returns -1 for invalid, a marker value that an if right after checks; later modules show cleaner ways to report errors.

Step 3: predict before you run

For each test input you will write the expected cost from the rules, using only paper and a calculator. Only then do you run the program. This is the discipline that gives the lab its value.

A note on decimals and money

This demo stores costs in double for simplicity. Module 9 flagged that double is a poor fit for real money, because it cannot represent every decimal exactly (0.1 + 0.2 prints 0.30000000000000004 in Java). The numbers in this lesson happen to print cleanly, but a production system would use a different representation. A later module covers the options.

Examples

Example 1: the code (simple)

java
public class Ship {
    public static void main(String[] args) {
        double weight = 5.01;
        int zone = 2;
        boolean member = true;

        if (weight <= 0) {
            System.out.println("Invalid weight");
            return;
        }
        double cost;
        if (weight <= 1) {
            cost = 5;
        } else if (weight <= 5) {
            cost = 10;
        } else {
            cost = 20;
        }
        double extra = switch (zone) {
            case 1 -> 0;
            case 2 -> 3;
            case 3 -> 8;
            default -> -1;
        };
        if (extra < 0) {
            System.out.println("Invalid zone");
            return;
        }
        cost = cost + extra;
        if (member) {
            cost = cost * 0.9;
        }
        System.out.println("Shipping cost: " + cost);
    }
}

Predict first. Weight 5.01 is above 5, so the base is 20. Zone 2 adds 3, giving 23. A member pays 90 percent: 23 * 0.9 = 20.7. Output of the real run:

text
Shipping cost: 20.7

The prediction matches. The hard-coded values at the top are what you change from test to test (in the lab you will read them with Scanner instead).

Example 2: the test table (practical)

I changed the three input lines and ran the program for each row below; expected values were worked out from the rules first.

WeightZoneMemberExpectedActualWhy this input
0.51no55.0well inside the first band
11no55.0upper boundary of band 1
1.011no1010.0just above the boundary
51no1010.0upper boundary of band 2
5.011no2020.0just above the boundary
12yes(5 + 3) * 0.9 = 7.27.2surcharge and discount together
53yes(10 + 8) * 0.9 = 16.216.2zone 3 with a member
5.013yes(20 + 8) * 0.9 = 25.225.2band 3 with zone and discount
01noinvalid weightInvalid weightguard on weight
24noinvalid zoneInvalid zoneswitch default

Ten rows, all matching. Look at how the rows were chosen: every weight boundary appears with the value itself and just above it, each zone has at least one row, members and non-members both appear, and both kinds of invalid input are covered. For extra safety you could add a row just below each boundary, such as 0.99 or 4.99.

Example 3: the planned mistake and the fix (realistic)

Now suppose someone "tidies" the first test and writes weight < 1 instead of weight <= 1:

java
if (weight < 1) {
    cost = 5;
} else if (weight <= 5) {
    cost = 10;
} else {
    cost = 20;
}

Test rows 0.5, 1.01, 5, and 5.01 still give the right answer: 5, 10, 10, 20. A test table with only "ordinary" values would pass. But the 1 kg row now produces a wrong cost:

text
buggy: 10.0
fixed: 5.0

For exactly 1 kg the rule says 5, the buggy code gives 10 (the value falls into the next band), and only the boundary row exposes it. Fix the < to <=, rerun the entire table (not just the failed row), and tick every line again. Rerunning everything matters because a fix can break something that was working.

The lesson in miniature: boundary rows find boundary bugs, and ordinary rows do not. Which values find a boundary bug fastest? The exact boundary values and the ones just beside them.

Backend Insight

Pricing, fees, limits, and eligibility are among the most common things backends compute, and they are also where an off-by-one-at-the-boundary bug directly costs money or customers: undercharging every 1 kg parcel is a quiet, constant loss. In professional teams, the test table is not a school exercise. It becomes automated tests (you will write such tests later in the course), reviewed alongside the code, and run on every change so that a later edit to the weight bands cannot silently break the 1 kg case.

A second professional habit shown here: the rules were written down first, in words that a non-programmer on the business side could read and confirm. When a bug report says "my parcel was charged 10, not 5", you can point to sentence 2 of the rule and to the test row that covers it. Without the written rule you have an argument; with it you have a spec.

Finally, notice the structure: validation first, then the calculation, then adjustments. Real services follow the same order, and it is the flat style from Lesson 5.

Common Mistakes

Mistake 1: Testing only typical values

  • Incorrect: a table of 0.5, 3, 10, and 25 kg.
  • Correct: include each boundary and its neighbours, plus invalid inputs.

Mistake 2: Computing the expected result from the program's output

If the table's "expected" column is filled in after running, it just repeats the program, including its bugs. Calculate by hand first.

Mistake 3: Wrong order of the steps

Applying the discount before the zone surcharge changes the totals: (5 + 3) * 0.9 is 7.2, but 5 * 0.9 + 3 is 7.5. The written rule decides which is right, not the code.

Mistake 4: Ordinary tests before the invalid-input guard

If weight <= 1 came first, a weight of 0 or -3 would be charged 5. Put the rejecting guards first.

Mistake 5: Fixing one row and not rerunning the table

After any fix, rerun every row.

Exercises

Level A: Foundation

A1. For the rules in this lesson, what is the cost for weight 3, zone 1, non-member?

Show solution A1

A1. Band 2 (above 1, up to 5) gives 10. Zone 1 adds 0. Total 10.

A2. Which weight values are the boundaries of the bands? Which side of each boundary do the words "up to and including" put the boundary value on?

Show solution A2

A2. The boundaries are 1 and 5. "Up to and including" puts the boundary value in the lower band, so the tests are <= 1 and <= 5.

A3. Why is the check for weight <= 0 placed before the weight chain?

Show solution A3

A3. Otherwise 0 or a negative weight would match weight <= 1 and be charged as a valid light parcel.

Level B: Application

B1. Calculate by hand: weight 4.5, zone 3, member; and weight 1, zone 3, non-member. Show the steps.

Show solution B1

B1. 4.5 kg is band 2, so base 10. Zone 3 adds 8, so 18. A member pays 90 percent: 18 * 0.9 = 16.2. Second: 1 kg is band 1, so 5, plus 8 is 13, no discount: 13.

B2. Change the rule to "above 1 kg and under 5 kg costs 10; 5 kg and above costs 20". Which line changes and how? What new row would you add to the test table, and what should it show?

Show solution B2

B2. The second test changes from weight <= 5 to weight < 5. The new row is weight 5: it now falls to the else and costs 20. In the old rule it cost 10, so this row changes expected value and shows the boundary moved.

B3. The discount for members changes to 15 percent. Which line changes? How many existing table rows have an expected value that changes? Which ones?

Show solution B3

B3. The line cost = cost * 0.9 becomes cost * 0.85. Only member rows change: 1/zone 2 gives (8) * 0.85 = 6.8; 5/zone 3 gives 18 * 0.85 = 15.3; 5.01/zone 3 gives 28 * 0.85 = 23.8. Three rows change; non-member rows stay the same.

Level C: Challenge

C1. A new rule: orders with a weight above 30 kg are rejected with the message Too heavy. Where does the check go, and which three new test rows do you add?

Show solution C1

C1. Put it with the other guards at the top, after the <= 0 check (or directly with it, since both are weight validity). Rows: 30 (accepted, band 3), 30.01 (Too heavy), and 100 (Too heavy). A row at 30 matters because it is the boundary and confirms the rule says "above 30".

C2. The "buggy" program computed the discount before adding the zone surcharge. Write the buggy line order, then give one table row where the output differs from the correct one and by how much.

Show solution C2

C2. Buggy order: base, discount, then surcharge:

java
cost = cost * 0.9;
cost = cost + extra;

Row weight 1, zone 2, member: correct 7.2, buggy (5 * 0.9) + 3 = 7.5. Difference 0.3.

Mini Challenge

Extend the program with a "free shipping" rule: member orders of weight 1 kg or less in zone 1 ship free (cost 0). Decide where the check goes so that earlier guards still run first. Add at least five new table rows (at least two on the boundary of the new rule, one non-member in the same situation, one in zone 2) with expected values worked out first.

Check your work: invalid weights and zones are still rejected for members, the boundary rows (1 and 1.01) behave differently, and your expectations came from the written rule rather than from running.

Summary & Next Lesson

Key takeaways

  • Write the rules in plain sentences first, deciding which side of each boundary the words put the boundary value on.
  • Guard clauses reject bad input first, then a chain handles ranges, a switch expression handles fixed choices, and the adjustments follow in the order the rule states.
  • A test table needs the boundary values, values just beside them, each category, and each kind of invalid input.
  • Compute expected results by hand before running, and rerun the whole table after each fix.
  • Boundary rows find boundary bugs; ordinary rows do not.

Next lesson

This was the last lecture of the module. The shipping cost lab (BEG-012-LAB1) uses this program with Scanner input and at least ten test rows. The module after this one covers repeating actions with loops, starting from "Why loops: repetition without copy-paste".

Self-check

  1. Which input values find a < versus <= bug fastest?
  2. Why is the expected column filled in before running?
  3. Why does the weight guard come first?
  4. What changes if the discount is applied before the surcharge?
  5. Why rerun the whole table after a fix?
Show answers
  1. The exact boundary values and the ones just beside them.
  2. So that it reflects the rule, not the program's possibly wrong behaviour.
  3. So invalid weights never reach the bands and get charged.
  4. The totals change: the surcharge is no longer discounted, as in 7.5 versus 7.2.
  5. A fix can break a row that passed before.

References

  • Oracle, The Java Language Specification, Java SE 21, Chapter 14, for the semantics of if/else and switch used in the program.
  • All program outputs and the test table were produced by compiling and running the code with JDK 21. The 0.1 + 0.2 result was observed in the same JDK; the general reason (binary floating point cannot represent every decimal exactly) is not cited here beyond Module 9.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Type to search levels, phases, modules and lessons.