Logging with SLF4J

Why Not System.out.println?

System.out.printlnLogger
Timestamp, thread, class namenoyes, automatically
Levels (turn DEBUG off in production)noyes
Write to files, rotate them, send to ELK/Grafananoyes, by configuration
Performancealways runs, synchronizedskipped cheaply when the level is off

SLF4J + Logback

Spring Boot includes SLF4J (the logging API you code against) and Logback (the implementation that writes the logs) through spring-boot-starter-logging. Nothing to add.

package com.shopease.product;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/products")
public class ProductController {
    private static final Logger log = LoggerFactory.getLogger(ProductController.class);
    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public List<Product> all() {
        List<Product> products = productService.findAll();
        log.info("Returning {} products", products.size());        // {} = placeholder
        log.debug("Product ids: {}", products.stream().map(Product::id).toList());
        return products;
    }
}

Example

curl -s http://localhost:8080/api/products > /dev/null

Example output

2026-01-15T10:20:04.118+05:30  INFO 18244 --- [shopease] [nio-8080-exec-1] c.s.product.ProductController            : Returning 4 products

Tip: Always use {} placeholders, not string concatenation: log.debug("x=" + x) builds the string even when DEBUG is off. With Lombok, @Slf4j on the class creates the log field for you.

Reading a Log Line

2026-01-15T10:20:04.118+05:30  INFO 18244 --- [shopease] [nio-8080-exec-1] c.s.product.ProductController : Returning 4 products
|                              |    |          |          |                 |                               |
timestamp                      level PID      app name   thread            logger (class)                  message

Placeholders in Plain Java

Example

How SLF4J fills {} placeholders, and why the message is not even built when the level is disabled:

public class Main {
    static boolean debugEnabled = false;
    static int messagesBuilt = 0;

    static String format(String pattern, Object... args) {
        messagesBuilt++;
        StringBuilder sb = new StringBuilder();
        int argIndex = 0, from = 0, at;
        while ((at = pattern.indexOf("{}", from)) >= 0 && argIndex < args.length) {
            sb.append(pattern, from, at).append(args[argIndex++]);
            from = at + 2;
        }
        return sb.append(pattern.substring(from)).toString();
    }

    static void info(String pattern, Object... args) { System.out.println("INFO  " + format(pattern, args)); }

    static void debug(String pattern, Object... args) {
        if (debugEnabled) System.out.println("DEBUG " + format(pattern, args));   // skipped cheaply
    }

    public static void main(String[] args) {
        info("Order {} placed by user {} for Rs.{}", 101, 57, 2998);
        debug("Cart details: {}", "a very big object");
        info("Stock left for {}: {}", "Boat Rockerz 450", 23);
        System.out.println("Messages built: " + messagesBuilt + " (the DEBUG one was never formatted)");
    }
}

Output

INFO  Order 101 placed by user 57 for Rs.2998
INFO  Stock left for Boat Rockerz 450: 23
Messages built: 2 (the DEBUG one was never formatted)