Kestrel Exams kestrel logo KESTREL EXAMSPRACTICE TO PASS
Java19 min readCode verified

The software engineering life cycle: six steps from a problem to a working program

A program does not start in the editor. This guide walks one laundromat pricing program through all six steps of the software engineering life cycle, from the owner’s words to a bug the tests catch and a requirement nobody wrote down. Every listing was compiled and run on OpenJDK 21.

Practice this topic in the free app →All Java guides

A red sock in the white wash#

You already know the shape of this guide from doing laundry. Sort the clothes into loads, pick a setting, run the machines, and check the results before you fold. If a red sock turned the white shirts pink, you do not fix that at the folding table. You go back to the sorting, where the mistake was made.

Writing a program follows the same kind of order, called the software engineering life cycle: the steps that take a program from someone’s problem to code that solves it. This guide walks one program for a neighborhood laundromat through every step. Along the way a test catches a bug, and a question nobody asked sends us back to the start.

You do not need to read every line of Java yet

Watch what each step produces; the guides on writing your own class and on methods explain the code line by line.

The six steps, in order#

Each step answers one question and produces something you can point at.

StepThe question it answersWhat it produces
1. Problem statementWhat exactly must the program do?a numbered list of requirements
2. Problem decompositionWhat smaller problems is it made of?subproblems, grouped
3. Class designWhich classes, holding which data?each class’s data and types, and its method headers
4. Method designWhat steps happen inside each method?numbered steps in plain words
5. Coding into JavaHow are those steps written in Java?source code that compiles
6. Testing, debugging, revisingDoes it do what the statement says?test results, fixes, revised steps

A requirement is one thing the program must do, stated precisely enough to check. The problem statement is also called the problem specification. Data design, choosing which values the program stores and the type of each (fields, return values, and working values such as a total or an average), happens inside class design. Algorithm design, writing the ordered steps that solve a subproblem, happens inside method design. (An algorithm is an ordered list of steps.) If a question names either one as a step of its own, place data design after decomposition and before any method’s steps, and algorithm design right before coding. Some lists count method headers as the start of method design; either way they are fixed before any method’s steps are written.

Step 6 is iterative: it repeats. A failed test can send you back to any earlier step, and after the fix you test again. That loop is the “cycle” in life cycle.

Try it first

Why must the problem statement come before any decision about classes, types or code?

Show the answer

Every later step is checked against it. You cannot design for a problem you have not pinned down, or write a test without knowing the right answer.

Step 1: the problem statement#

The owner explains the job at the counter: “Washing is three fifty a load. Drying is ten cents a minute. Club card holders get ten percent off. And at closing, tell me how many loads people do per visit, on average.”

Try it first

For each visit, which facts must the person at the desk type in?

Show the answer

Loads washed, dryer minutes, and whether the customer has a card. These are the inputs; the money figures worked out from them are the outputs.

Step 1 turns talk into numbered, checkable lines. Version 1:

R1For each visit, the desk enters loads, dryer minutes, and whether the customer has a club card.
R2A wash costs $3.50 per load.
R3Club card holders get 10 percent off.
R4Drying costs $0.10 per minute.
R5The program shows each visit’s total cost.
R6At closing, it shows the average number of loads per visit.

No Java, no types: the statement says what, never how. Keep an eye on R3.

Step 2: problem decomposition#

Problem decomposition breaks the job into subproblems: pieces small enough to solve one at a time. The result is a list of pieces, with no Java in it yet:

  1. Price the washes for one visit.
  2. Price the drying for one visit.
  3. Work out the card discount for one visit.
  4. Combine those into the visit’s total.
  5. Count the day’s visits and loads.
  6. Work out the average loads per visit.
Try it first

Sort the six into piles of things that belong together. How many piles?

Show the answer

Two. Subproblems 1 to 4 are about one visit; 5 and 6 are about the whole day.

Each pile becomes a class, which bundles data with the methods (named blocks of steps) that work on it. So the program gets LaundryVisit and DaySummary, plus a class with main to run them. Rule of thumb: nouns in a statement suggest classes; verbs suggest methods.

Step 3: class design, with its data and types#

A class’s data lives in fields, variables that belong to an object. Choosing each one’s type, with a reason, is the data design. You also write each method’s header: what it gives back (its return type), its name, and what it needs.

