Systematic Debugging

The Scientific Debugging Loop and the Stack Trace

Debugging is not randomly poking at code with System.out.println: it is an empirical investigation cycle. A stack trace reads top to bottom until it hits the first frame of our own code.

1 Reproduce 2 Hypothesis 3 Observe 4 Fix root cause 5 Verify
Click a step in the cycle

1. Reproduce: make the failure happen consistently and on demand, never rely on luck.

2. Hypothesis: state a concrete, testable explanation of the likely cause before touching any code.

3. Observe: use breakpoints, watches, and logs to confirm or discard the hypothesis with real evidence.

4. Fix root cause: repair the origin of the problem, never the superficial symptom.

5. Verify: confirm the failure is gone and that nothing else broke.

Simulator: Stack Trace Anatomy Click each part to understand the trace
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Order.subtotal()"
    at OrderCalculator.total(OrderCalculator.java:14) ◄ relevant frame
    at Main.main(Main.java:9)

The exception type names the error class: NullPointerException means a null reference was used.

The message details exactly which call failed: Order.subtotal() invoked on a null Order object.

The call stack reads top to bottom: the first frame belonging to our own code (OrderCalculator.java:14) is where investigation should start.

IDE Tooling

The Debugger Cockpit

A debugger freezes the JVM at runtime and lets you inspect live memory state without touching the source file. Try the stepping controls over the code.

OrderCalculator.java
? public int total(Order order) { int subtotal = 0; for (Item item : order.getItems()) { subtotal += item.getPrecio(); if (itemCount > 10) { subtotal = applyBulkDiscount(subtotal); } } return subtotal;
Unconditional breakpoint Conditional breakpoint: itemCount > 10
Inside applyBulkDiscount(int subtotal)
Local Variables
orderOrder@0x4f2a
itemCount3
subtotal0.0
Watches
order.getItems().size()3

Stopped at breakpoint · line 3

Weak Design

Code Smells Gallery

A code smell does not stop the code from compiling: it signals the code will be fragile and costly to maintain later. Pick one to see its code excerpt and technical prescription.

A method that does too much, line after line, impossible to read at a glance.

public void procesarPedido(Pedido pedido) {
    // valida cliente...
    // valida stock...
    // calcula subtotal...
    // aplica descuentos...
    // calcula impuestos...
    // ... 150 líneas más ...
    // genera factura y notifica
}
Prescription: Extract Method — split each responsibility block into its own, descriptively named method.

Modeling domain concepts with loose primitive types instead of objects with their own meaning.

String telefono;
double precio;
String moneda;
Prescription: Introduce PhoneNumber and Money — value objects that validate and group their own behavior.

A method more interested in another class’s data than its own.

// dentro de Factura
double total() {
    return cliente.getPrecioBase()
        * cliente.getFactorDescuento()
        + cliente.getRecargoEnvio();
}
Prescription: Move Method — relocate the calculation into Cliente, the real owner of that data.

A class that absorbed responsibilities from the whole system.

class SistemaManager {
    // 50 métodos: login(), facturar(),
    // enviarEmail(), generarReporte()...
    // 3.000 líneas en total
}
Prescription: Split by single responsibility: AuthService, BillingService, NotificationService...
Safe Refactoring

JUnit’s Safety Belt

Refactoring means changing internal structure without changing observable behavior. Golden rule: never refactor code without tests in the green.

Step 1
JUnit Suite in Green

Guarantee that current behavior is covered by passing tests.

Step 2
One Small Change

Apply a single minimal structural change, e.g. Extract Method via the IDE shortcut.

Step 3
Run the Suite Again
Still green: step complete, atomic commit.
Turned red: revert immediately with git checkout or Ctrl+Z. Never stack up errors.
Pyramid of Doom (4 levels)
public String obtenerDescuento(Cliente cliente) {
    if (cliente != null) {
        if (cliente.isActivo()) {
            if (cliente.getAntiguedad() > 5) {
                if (cliente.getSaldo() > 0) {
                    return "20%";
                }
            }
        }
    }
    return "0%";
}
Guard Clauses (early returns)
public String obtenerDescuento(Cliente cliente) {
    if (cliente == null) return "0%";
    if (!cliente.isActivo()) return "0%";
    if (cliente.getAntiguedad() <= 5) return "0%";
    if (cliente.getSaldo() <= 0) return "0%";

    return "20%";
}
Case Study

From Spaghetti Code to Clean Code

A billing function with multiple responsibilities and magic numbers, transformed into a modular, testable, self-documenting design.

Before 45 lines · 3 levels of nesting
public double calcularTotal(List<Object> items, boolean flag) {
    double aux = 0;
    for (int i = 0; i < items.size(); i++) {
        Object it = items.get(i);
        if (it != null) {
            if (flag == true) {
                double precio = (double) ((Map) it).get("precio");
                int cantidad = (int) ((Map) it).get("cantidad");
                aux = aux + (precio * cantidad);
            }
        }
    }
    aux = aux + (aux * 0.21);
    if (aux > 1000) {
        aux = aux - (aux * 0.05);
    }
    return aux;
    // ... 30 líneas más de validaciones ...
}
After 12 lines · no magic numbers
private static final double IVA_GENERAL = 0.21;
private static final double UMBRAL_DESCUENTO = 1000.0;

public double calcularTotal(List<ItemFactura> items) {
    double subtotal = calcularSubtotal(items);
    double conIva = subtotal + (subtotal * IVA_GENERAL);
    return aplicarDescuento(conIva);
}

private double calcularDescuentoVolumen(double total) {
    return total > UMBRAL_DESCUENTO
        ? total * 0.05
        : 0;
}