Logging, design patterns & debugging
SLF4J over System.out, Singleton/Factory/Strategy in brief, and reading a stack trace to the root cause.
Logging: prefer SLF4J over System.out
System.out.println is fine for learning; it is a liability in production. A logging framework lets you:
- Control verbosity per level (TRACE, DEBUG, INFO, WARN, ERROR) without changing code. - Route output to files or a log aggregator — by config, not redeployment. - Include context automatically (timestamp, thread name, class name).
SLF4J is the de-facto standard facade; Logback is its most common implementation:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
private static final Logger log =
LoggerFactory.getLogger(OrderService.class);
public void process(Order order) {
log.info("Processing order id={}", order.getId());
log.debug("Order details: {}", order); // only if DEBUG enabled
}
}
The {} placeholder is lazy — the argument is only converted to String if the message will be emitted. Writing log.debug("Order: " + order) builds the string regardless; log.debug("Order: {}", order) does not.
Three everyday design patterns
Singleton — exactly one instance for the lifetime of the application. Modern Java prefers a single-element enum:
public enum Config {
INSTANCE;
private final String apiKey = System.getenv("API_KEY");
public String getApiKey() { return apiKey; }
}
Use Singleton when the instance is expensive, stateful, and legitimately shared. Avoid it for stateless utilities.
Factory — centralises object creation and hides which concrete type is returned:
Shape shape = ShapeFactory.create("circle"); // returns a Circle
Strategy — injects a behaviour at runtime via an interface, rather than baking it in with if/else. This is how Comparator, Runnable, and most functional interfaces in the JDK work:
@FunctionalInterface
interface Sorter { void sort(int[] data); }
class Report {
private final Sorter sorter;
Report(Sorter sorter) { this.sorter = sorter; }
void generate(int[] data) { sorter.sort(data); }
}
Report r = new Report(Arrays::sort);
Strategy makes code testable (inject a test double) and open for extension without modification.
Reading a stack trace
When an exception is thrown, Java prints a stack trace to standard error. Read it top to bottom:
Exception in thread "main" java.lang.NullPointerException
at com.example.OrderService.process(OrderService.java:42)
at com.example.Main.main(Main.java:15)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
- Line 1: exception type and message — often the complete diagnosis.
- First
atline in your own package — the exact line that threw. - Frames below show what called it — useful when the throw site was called with bad arguments.
Skip JDK frames (java.base/...) on your first pass. Focus on lines in your own package.