LaundryVisitTypeWhy
washes, dryerMinutesintwhole loads, whole minutes
hasCardbooleanonly yes or no
WASH_PRICE, DRY_PRICEdoublemoney has cents, a fractional part
DISCOUNT_PERCENTintthe owner said “ten percent”
washCost(), dryCost(), discount(), total()return doubleamounts of money
getWashes()returns intso the day can count loads
DaySummaryTypeWhy
visits, loadsintcounts of whole things
addVisit(LaundryVisit v)returns nothingit only updates the counts
averageLoads()returns intloads are whole numbers, so the average is too
One row above is a mistake

It is left in on purpose: real designers make exactly this one. Testing will catch it.

Names in capitals are constants, marked final so they never change. Money is a double here because an int counting dollars would lose the 50 cents in $3.50. The price of that choice: a double stores most decimals, such as 0.10, as the nearest value it can hold, so results can land a hair off.

QCents.java
public class QCents {
    public static void main(String[] args) {
        double dryCost = 3 * 0.10;
        System.out.println(dryCost);
        System.out.println(dryCost == 0.30);
        System.out.println(Math.abs(dryCost - 0.30) < 0.005);
    }
}
0.30000000000000004
false
true

That is why our tests compare money to within half a cent, never with ==. Programs that handle real money often count whole cents in an int or long, or use Java’s BigDecimal class.

Why class design comes before method design: a Java method cannot stand alone; it is always written between the curly braces of some class. So washCost needs a home before its steps can be written, and those steps use washes and WASH_PRICE, which only class design creates.

Step 4: method design, in numbered steps#

Each method now gets its steps in plain words, called pseudocode. Writing them is the algorithm design. “Give back” is what Java calls returning a value.

method design.txt
washCost()
  1. Give back washes times the wash price.
dryCost()
  1. Give back dryer minutes times the drying price.
discount()
  1. If the customer has no card, give back 0.
  2. Otherwise, find the visit's cost: wash cost plus drying cost.
  3. Give back that cost times 10, divided by 100.
total()
  1. Add the wash cost and the drying cost.
  2. Subtract the discount, and give back the result.
addVisit(v)
  1. Add 1 to visits.
  2. Add v's washes to loads.
averageLoads()
  1. Give back loads divided by visits.

A good method design is one someone else could follow with a calculator.

Try it first

Now that the class design is agreed, two people want to work at once. Which methods can they design at the same time?

Show the answer

washCost and dryCost: neither uses the other. discount and total rely on their results, so they wait. DaySummary’s two methods can go in parallel too: they use none of the pricing methods, only getWashes(), whose one step (give back washes) is settled the moment it is named.

Splitting the work waits until the class design is agreed. If two people each invented half the classes, the halves would use different names and types and would not connect.

Step 5: coding into Java#

Try it first

Using the names from the class design, write method design step 1 of washCost() as one line of Java.

Show the answer

return washes * WASH_PRICE; Every name in the step already exists.

Every method goes the same way: one numbered step, about one line. Here is discount():

Method designJava
1. If the customer has no card, give back 0.if (!hasCard) { return 0.0; }
2. Otherwise, find the visit’s cost.double cost = washCost() + dryCost();
3. Give back that cost times 10, divided by 100.return cost * DISCOUNT_PERCENT / 100;

(! means “not”.) With a finished method design, coding is mostly translation: each plain-words step becomes Java, and nothing new gets decided. So if you find yourself choosing a type or a rule while typing Java, pause: that choice belongs to an earlier step, so make it there and write it down. The whole program appears in the next step, because the first thing to do with new code is test it.

Step 6: testing, with expected values from the statement#

A test case is an input plus the output you expect. Work the expected output out by hand from the statement. Never copy it from the program’s output, which only tests the code against itself. Choose tests on purpose: an ordinary case, the edges (no washing, no drying), each special rule, and an average that is not whole. The testing guide explains why edges find the most bugs.

TestInputExpected
12 loads, 40 minutes, no card11.00
20 loads, 30 minutes, no card3.00
31 load, 0 minutes, no card3.50
42 loads, 40 minutes, card?
5a day of 4 visits with 2, 1, 2 and 1 loads6 ÷ 4 = 1.5
Try it first

