Module 4 · Methods & Modularity ⏱ 18 min

Recursion Basics (base case, call stack)

By the end of this lesson you will be able to:
  • Identify the base case and recursive case in a recursive method
  • Trace the call stack as frames push on and unwind back off
  • Predict a recursive method's output, including ascending versus descending order

Some problems are awkward to solve in a straight line but obvious once you let them shrink: 'count down from 3' is just 'say 3, then count down from 2'. A method that solves a smaller version of its own problem by calling itself is recursive. Recursion is not a trick for its own sake — it is the natural shape for anything nested or self-similar, from walking a folder tree to evaluating an expression with parentheses inside parentheses.

The trade for that elegance is discipline. A recursive method must know when to stop, or it will call itself forever and bring the program down. This lesson teaches you to read a recursive method, trace its calls, and predict exactly what it prints — by following the same call stack the JVM uses.

flowchart TD
  R["recursiveMethod(input)"] --> Q{"is this the base case?"}
  Q -->|"yes"| BASE["return the trivial answer<br/>no further call"]
  Q -->|"no"| REC["recursive case:<br/>call on smaller input<br/>then combine the result"]
  style BASE fill:#15803d,color:#fff
  style REC fill:#3776ab,color:#fff
  style Q fill:#c2410c,color:#fff
Every recursion branches on one question: is this the base case, or do we shrink and call again?

Two parts: base case and recursive case

Every working recursion has two halves. The base case is the condition that stops the recursion — the smallest, trivial input the method can answer without calling itself. The recursive case is everything else: it does a little work, then calls itself with an input that is strictly closer to the base case.

That last phrase is the whole game. If the recursive call does not move toward the base case, the method never stops. countdown(n) must call countdown(n - 1), never countdown(n + 1) or countdown(n) — only shrinking the input guarantees the base case is eventually reached.

Main.java — countdown prints n, then shrinks toward the base case n == 0. Real, compiling Java.
public class Main {
    static void countdown(int n) {
        if (n == 0) {            // base case: stop here
            System.out.println("go");
            return;
        }
        System.out.println(n);   // recursive case: print, then shrink
        countdown(n - 1);
    }

    public static void main(String[] args) {
        countdown(3);
    }
}

Shrinking is not optional

It is tempting to think any self-call counts as recursion — it does not, unless the problem actually got smaller. factorial(n) calling factorial(n) would loop forever, because n never approaches the base case; calling factorial(n - 1) works because each step removes one from n until n <= 1 halts it.

The base case and the shrinking call are a single contract: the recursive case exists to feed the base case, and the base case exists to end what the recursive case started. Break either half and the method is broken.

The call stack: each call gets its own frame

When a method calls itself, the JVM does not overwrite the first call — it stacks a new frame on top of it. Each frame holds its own private copy of the parameters and local variables, so countdown(3) and countdown(2) each have their own n and never confuse them. The first call pauses, mid-execution, while the calls above it run.

Frames pile up as the recursion deepens, then unwind in reverse order as each call finishes and hands control back to the one below it. Reading recursion is mostly a matter of picturing that stack: who is paused where, and who is waiting on whose answer.

flowchart TD
  F3["countdown(3)"] --> F2["countdown(2)"]
  F2 --> F1["countdown(1)"]
  F1 --> F0["countdown(0) base case"]
  F0 -.->|"returns"| F1
  F1 -.->|"returns"| F2
  F2 -.->|"returns"| F3
  style F0 fill:#15803d,color:#fff
  style F3 fill:#3776ab,color:#fff
Solid arrows push frames as the recursion deepens; dotted arrows return as the stack unwinds in reverse.
Main.java — factorial assembles its answer on the way back down the stack. Real, compiling Java.
public class Main {
    static int factorial(int n) {
        if (n <= 1) {                       // base case: 1! = 1
            return 1;
        }
        return n * factorial(n - 1);        // recursive case
    }

    public static void main(String[] args) {
        System.out.println(factorial(4));
    }
}

Returns unwind back down the stack

For a method that returns a value — like factorial — the answer is assembled in reverse, as the stack unwinds. factorial(1) returns 1 first; factorial(2) then computes 2 * 1; factorial(3) computes 3 * 2; factorial(4) computes 4 * 6 and yields 24. Each frame multiplies its own n by the result handed back from above.

Where you put work relative to the recursive call changes everything. Print before the recursive call and output appears as the stack grows (descending). Print after it and output appears as the stack shrinks (ascending). Same method, opposite order — a favourite interview question, and a habit worth building now.

Main.java — printing AFTER the recursive call emits the numbers in ascending order as the stack unwinds. Real, compiling Java.
public class Main {
    static void countUp(int n) {
        if (n == 0) {
            return;
        }
        countUp(n - 1);          // recurse first
        System.out.println(n);   // print while unwinding
    }

    public static void main(String[] args) {
        countUp(3);
    }
}
flowchart TD
  A["call 1"] --> B["call 2"]
  B --> C["call 3"]
  C --> DOTS["...keeps going..."]
  DOTS --> ERR["call stack full<br/>StackOverflowError"]
  style A fill:#b45309,color:#fff
  style ERR fill:#b91c1c,color:#fff
Without a base case, frames pile up until the stack is exhausted.
Exercise

Descending. The print sits BEFORE the recursive call. Predict all four lines from countdown(3).

public class Main {
    static void countdown(int n) {
        if (n == 0) {
            System.out.println("go");
            return;
        }
        System.out.println(n);
        countdown(n - 1);
    }

    public static void main(String[] args) {
        countdown(3);
    }
}
Exercise

Return value. factorial returns n * factorial(n - 1). Predict the single line from factorial(4) — the answer is built on the way back down.

public class Main {
    static int factorial(int n) {
        if (n <= 1) {
            return 1;
        }
        return n * factorial(n - 1);
    }

    public static void main(String[] args) {
        System.out.println(factorial(4));
    }
}
Exercise

Ascending. Now the print sits AFTER the recursive call. Predict all three lines from countUp(3) — this is the mirror image of countdown.

public class Main {
    static void countUp(int n) {
        if (n == 0) {
            return;
        }
        countUp(n - 1);
        System.out.println(n);
    }

    public static void main(String[] args) {
        countUp(3);
    }
}
Exercise

A recursive method has no base case — it always calls itself with a smaller argument. What happens when it runs?

Exercise

A method is defined as static int sum(int n) { if (n == 0) return 0; return n + sum(n - 1); }. What does sum(4) return?

Recap

  • A recursive method calls itself; it needs a base case (to stop) and a recursive case that moves the input toward that base case.
  • Each call gets its own frame on the call stack, with its own parameters and locals; frames stack up as the recursion deepens and unwind in reverse as calls finish.
  • A returning recursion assembles its answer on the way back down (factorial builds 1, then 2, then 6, then 24).
  • Work done before the recursive call runs as the stack grows (descending); work done after runs as it shrinks (ascending).
  • A missing or unreachable base case overflows the stack with a StackOverflowError.

Next you will learn whether a method can change the data your caller passed in — Java's pass-by-value, for primitives and references alike.

Checkpoint quiz

Every recursive method needs two things. They are…

When a method calls itself, what does the JVM do with each call's parameters and locals?

Go deeper — technical resources