Fill in the question mark, using only the statement.

Show the answer

You cannot, and that is the discovery. R3 says “10 percent off” but not off what. Off the whole visit, the total is 9.90; off the washes only, 10.30. The code gives 9.90, but that shows what the code does, not what the owner wants.

A test forces one exact answer, which makes it a good way to find gaps in a statement. Test 4 goes on hold while a question goes to the owner. The test driver below is a class whose main only runs tests. Its check passes when Math.abs(actual - expected), the distance between the two numbers, is under half a cent. (Math.abs drops a minus sign: Math.abs(-0.2) is 0.2.)

LaundromatTest.java
class LaundryVisit {
    static final double WASH_PRICE = 3.50;     // per load
    static final double DRY_PRICE = 0.10;      // per dryer minute
    static final int DISCOUNT_PERCENT = 10;

    private int washes;
    private int dryerMinutes;
    private boolean hasCard;

    public LaundryVisit(int washes, int dryerMinutes, boolean hasCard) {
        this.washes = washes;
        this.dryerMinutes = dryerMinutes;
        this.hasCard = hasCard;
    }

    public int getWashes() {
        return washes;
    }

    public double washCost() {
        return washes * WASH_PRICE;            // step 1
    }

    public double dryCost() {
        return dryerMinutes * DRY_PRICE;       // step 1
    }

    public double discount() {
        if (!hasCard) {                        // step 1
            return 0.0;
        }
        double cost = washCost() + dryCost();  // step 2
        return cost * DISCOUNT_PERCENT / 100;  // step 3
    }

    public double total() {
        return washCost() + dryCost() - discount();   // steps 1 and 2
    }
}

class DaySummary {
    private int visits;
    private int loads;

    public void addVisit(LaundryVisit v) {
        visits = visits + 1;                   // step 1
        loads = loads + v.getWashes();         // step 2
    }

    public int averageLoads() {
        return loads / visits;                 // step 1
    }
}

public class LaundromatTest {
    static void check(String name, double actual, double expected) {
        if (Math.abs(actual - expected) < 0.005) {
            System.out.println("PASS  " + name);
        } else {
            System.out.println("FAIL  " + name + ": expected " + expected + ", got " + actual);
        }
    }

    public static void main(String[] args) {
        check("1 typical visit", new LaundryVisit(2, 40, false).total(), 11.00);
        check("2 dryer only   ", new LaundryVisit(0, 30, false).total(), 3.00);
        check("3 wash only    ", new LaundryVisit(1, 0, false).total(), 3.50);
        // test 4, card holder: on hold until the owner answers

        DaySummary day = new DaySummary();
        day.addVisit(new LaundryVisit(2, 40, false));
        day.addVisit(new LaundryVisit(1, 30, false));
        day.addVisit(new LaundryVisit(2, 0, false));
        day.addVisit(new LaundryVisit(1, 20, false));
        check("5 average loads", day.averageLoads(), 1.5);
    }
}
PASS  1 typical visit
PASS  2 dryer only   
PASS  3 wash only    
FAIL  5 average loads: expected 1.5, got 1.0

Test 5 expected 1.5 and got 1.0. (The method returned the int 1; check takes a double, so Java converted it to 1.0.)

Some Java, named once. The constructor is the block named after the class. It copies the inputs into the fields when new creates a visit, and this.washes means the field, not the input. Number fields start at 0. The classes share one file, and only one, the one named after the file, may be public.

Debugging: tracing a failure back to its step#

Debugging is finding the cause of a failure and fixing it: which line is wrong, and in which step was the wrong decision made?

First, a sanity check: does 1 make sense? Each visit had 1 or 2 loads, so 1 is possible. That is how this bug slips through when nobody works out the answer first. Keep the habit when an AI tool hands you code: decide roughly what the answer should be, then compare.

Try it first

loads is 6 and visits is 4. Why does loads / visits give 1?

Show the answer

Both are int, so Java does integer division: it keeps the whole part and throws the fraction away. 6 ÷ 4 is 1.5, and the .5 is dropped.

Trap check: integer or floating-point division?

At every /, ask what type each side is. Two int values give an int, fraction gone. If either side is a double, the fraction stays. It tops the hand-tracing list in the exam study guide.

Now trace it back. The code faithfully translates “give back loads divided by visits”, and dividing is right. The wrong decision is the return type int, made in class design for a false reason: an average of whole numbers can have a fraction. That was the planted mistake, and since choosing a type is data design, the fix starts there. The design row becomes “returns double”, and the code follows:

public double averageLoads() {
    return (double) loads / visits;
}
Trap check: changing the type is not enough

With only the return type changed, loads / visits still divides two int values, gets 1, then converts it to 1.0. The cast (double) converts loads to 6.0 before dividing, so the .5 survives. The casting guide shows where a cast must sit.

Shrink it to the smallest case. One visit of 2 loads gives 2 ÷ 1 = 2.0, which is right. Zero visits divides 0 by 0, and the statement never says what the report should show then: the same kind of gap as test 4 (practice question 3). A fix is finished only when the tests pass again, and that run comes next.

Revising: when a test exposes a missing requirement#

The owner replies: “Washes only. Drying is always full price; the dryers barely pay for their own electricity.” Three tempting moves all go wrong:

  • Code the easier reading and leave a comment. The requirement stays unwritten.
  • Make test 4 expect whatever the code prints. That writes down today’s behavior as if it were the rule.
  • Change the code but not the statement. Now they disagree.

Instead, settle it where the problem began, the statement, with the person who owns the requirement. Then carry the change forward:

StepWhat changes
1. StatementR3: “Club card holders get 10 percent off the washes. Drying is always full price.”
2. Decompositionnothing
3. Class designnothing: no new data, same headers
4. Method designdiscount() step 2: “Otherwise, find the wash cost.”
5. Codingone line: double cost = washCost();
6. Testingtest 4 comes off hold; every test runs again
Try it first

What should test 4 expect now?

Show the answer

The discount is 7.00 × 10 ÷ 100 = 0.70, so the total is 7.00 + 4.00 − 0.70 = 10.30.

Both changes in, all five tests:

LaundromatTest.java
class LaundryVisit {
    static final double WASH_PRICE = 3.50;     // per load
    static final double DRY_PRICE = 0.10;      // per dryer minute
    static final int DISCOUNT_PERCENT = 10;

    private int washes;
    private int dryerMinutes;
    private boolean hasCard;

    public LaundryVisit(int washes, int dryerMinutes, boolean hasCard) {
        this.washes = washes;
        this.dryerMinutes = dryerMinutes;
        this.hasCard = hasCard;
    }

    public int getWashes() {
        return washes;
    }

    public double washCost() {
        return washes * WASH_PRICE;            // step 1
    }

    public double dryCost() {
        return dryerMinutes * DRY_PRICE;       // step 1
    }

    public double discount() {
        if (!hasCard) {                        // step 1
            return 0.0;
        }
        double cost = washCost();              // step 2, revised
        return cost * DISCOUNT_PERCENT / 100;  // step 3
    }

    public double total() {
        return washCost() + dryCost() - discount();   // steps 1 and 2
    }
}

class DaySummary {
    private int visits;
    private int loads;

    public void addVisit(LaundryVisit v) {
        visits = visits + 1;                   // step 1
        loads = loads + v.getWashes();         // step 2
    }

    public double averageLoads() {
        return (double) loads / visits;        // step 1, revised type
    }
}

public class LaundromatTest {
    static void check(String name, double actual, double expected) {
        if (Math.abs(actual - expected) < 0.005) {
            System.out.println("PASS  " + name);
        } else {
            System.out.println("FAIL  " + name + ": expected " + expected + ", got " + actual);
        }
    }

    public static void main(String[] args) {
        check("1 typical visit", new LaundryVisit(2, 40, false).total(), 11.00);
        check("2 dryer only   ", new LaundryVisit(0, 30, false).total(), 3.00);
        check("3 wash only    ", new LaundryVisit(1, 0, false).total(), 3.50);
        check("4 card holder  ", new LaundryVisit(2, 40, true).total(), 10.30);

        DaySummary day = new DaySummary();
        day.addVisit(new LaundryVisit(2, 40, false));
        day.addVisit(new LaundryVisit(1, 30, false));
        day.addVisit(new LaundryVisit(2, 0, false));
        day.addVisit(new LaundryVisit(1, 20, false));
        check("5 average loads", day.averageLoads(), 1.5);
    }
}
PASS  1 typical visit
PASS  2 dryer only   
PASS  3 wash only    
PASS  4 card holder  
PASS  5 average loads

Five out of five. Re-running every old test after a change is regression testing: it shows nothing that used to work broke. Because each step had a clear job, one changed requirement touched one design step and one line.

Type this one yourself

Do not paste it. The owner adds R7: “Detergent costs $1.25 a box, never discounted.” Design the change on paper, then type it into your own copy with a test: a card holder, 2 loads, 40 minutes, 2 boxes.

Show the answer

Class design: an int field soapBoxes, a constant SOAP_PRICE = 1.25, a fourth constructor input, and soapCost() returning double. Method design: soapCost() gives back boxes times the soap price; total() adds it; discount() is unchanged. Expected: 7.00 + 4.00 + 2.50 − 0.70 = 12.80. Decomposition gains one subproblem: price the detergent for one visit. Testing: every old new LaundryVisit(...) call now needs a fourth input (0 boxes) or the file will not compile; then every test runs again.

What was new in this guide#

  1. The software engineering life cycle: six steps in a fixed order, each with a product.
  2. The problem statement: numbered, checkable requirements, no Java.
  3. Decomposition: subproblems, grouped into classes.
  4. Class design, with data design: fields, types and why, method headers.
  5. Method design, with algorithm design: numbered steps in pseudocode.
  6. Coding: mostly translation.
  7. Testing against the statement, debugging back to the step that went wrong, revising from the statement, and re-running every test: regression testing.
One sentence to carry into the exam

State the problem, split it up, design the classes and their data, design each method’s steps, translate them into Java, then test against the statement. When a test fails, fix the step where the wrong decision was made, carry the change forward, and run every test again.

Where these go wrong#

  • Starting in the editor. Write the numbered requirements first; without them, nothing says what correct means.
  • Copying a test’s expected value from the program’s output. Work it out from the statement, by hand.
  • Patching the code when the statement is silent. Settle the requirement with its owner, then carry it through design, code and tests.
  • Storing an average as an int. An average can have a fractional part, so it needs a double: a data design decision. (Money is different: an int counting cents is exact, while a double needs a tolerance when compared.)
  • Fixing integer division by changing only the return type. Cast first: (double) loads / visits.
  • Re-running only the test that failed. Run every test after every change.

On the exam: For “which step is this?”, ask what the thing is: a requirement, subproblems, a type choice, numbered steps, Java, or test results. For “where does the fix belong?”, find the earliest step where the wrong or missing decision was made, which is rarely testing itself.

Practice#

Answer each one on paper before you look at the solutions. Every answer that involves running code was checked by compiling and running it.

  1. Put these in life cycle order and name each one’s step: (a) test inputs with expected outputs; (b) return cost * 2; typed in an editor; (c) “R2: a dryer cycle costs $2.00”; (d) “1. Add the costs. 2. Subtract the discount.”; (e) “total: double, money has cents”; (f) “price one visit; summarize the day”.
  2. A teammate writes the discount as the first line below, and card holders never get anything off. Predict both lines, and name the step the mistake belongs to.
    double cost = 7.00;
    System.out.println(cost * (DISCOUNT_PERCENT / 100));
    System.out.println(cost * DISCOUNT_PERCENT / 100);
  3. The owner runs the closing report before anyone has come in, so loads and visits are both 0. What does (double) loads / visits print? What would the original int version do? Where does the fix belong?
  4. Translate this method design into a Java method freeMinutes(int washes) that returns an int, and pick the most useful test value. “1. Start with 5 free dryer minutes per wash. 2. If the visit has 3 or more washes, add 10 bonus minutes. 3. Give back the minutes.”
  5. A teammate says: “I’ll skip method design and work the steps out in the editor.” Answer in one or two sentences.
  6. Name the earliest step that has to change: (a) the wash price rises to $3.75; (b) the code says dryerMinutes * WASH_PRICE, but the design says the drying price; (c) a new tips field is an int, and tips have cents.

Solutions#

  1. (c) problem statement, (f) decomposition, (e) class design (data design), (d) method design (algorithm design), (b) coding, (a) testing. Ask what kind of thing each one is.
  2. The parentheses make Java do 10 / 100 first: integer division, so 0. Without them, 7.00 * 10 comes first and is the double 70.0.QDiscount.java
    public class QDiscount {
        static final int DISCOUNT_PERCENT = 10;
    
        public static void main(String[] args) {
            double cost = 7.00;
            System.out.println(cost * (DISCOUNT_PERCENT / 100));
            System.out.println(cost * DISCOUNT_PERCENT / 100);
        }
    }
    0.0
    0.7

    This belongs to coding: the design said “times 10, divided by 100”, in that order, and the translation changed the order. In the average bug, the code matched its design and the design was wrong.

  3. NaN, short for “not a number”. The int version stops with an ArithmeticException, because whole-number division by zero is not allowed:QEmpty.java
    public class QEmpty {
        public static void main(String[] args) {
            int loads = 0;
            int visits = 0;
            System.out.println((double) loads / visits);
            System.out.println(loads / visits);
        }
    }
    NaN
    Exception in thread "main" java.lang.ArithmeticException: / by zero
    	at QEmpty.main(QEmpty.java:6)

    Nobody chose either result. The fix starts at the problem statement: ask the owner what an empty day should show, then carry it into method design, code and a new test.

  4. Each step becomes about one line:QFreeMinutes.java
    public class QFreeMinutes {
        public static int freeMinutes(int washes) {
            int minutes = washes * 5;         // step 1
            if (washes >= 3) {                // step 2
                minutes = minutes + 10;
            }
            return minutes;                   // step 3
        }
    
        public static void main(String[] args) {
            System.out.println(freeMinutes(2));
            System.out.println(freeMinutes(3));
            System.out.println(freeMinutes(0));
        }
    }
    10
    25
    0

    The most useful test is 3, the edge where the bonus starts: it catches > typed in place of >=.

  5. Then the design decisions get made in the middle of Java syntax, where gaps like “10 percent off what?” get guessed instead of asked. With the steps designed first, coding is mostly translation, and there is something to check the code against.
  6. (a) The problem statement, then the constant and the affected tests. (b) Coding: the design was right, the translation was not. (c) Class design (data design), then the code.

Test-readiness checklist#

These are the items from the course’s Exam 1 topic list that this guide covers. Tick each one only when you can do it without looking.

  • ☐  I can list the six steps in order and name what each produces.
  • ☐  I can say which step a decision belongs to, including data design and algorithm design.
  • ☐  I can explain why class design comes before method design, and what can be done in parallel.
  • ☐  I can trace a failed test back to its step, and say what to do about an ambiguous requirement.

Test yourself in the free Kestrel Exams app

16 practice questions on the software engineering life cycle, with every code answer checked by compiling and running it. Free, offline, no account.

Practice the software engineering life cycle →

Frequently asked questions#

What are the steps of the software engineering life cycle?

In order: problem statement (specification), problem decomposition, class design, method design, coding into Java, and testing, debugging and revising. Data design happens within class design, and algorithm design within method design.

Why is the software engineering life cycle called a cycle?

Because the last step repeats. Testing can expose a problem whose cause is in an earlier step, so you fix that step, carry the change forward, and test again.

What is the difference between class design and method design?

Class design decides the classes, their data and types, and the method headers. Method design writes the steps inside each method. Class design comes first because every Java method lives in a class.

Is algorithm design the same as coding?

No. Algorithm design writes the ordered steps in plain words or pseudocode. Coding translates those finished steps into Java, so with a good design, coding is mostly translation.

What should you do when testing finds an ambiguous requirement?

Do not guess, and do not make the test match the code. Settle the requirement in the problem statement with the person who owns it, carry the change through design and code, then re-run every test.

Is the software engineering life cycle the same as the SDLC?

Closely related. SDLC usually means software development life cycle, which in industry also covers deployment and maintenance; models such as waterfall and agile arrange the activities differently.

Suggest a change

Something here not clear? A topic you wish we covered? Tell us. We read every message, and a request is the fastest way to get a guide written: several of these exist because somebody asked.