log4j

1. Why Logging?

Imagine you are a detective investigating a crime scene. Without evidence — fingerprints, security camera footage, witness statements — you would have no way to reconstruct what happened. Logging is the evidence trail for your application. It records what your program did, when it did it, and what went wrong.

Logging is the practice of recording messages from your application during runtime. These messages capture events, errors, state changes, and diagnostic information that help you understand your application’s behavior — especially when things go wrong in production at 3 AM and you cannot attach a debugger.

Why Not System.out.println?

Every Java developer starts with System.out.println() for debugging. It works, but it is the equivalent of using a flashlight when you need a full surveillance system. Here is why it falls short in real applications:

Feature System.out.println Logging Framework
Severity levels None — everything looks the same TRACE, DEBUG, INFO, WARN, ERROR
On/off control Must delete or comment out lines Change config file, no code changes
Output destination Console only Console, files, databases, remote servers
Timestamps Must add manually Automatic
Thread info Must add manually Automatic
Class/method info Must add manually Automatic
File rotation Not possible Automatic (e.g., daily, by size)
Performance Always executes string building Lazy evaluation, skip if level disabled
Production ready No Yes

What to Log

  • Application startup and shutdown — configuration loaded, services initialized, graceful shutdown
  • Business events — order placed, payment processed, user registered
  • Errors and exceptions — failed database connections, invalid input, timeout errors
  • Warnings — deprecated API usage, retry attempts, approaching resource limits
  • Performance data — request duration, query execution time, cache hit/miss ratios
  • External system interactions — API calls sent/received, database queries, message queue operations

What NOT to Log

  • Passwords — never log user passwords, even encrypted ones
  • Credit card numbers — PCI-DSS compliance requires masking (show only last 4 digits)
  • Social Security Numbers — personally identifiable information (PII)
  • API keys and secrets — attackers read log files too
  • Session tokens — could enable session hijacking
  • Medical or health data — HIPAA compliance

A simple rule: if you would not want it on a billboard, do not log it.

// BAD: System.out.println for debugging
public class BadDebugging {
    public void processOrder(Order order) {
        System.out.println("Processing order: " + order.getId());      // No timestamp
        System.out.println("Order total: " + order.getTotal());        // No severity
        System.out.println("Sending to payment...");                   // Cannot turn off
        // These println calls will clutter production logs forever
    }
}

// GOOD: Proper logging
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GoodLogging {
    private static final Logger log = LoggerFactory.getLogger(GoodLogging.class);

    public void processOrder(Order order) {
        log.info("Processing order id={}, total={}", order.getId(), order.getTotal());
        log.debug("Sending order to payment gateway");
        // Output: 2026-02-28 10:15:32.451 [main] INFO  GoodLogging - Processing order id=12345, total=99.99
        // In production, DEBUG messages are automatically suppressed
    }
}

2. Java Logging Landscape

Java has multiple logging frameworks, which can be confusing for newcomers. Here is the landscape and how the pieces fit together:

Framework Type Description Status
java.util.logging (JUL) Implementation Built into the JDK since Java 1.4. No external dependencies needed. Active but rarely used in modern projects
Log4j 1.x Implementation Was the de facto standard for years. Uses log4j.properties or log4j.xml. END OF LIFE — Critical security vulnerability CVE-2021-44228. DO NOT USE.
Log4j 2 Implementation Complete rewrite of Log4j. Async logging, plugin architecture, modern design. Active, maintained by Apache
Logback Implementation Created by the founder of Log4j as its successor. Native SLF4J implementation. Active, default in Spring Boot
SLF4J Facade (API) Simple Logging Facade for Java. An abstraction layer — you code against SLF4J and swap implementations without changing code. Active, industry standard

The Facade Pattern: Why SLF4J Matters

Think of SLF4J like a universal remote control. You press the same buttons regardless of whether your TV is Samsung, LG, or Sony. Similarly, you write logging code using SLF4J’s API, and the actual logging is handled by whichever implementation (Logback, Log4j2) is on the classpath.

This means:

  • Your application code uses org.slf4j.Logger — never a specific implementation class
  • You can switch from Logback to Log4j2 by changing a Maven dependency — zero code changes
  • Libraries you depend on can use SLF4J too, and their logs funnel through your chosen implementation

Recommended Stack

For most Java applications in 2026, use: SLF4J (facade) + Logback (implementation). This is the default in Spring Boot and the most widely adopted combination. This tutorial will focus primarily on this stack, but we will also cover JUL and Log4j2.

3. Log Levels

Log levels let you categorize messages by severity. You can then configure your application to show only messages at or above a certain level — for example, showing everything in development but only WARN and ERROR in production.

SLF4J Log Levels (from least to most severe)

Level Purpose When to Use Example
TRACE Extremely detailed diagnostic information Step-by-step algorithm execution, variable values in loops, entering/exiting methods log.trace("Entering calculateTax with amount={}", amount)
DEBUG Detailed information useful during development SQL queries executed, cache hit/miss, intermediate calculation results, request/response payloads log.debug("Query returned {} rows in {}ms", count, elapsed)
INFO Important business or application events Application started, user logged in, order processed, scheduled job completed log.info("Order {} placed successfully by user {}", orderId, userId)
WARN Potentially harmful situations that are recoverable Retry attempts, deprecated API usage, approaching disk/memory limits, fallback to default log.warn("Payment gateway timeout, retrying (attempt {}/3)", attempt)
ERROR Serious failures that need attention Unhandled exceptions, failed database connections, data corruption, business rule violations that halt processing log.error("Failed to process payment for order {}", orderId, exception)

Level Hierarchy

Levels form a hierarchy. When you set the log level to a certain value, all messages at that level and above are logged. Messages below that level are suppressed.

Configured Level TRACE DEBUG INFO WARN ERROR
TRACE Yes Yes Yes Yes Yes
DEBUG No Yes Yes Yes Yes
INFO No No Yes Yes Yes
WARN No No No Yes Yes
ERROR No No No No Yes

Rule of thumb: Development uses DEBUG or TRACE. Production uses INFO (or WARN for very high-throughput systems). You should be able to understand what your application is doing from INFO logs alone.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogLevelDemo {
    private static final Logger log = LoggerFactory.getLogger(LogLevelDemo.class);

    public void processPayment(String orderId, double amount) {
        log.trace("Entering processPayment(orderId={}, amount={})", orderId, amount);

        log.debug("Validating payment amount: {}", amount);

        if (amount <= 0) {
            log.warn("Invalid payment amount {} for order {}, using minimum $0.01", amount, orderId);
            amount = 0.01;
        }

        try {
            log.info("Processing payment of ${} for order {}", amount, orderId);
            // ... payment logic ...
            log.info("Payment successful for order {}", orderId);
        } catch (Exception e) {
            log.error("Payment failed for order {} with amount ${}", orderId, amount, e);
            // The exception 'e' is passed as the LAST argument -- SLF4J will print the full stack trace
        }

        log.trace("Exiting processPayment for order {}", orderId);
    }
}
// If level is set to INFO, output would be:
// 2026-02-28 10:30:00.123 [main] INFO  LogLevelDemo - Processing payment of $49.99 for order ORD-001
// 2026-02-28 10:30:00.456 [main] INFO  LogLevelDemo - Payment successful for order ORD-001
// (TRACE and DEBUG messages are suppressed)

4. java.util.logging (JUL)

Java includes a built-in logging framework in the java.util.logging package. It requires no external dependencies, which makes it a good starting point for learning and for simple applications where you want zero third-party libraries.

JUL Log Levels

JUL uses its own level names, which differ from SLF4J:

JUL Level SLF4J Equivalent Description
FINEST TRACE Highly detailed tracing
FINER TRACE Fairly detailed tracing
FINE DEBUG General debugging
CONFIG - Configuration info
INFO INFO Informational messages
WARNING WARN Potential problems
SEVERE ERROR Serious failures
import java.util.logging.Level;
import java.util.logging.Logger;

public class JulExample {
    // Create a logger named after the class
    private static final Logger logger = Logger.getLogger(JulExample.class.getName());

    public static void main(String[] args) {
        // Basic logging at different levels
        logger.info("Application starting");
        logger.warning("Configuration file not found, using defaults");
        logger.severe("Database connection failed!");

        // Parameterized logging (JUL uses {0}, {1} style -- not {} like SLF4J)
        String user = "alice";
        int loginAttempts = 3;
        logger.log(Level.INFO, "User {0} logged in after {1} attempts", new Object[]{user, loginAttempts});

        // Logging an exception
        try {
            int result = 10 / 0;
        } catch (ArithmeticException e) {
            logger.log(Level.SEVERE, "Division error occurred", e);
        }

        // Check if level is enabled before expensive operations
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("Debug data: " + expensiveToString());
        }
    }

    private static String expensiveToString() {
        // Imagine this method is costly to call
        return "detailed debug information";
    }
}
// Output:
// Feb 28, 2026 10:45:00 AM JulExample main
// INFO: Application starting
// Feb 28, 2026 10:45:00 AM JulExample main
// WARNING: Configuration file not found, using defaults
// Feb 28, 2026 10:45:00 AM JulExample main
// SEVERE: Database connection failed!

JUL Limitations

While JUL works for simple cases, it has significant drawbacks compared to modern frameworks:

  • Verbose API -- logger.log(Level.INFO, "msg {0}", new Object[]{val}) vs. SLF4J's log.info("msg {}", val)
  • Limited formatting -- the default output format is ugly and multi-line (class name and method on separate lines)
  • Poor configuration -- uses a global logging.properties file that is awkward to customize per-package
  • No native support for modern features -- no built-in JSON output, no MDC (Mapped Diagnostic Context), no async logging
  • Performance -- slower than Logback and Log4j2 for high-throughput scenarios

Verdict: Use JUL for quick scripts or when you truly cannot add dependencies. For any real application, use SLF4J + Logback.

5. SLF4J + Logback (Recommended)

SLF4J (Simple Logging Facade for Java) + Logback is the most popular logging stack in the Java ecosystem. Spring Boot uses it by default. SLF4J provides the API you code against; Logback provides the engine that does the actual logging.

5.1 Maven Dependencies



    
    
        org.slf4j
        slf4j-api
        2.0.16
    

    
    
        ch.qos.logback
        logback-classic
        1.5.15
    
// Gradle: Add to build.gradle
dependencies {
    implementation 'org.slf4j:slf4j-api:2.0.16'
    implementation 'ch.qos.logback:logback-classic:1.5.15'
}

5.2 Basic Setup

The setup follows a consistent two-step pattern in every class:

  1. Import org.slf4j.Logger and org.slf4j.LoggerFactory
  2. Create a private static final Logger field using LoggerFactory.getLogger(YourClass.class)

Passing the class to getLogger() means the logger is named after your class, so log output shows exactly which class produced each message.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UserService {
    // Step 1: Declare the logger -- always private static final
    private static final Logger log = LoggerFactory.getLogger(UserService.class);

    public User findUserById(long id) {
        log.info("Looking up user with id={}", id);

        User user = userRepository.findById(id);

        if (user == null) {
            log.warn("User not found for id={}", id);
            return null;
        }

        log.debug("Found user: name={}, email={}", user.getName(), user.getEmail());
        return user;
    }
}
// Output with INFO level:
// 2026-02-28 10:30:00.123 [main] INFO  c.e.service.UserService - Looking up user with id=42
// 2026-02-28 10:30:00.125 [main] WARN  c.e.service.UserService - User not found for id=42

5.3 Parameterized Logging with {} Placeholders

This is one of SLF4J's most important features. Never use string concatenation in log statements. Use {} placeholders instead.

Why? With string concatenation, Java builds the string every time, even if the log level is disabled. With placeholders, SLF4J only builds the string if the message will actually be logged.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ParameterizedLogging {
    private static final Logger log = LoggerFactory.getLogger(ParameterizedLogging.class);

    public void demonstrate(Order order) {
        // BAD: String concatenation -- always builds the string, even if DEBUG is off
        log.debug("Processing order " + order.getId() + " for user " + order.getUserId()
                + " with " + order.getItems().size() + " items");

        // GOOD: Parameterized logging -- string built ONLY if DEBUG is enabled
        log.debug("Processing order {} for user {} with {} items",
                order.getId(), order.getUserId(), order.getItems().size());

        // Multiple placeholders -- they are filled in order
        log.info("User {} placed order {} with total ${}", "alice", "ORD-123", 99.99);
        // Output: User alice placed order ORD-123 with total $99.99

        // Logging exceptions -- exception is ALWAYS the last argument
        try {
            processPayment(order);
        } catch (Exception e) {
            // The exception goes last -- SLF4J recognizes it and prints the full stack trace
            log.error("Payment failed for order {}", order.getId(), e);
            // Output:
            // 2026-02-28 10:30:00.123 [main] ERROR ParameterizedLogging - Payment failed for order ORD-123
            // java.lang.RuntimeException: Insufficient funds
            //     at ParameterizedLogging.processPayment(ParameterizedLogging.java:35)
            //     at ParameterizedLogging.demonstrate(ParameterizedLogging.java:22)
            //     ...
        }
    }
}

5.4 Logging Exceptions Correctly

When logging exceptions, always pass the exception object as the last argument. SLF4J will automatically print the full stack trace. This is the single most important logging pattern to get right.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ExceptionLogging {
    private static final Logger log = LoggerFactory.getLogger(ExceptionLogging.class);

    public void demonstrateExceptionLogging() {
        try {
            riskyOperation();
        } catch (Exception e) {
            // BAD: Loses the stack trace entirely
            log.error("Something failed");

            // BAD: Only logs the exception message, no stack trace
            log.error("Something failed: " + e.getMessage());

            // BAD: Converts stack trace to string manually -- ugly and loses structure
            log.error("Something failed: " + e.toString());

            // GOOD: Pass exception as the last argument -- full stack trace is printed
            log.error("Something failed", e);

            // GOOD: With context AND exception -- placeholders first, exception last
            log.error("Failed to process order {} for user {}", orderId, userId, e);
            // SLF4J knows the last argument is an exception because {} count (2) < argument count (3)
        }
    }
}

6. Log4j2

Log4j2 is the modern successor to Log4j 1.x, built from the ground up by Apache. It is a completely different codebase from Log4j 1.x.

Critical Warning: Log4j 1.x (versions 1.2.x) reached end of life in 2015 and has the critical Log4Shell vulnerability (CVE-2021-44228), one of the most severe security vulnerabilities in Java history. If you are using Log4j 1.x, you must migrate immediately. Log4j2 (versions 2.x) is the safe, modern version.

6.1 Maven Dependencies



    
    
        org.slf4j
        slf4j-api
        2.0.16
    

    
    
        org.apache.logging.log4j
        log4j-slf4j2-impl
        2.24.3
    

    
    
        org.apache.logging.log4j
        log4j-core
        2.24.3
    

6.2 Log4j2 Configuration (log4j2.xml)

Place this file in src/main/resources/log4j2.xml:



    
        
        
            
        

        
        
            
            
                
                
            
            
        
    

    
        
        
            
            
        

        
        
            
        
    

6.3 Log4j2 Async Logging

Log4j2's standout feature is its async logging capability using the LMAX Disruptor library. This can dramatically improve performance in high-throughput applications by logging on a separate thread.



    com.lmax
    disruptor
    4.0.0



    
        
            
        
    

    
        
        
            
        

        
            
        
    

6.4 When to Choose Log4j2 Over Logback

Feature Logback Log4j2
Spring Boot default Yes No (requires exclusion + config)
Async performance Good (AsyncAppender) Excellent (LMAX Disruptor)
Garbage-free logging No Yes (reduces GC pauses)
Lambda support No Yes (lazy message construction)
Plugin architecture Limited Extensive
Community adoption Higher (Spring ecosystem) Strong (Apache ecosystem)
Configuration reload Yes Yes (automatic)

Bottom line: Use Logback for most applications, especially with Spring Boot. Choose Log4j2 if you need maximum throughput with async logging (e.g., high-frequency trading, real-time data pipelines).

7. Logback Configuration

Logback is configured via an XML file named logback.xml (or logback-spring.xml in Spring Boot) placed in src/main/resources/. The configuration has three main components:

  • Appenders -- Where log output goes (console, files, remote servers)
  • Encoders/Patterns -- How log messages are formatted
  • Loggers -- Which packages/classes log at which level

7.1 Basic logback.xml




    
    
    
    
        
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
        
    

    
    
    
    
        logs/application.log
        
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
        
    

    
    
    
    
        logs/application.log

        
        
            logs/application.%d{yyyy-MM-dd}.%i.log.gz
            10MB
            30          
            1GB     
        

        
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
        
    

    
    
    

    
    
        
        
    

    
    

    
    
    

    
    
        
    

7.2 Encoder Pattern Reference

The pattern string controls how each log message is formatted. Here are the most common conversion specifiers:

Specifier Output Example
%d{pattern} Date/time %d{yyyy-MM-dd HH:mm:ss.SSS} = 2026-02-28 10:30:00.123
%level or %-5level Log level (padded to 5 chars) INFO, DEBUG, ERROR
%logger{n} Logger name (abbreviated to n chars) %logger{36} = c.e.service.UserService
%msg The log message User logged in successfully
%n Newline (platform-specific) \n or \r\n
%thread Thread name main, http-nio-8080-exec-1
%class Full class name (slow) com.example.service.UserService
%method Method name (slow) findUserById
%line Line number (slow) 42
%X{key} MDC value %X{requestId} = abc-123
%highlight() ANSI color by level (console only) ERROR in red, WARN in yellow

Performance note: %class, %method, and %line are computed by generating a stack trace, which is expensive. Avoid them in production patterns.

7.3 Common Patterns

// Development pattern (human-readable with colors)
%d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%logger{36}) - %msg%n
// Output: 10:30:00.123 INFO  c.e.service.UserService - Order placed

// Production pattern (full detail, no color)
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
// Output: 2026-02-28 10:30:00.123 [http-nio-8080-exec-1] INFO  c.e.service.UserService - Order placed

// Production with MDC (request tracking)
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [requestId=%X{requestId}] - %msg%n
// Output: 2026-02-28 10:30:00.123 [http-nio-8080-exec-1] INFO  c.e.service.UserService [requestId=abc-123-def] - Order placed

// JSON pattern for ELK/Splunk (see Section 12)
{"timestamp":"%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ}","level":"%level","logger":"%logger","thread":"%thread","message":"%msg","requestId":"%X{requestId}"}%n

7.4 Filtering by Package

One of the most powerful configuration features is setting different log levels for different packages. This lets you see detailed logs from your code while keeping framework noise quiet.


    
        
            %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n
        
    

    
    

    
    

    
    

    
    

    
    

    
    

    
    
        
    

8. Logging Patterns and Formats

The format of your log messages matters more than you might think. In development, you want human-readable output. In production, you often want structured (JSON) output that can be parsed by log aggregation tools like the ELK stack (Elasticsearch, Logstash, Kibana) or Splunk.

8.1 Pattern Format Quick Reference

Environment Pattern Why
Development %d{HH:mm:ss} %-5level %logger{20} - %msg%n Short, readable, fast to scan
Staging %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n Full detail for debugging issues that match production
Production (text) %d{ISO8601} [%thread] %-5level %logger{36} [%X{requestId}] - %msg%n ISO timestamps, MDC context, full logger names
Production (JSON) Use Logstash encoder (see below) Machine-parseable for log aggregation

8.2 JSON Logging for Production

For production environments using ELK stack, Splunk, or Datadog, structured JSON logs are essential. Each log line is a valid JSON object that these tools can parse, index, and search.



    net.logstash.logback
    logstash-logback-encoder
    8.0


    
    
        
            
            requestId
            userId
        
    

    
        
    

With JSON logging, each log line looks like this:

{"@timestamp":"2026-02-28T10:30:00.123Z","@version":"1","message":"Order ORD-123 placed successfully","logger_name":"com.myapp.service.OrderService","thread_name":"http-nio-8080-exec-1","level":"INFO","requestId":"abc-123-def","userId":"user-42"}

This structured output means you can search for all logs where userId="user-42" or find all ERROR-level messages for a specific requestId -- something that is extremely difficult with plain text logs.

9. MDC (Mapped Diagnostic Context)

Imagine you are a doctor in a busy emergency room, treating 20 patients simultaneously. Without patient wristbands (IDs), you would have no way to tell which vitals belong to which patient. MDC is the wristband for your application's requests.

MDC (Mapped Diagnostic Context) lets you attach key-value pairs to the current thread. These values are then automatically included in every log message produced by that thread. This is invaluable in multi-threaded web applications where dozens of requests are processed concurrently.

Common MDC Fields

  • requestId -- A unique ID for each HTTP request, used to trace all log lines for one request
  • userId -- The authenticated user making the request
  • sessionId -- The user's session
  • transactionId -- For tracing business transactions across services
  • correlationId -- For tracing requests across microservices
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class MdcExample {
    private static final Logger log = LoggerFactory.getLogger(MdcExample.class);

    public void handleRequest(String requestId, String userId) {
        // Put values into MDC at the start of the request
        MDC.put("requestId", requestId);
        MDC.put("userId", userId);

        try {
            log.info("Request received");
            processOrder();
            sendConfirmation();
            log.info("Request completed successfully");
        } finally {
            // CRITICAL: Always clear MDC when the request is done
            // Threads are reused in thread pools -- leftover MDC values leak into other requests!
            MDC.clear();
        }
    }

    private void processOrder() {
        // This log line automatically includes requestId and userId from MDC
        log.info("Processing order");
        // Output: 2026-02-28 10:30:00.123 [http-exec-1] INFO  MdcExample [requestId=abc-123, userId=user-42] - Processing order
    }

    private void sendConfirmation() {
        log.info("Sending confirmation email");
        // Output: 2026-02-28 10:30:00.456 [http-exec-1] INFO  MdcExample [requestId=abc-123, userId=user-42] - Sending confirmation email
    }
}

9.1 MDC with Web Applications (Servlet Filter)

In real applications, you set up MDC in a servlet filter or Spring interceptor so that every request automatically gets a unique ID. You never have to manually add MDC in individual controllers or services.

import org.slf4j.MDC;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

public class LoggingFilter implements Filter {
    private static final String REQUEST_ID = "requestId";
    private static final String USER_ID = "userId";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;

        try {
            // Generate or extract request ID
            String requestId = httpRequest.getHeader("X-Request-ID");
            if (requestId == null || requestId.isBlank()) {
                requestId = UUID.randomUUID().toString().substring(0, 8);
            }

            // Set MDC values
            MDC.put(REQUEST_ID, requestId);

            // Extract user from security context (if authenticated)
            String userId = extractUserId(httpRequest);
            if (userId != null) {
                MDC.put(USER_ID, userId);
            }

            // Continue processing the request
            chain.doFilter(request, response);

        } finally {
            // Always clean up to prevent thread pool contamination
            MDC.clear();
        }
    }

    private String extractUserId(HttpServletRequest request) {
        // In a real app, extract from security context or JWT token
        return request.getRemoteUser();
    }
}

9.2 MDC in logback.xml

To display MDC values in your log output, use the %X{key} specifier in your pattern:


    
        
            
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [req=%X{requestId} user=%X{userId}] - %msg%n
        
    

    
        
    


10. Best Practices

These are the logging practices that separate junior developers from senior developers. Follow these in every Java project.

10.1 Use SLF4J as Your Logging Facade

Always code against the SLF4J API, never a specific implementation. This gives you the freedom to switch between Logback, Log4j2, or any future implementation without touching your code.

// BAD: Coupling to a specific implementation
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
private static final Logger log = LogManager.getLogger(MyClass.class);

// BAD: Using java.util.logging directly
import java.util.logging.Logger;
private static final Logger log = Logger.getLogger(MyClass.class.getName());

// GOOD: SLF4J facade -- works with ANY implementation
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger log = LoggerFactory.getLogger(MyClass.class);

10.2 Use Parameterized Logging

This is the single most common logging mistake in Java code reviews. Never concatenate strings in log statements.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ParameterizedBestPractice {
    private static final Logger log = LoggerFactory.getLogger(ParameterizedBestPractice.class);

    public void process(Order order) {
        // BAD: String concatenation -- always builds the string even if DEBUG is off
        log.debug("Order " + order.getId() + " has " + order.getItems().size() + " items totaling $" + order.getTotal());
        // This calls order.getId(), order.getItems().size(), and order.getTotal()
        // PLUS concatenates 5 strings -- all wasted work if DEBUG is disabled

        // GOOD: Parameterized -- only builds string if DEBUG is enabled
        log.debug("Order {} has {} items totaling ${}", order.getId(), order.getItems().size(), order.getTotal());
    }
}

10.3 Log at the Appropriate Level

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AppropriateLevel {
    private static final Logger log = LoggerFactory.getLogger(AppropriateLevel.class);

    public void processOrder(Order order) {
        // TRACE: Very fine-grained, method entry/exit
        log.trace("Entering processOrder with order={}", order);

        // DEBUG: Technical detail helpful during development
        log.debug("Validating order items against inventory");

        // INFO: Business event -- this is what operations teams monitor
        log.info("Order {} placed by user {} for ${}", order.getId(), order.getUserId(), order.getTotal());

        // WARN: Something unusual but recoverable
        if (order.getTotal() > 10000) {
            log.warn("High-value order {} for ${} -- flagged for review", order.getId(), order.getTotal());
        }

        // ERROR: Something failed -- needs human attention
        try {
            chargePayment(order);
        } catch (PaymentException e) {
            log.error("Payment failed for order {} with amount ${}", order.getId(), order.getTotal(), e);
        }
    }
}

10.4 Include Context in Messages

A log message without context is like a clue without a case number. Always include the relevant IDs and values that will help you investigate.

// BAD: No context -- useless for debugging
log.error("Payment failed");
log.info("User logged in");
log.warn("Retry attempt");

// GOOD: Context-rich -- you can trace exactly what happened
log.error("Payment failed for order={} user={} amount=${} gateway={}", orderId, userId, amount, gateway);
log.info("User {} logged in from IP {} using {}", userId, ipAddress, userAgent);
log.warn("Retry attempt {}/{} for order={} after {}ms delay", attempt, maxRetries, orderId, delay);

10.5 Use isDebugEnabled() for Expensive Operations

While parameterized logging avoids string concatenation overhead, it does not avoid the cost of computing the arguments. If computing an argument is expensive, guard the log statement.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ExpensiveLogging {
    private static final Logger log = LoggerFactory.getLogger(ExpensiveLogging.class);

    public void processLargeDataSet(List records) {
        // BAD: computeStats() is called EVERY TIME, even when DEBUG is off
        log.debug("Dataset statistics: {}", computeStats(records));
        // computeStats() might iterate over millions of records

        // GOOD: Guard expensive computation
        if (log.isDebugEnabled()) {
            log.debug("Dataset statistics: {}", computeStats(records));
        }

        // ALSO GOOD for simple arguments -- no guard needed
        log.debug("Processing {} records", records.size());
        // records.size() is O(1) and trivially cheap
    }

    private String computeStats(List records) {
        // Imagine this iterates the entire list, computes averages, etc.
        return "min=1, max=100, avg=42.5, stddev=12.3";
    }
}

10.6 Do Not Log Sensitive Data

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SensitiveDataLogging {
    private static final Logger log = LoggerFactory.getLogger(SensitiveDataLogging.class);

    public void authenticateUser(String username, String password) {
        // BAD: NEVER log passwords
        log.info("Login attempt: user={}, password={}", username, password);

        // GOOD: Log the event without sensitive data
        log.info("Login attempt for user={}", username);
    }

    public void processPayment(String creditCardNumber, double amount) {
        // BAD: NEVER log full credit card numbers
        log.info("Charging card {} for ${}", creditCardNumber, amount);

        // GOOD: Mask the sensitive data
        String masked = maskCreditCard(creditCardNumber);
        log.info("Charging card {} for ${}", masked, amount);
        // Output: Charging card ****-****-****-4242 for $99.99
    }

    private String maskCreditCard(String number) {
        if (number == null || number.length() < 4) return "****";
        return "****-****-****-" + number.substring(number.length() - 4);
    }
}

10.7 Do Not Log Inside Tight Loops

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoopLogging {
    private static final Logger log = LoggerFactory.getLogger(LoopLogging.class);

    public void processRecords(List records) {
        // BAD: Logging inside a loop with 1 million records = 1 million log lines
        for (Record record : records) {
            log.debug("Processing record: {}", record.getId());
            process(record);
        }

        // GOOD: Log summary information
        log.info("Starting to process {} records", records.size());
        int successCount = 0;
        int failCount = 0;

        for (Record record : records) {
            try {
                process(record);
                successCount++;
            } catch (Exception e) {
                failCount++;
                // Only log individual failures -- these are exceptional
                log.warn("Failed to process record {}: {}", record.getId(), e.getMessage());
            }
        }

        log.info("Completed processing: {} succeeded, {} failed out of {} total",
                successCount, failCount, records.size());
    }
}

Best Practices Summary

Practice Do Do Not
API Use SLF4J facade Use implementation-specific API (JUL, Log4j directly)
Parameters log.info("User {}", userId) log.info("User " + userId)
Exceptions log.error("Msg", exception) log.error("Msg: " + e.getMessage())
Levels INFO for business events, DEBUG for technical details Everything at INFO or everything at DEBUG
Context Include IDs, amounts, counts Vague messages like "Error occurred"
MDC Set requestId/userId in filter Manually add IDs to every message
Sensitive data Mask or omit Log passwords, credit cards, tokens
Loops Log summary before/after Log every iteration
Guards if (log.isDebugEnabled()) for expensive computation Call expensive methods as log arguments
Logger declaration private static final Logger Creating new Logger per method call

11. Common Mistakes

Every experienced Java developer has made these mistakes. Recognizing them in code reviews will make you a better developer.

Mistake 1: Using System.out.println Instead of a Logger

// MISTAKE: System.out.println scattered through production code
public class OrderService {
    public void placeOrder(Order order) {
        System.out.println("Placing order: " + order);        // No level, no timestamp, no thread
        System.out.println("Validating...");                   // Cannot turn off without deleting
        System.out.println("Done!");                           // Goes to stdout only
    }
}

// FIX: Use a proper logger
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    public void placeOrder(Order order) {
        log.info("Placing order {}", order.getId());
        log.debug("Validating order items");
        log.info("Order {} placed successfully", order.getId());
    }
}

Mistake 2: String Concatenation in Log Statements

// MISTAKE: String concatenation is evaluated even when the level is disabled
log.debug("User " + user.getName() + " has " + user.getOrders().size() + " orders"
        + " totaling $" + calculateTotal(user.getOrders()));
// If DEBUG is off, Java still:
//   1. Calls user.getName()
//   2. Calls user.getOrders().size()
//   3. Calls calculateTotal() -- potentially expensive!
//   4. Concatenates 5 strings
//   5. Throws the result away

// FIX: Use parameterized logging
log.debug("User {} has {} orders totaling ${}",
        user.getName(), user.getOrders().size(), calculateTotal(user.getOrders()));
// With parameterized logging, if DEBUG is off, SLF4J skips building the string.
// NOTE: The arguments are still evaluated. For expensive arguments, use isDebugEnabled() guard.

Mistake 3: Not Logging Exceptions Properly

try {
    connectToDatabase();
} catch (SQLException e) {

    // MISTAKE 1: Swallowing the exception entirely
    // (empty catch block -- the worst possible thing)

    // MISTAKE 2: Only logging the message, losing the stack trace
    log.error("Database error: " + e.getMessage());
    // Output: Database error: Connection refused
    // WHERE did it fail? Which line? What was the root cause? All lost.

    // MISTAKE 3: Using printStackTrace() instead of logging
    e.printStackTrace();
    // This goes to System.err, bypassing the logging framework entirely.
    // No timestamp, no level, no file output, no MDC.

    // CORRECT: Pass the exception as the last argument
    log.error("Failed to connect to database", e);
    // Output includes the full stack trace:
    // 2026-02-28 10:30:00.123 [main] ERROR DatabaseService - Failed to connect to database
    // java.sql.SQLException: Connection refused
    //     at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:839)
    //     at com.mysql.cj.jdbc.ConnectionImpl.(ConnectionImpl.java:453)
    //     at DatabaseService.connectToDatabase(DatabaseService.java:42)
    //     ...
    // Caused by: java.net.ConnectException: Connection refused (Connection refused)
    //     at java.base/java.net.PlainSocketImpl.socketConnect(Native Method)
    //     ...
}

Mistake 4: Logging Too Much or Too Little

// MISTAKE: Logging too much -- "log diarrhea"
public double calculateTax(double amount, String state) {
    log.info("calculateTax called");                    // Noise
    log.info("amount = " + amount);                     // Noise + concatenation
    log.info("state = " + state);                       // Noise + concatenation
    double rate = getTaxRate(state);
    log.info("tax rate = " + rate);                     // Noise
    double tax = amount * rate;
    log.info("tax = " + tax);                           // Noise
    log.info("returning tax");                          // Noise
    return tax;
}
// This method generates 6 log lines for a simple calculation.
// Multiply by 1000 requests/second and you have 6000 lines/second of noise.

// MISTAKE: Logging too little
public double calculateTax(double amount, String state) {
    return amount * getTaxRate(state);
    // No logging at all. If tax calculations are wrong, where do you start?
}

// CORRECT: Log meaningful events at the right level
public double calculateTax(double amount, String state) {
    log.debug("Calculating tax for amount={} state={}", amount, state);
    double rate = getTaxRate(state);
    double tax = amount * rate;
    log.debug("Tax calculated: amount={} state={} rate={} tax={}", amount, state, rate, tax);
    return tax;
}
// Two DEBUG lines that can be turned off in production but enabled when needed.

Mistake 5: Logging Sensitive Data

// MISTAKE: Logging user data verbatim
public void registerUser(UserRegistration reg) {
    log.info("Registering user: {}", reg);
    // If UserRegistration.toString() includes password, SSN, or credit card... game over.
    // Log files are often stored in plain text, backed up to multiple servers,
    // and accessed by many team members.
}

// CORRECT: Log only safe, relevant fields
public void registerUser(UserRegistration reg) {
    log.info("Registering user: email={}", reg.getEmail());
    // Or override toString() to exclude sensitive fields:
    // @Override public String toString() {
    //     return "UserRegistration{email='" + email + "', name='" + name + "'}";
    //     // password, ssn, creditCard intentionally excluded
    // }
}

Mistake 6: Using Log4j 1.x

// MISTAKE: Still using Log4j 1.x (versions 1.2.x)
import org.apache.log4j.Logger;   // <-- This is Log4j 1.x -- SECURITY VULNERABILITY!

// Log4j 1.x reached End of Life in August 2015.
// CVE-2021-44228 (Log4Shell) allows Remote Code Execution -- attackers can take over your server.
// This is a CRITICAL vulnerability rated 10.0 out of 10.0 on the CVSS scale.

// FIX: Migrate to SLF4J + Logback (or Log4j2)
// Step 1: Remove log4j 1.x dependency
// Step 2: Add SLF4J + Logback dependencies (see Section 5)
// Step 3: Replace imports:
import org.slf4j.Logger;          // <-- SLF4J facade
import org.slf4j.LoggerFactory;

// Step 4: Replace logger creation:
// OLD: private static final Logger log = Logger.getLogger(MyClass.class);
// NEW:
private static final Logger log = LoggerFactory.getLogger(MyClass.class);

// Step 5: Replace log4j.properties with logback.xml (see Section 7)
// Step 6: The logging method calls (log.info, log.error, etc.) are almost identical

12. Logging in Production

Production logging has different requirements than development logging. In production, your logs are the primary tool for understanding what is happening across hundreds of servers processing thousands of requests per second.

12.1 Structured Logging (JSON)

In production, logs should be machine-parseable. Plain text logs like 2026-02-28 10:30 INFO OrderService - Order placed are hard for log aggregation tools to parse reliably. JSON format solves this.

With JSON logging, tools like Elasticsearch, Splunk, Datadog, and Grafana Loki can index every field and let you write queries like:

  • Show all ERROR logs from the last hour
  • Show all logs where userId = "alice" and orderId = "ORD-123"
  • Count the number of payment failures per minute
  • Alert when error rate exceeds 5% of total requests

12.2 The ELK Stack

The ELK stack (Elasticsearch, Logstash, Kibana) is the most popular open-source log aggregation platform:

Component Role Description
Elasticsearch Store and search Distributed search engine that indexes log data for fast queries
Logstash Collect and transform Ingests logs from multiple sources, parses them, and sends to Elasticsearch
Kibana Visualize Web UI for searching logs, building dashboards, and setting up alerts

12.3 Log Rotation

Without log rotation, log files grow until they fill the disk and your application crashes. Always configure rolling policies:



    logs/application.log

    
        
        logs/application.%d{yyyy-MM-dd}.%i.log.gz

        
        50MB

        
        90

        
        5GB
    

    
        %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{requestId}] - %msg%n
    

12.4 Performance Considerations

Concern Solution
High-throughput logging blocks threads Use async appenders (Logback's AsyncAppender or Log4j2's AsyncLogger)
Disk I/O bottleneck Write to a local buffer, ship to remote collector (Logstash, Fluentd)
Large stack traces Logback automatically shortens repeated stack frames with ... 42 common frames omitted
GC pressure from log string building Use parameterized logging ({}), consider Log4j2's garbage-free mode
Log file size Use GZIP compression on rolled files (.log.gz)
Too many DEBUG/TRACE in production Set root level to INFO, use DEBUG only for your packages when investigating

12.5 Async Appender Example



    
    
        logs/application.log
        
            logs/application.%d{yyyy-MM-dd}.log.gz
            30
        
        
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
        
    

    
    
        1024           
        0 
        false 
        
    

    
        
    

13. Complete Practical Example: OrderService

Let us tie everything together with a realistic, production-quality example. This OrderService demonstrates all the logging concepts we have covered: appropriate log levels, parameterized messages, exception handling, MDC for request tracking, and best practices throughout.

13.1 Dependencies (pom.xml)


    
        org.slf4j
        slf4j-api
        2.0.16
    
    
        ch.qos.logback
        logback-classic
        1.5.15
    

13.2 Configuration (logback.xml)



    
        
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30} [orderId=%X{orderId} user=%X{userId}] - %msg%n
        
    

    
        logs/orders.log
        
            logs/orders.%d{yyyy-MM-dd}.%i.log.gz
            10MB
            30
            1GB
        
        
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [orderId=%X{orderId} user=%X{userId}] - %msg%n
        
    

    
        
        
    

    
        
    

13.3 Order Model

package com.example.orders;

import java.util.List;

public class Order {
    private final String id;
    private final String userId;
    private final List items;
    private double total;
    private OrderStatus status;

    public Order(String id, String userId, List items) {
        this.id = id;
        this.userId = userId;
        this.items = items;
        this.total = items.stream().mapToDouble(OrderItem::getSubtotal).sum();
        this.status = OrderStatus.PENDING;
    }

    public String getId() { return id; }
    public String getUserId() { return userId; }
    public List getItems() { return items; }
    public double getTotal() { return total; }
    public OrderStatus getStatus() { return status; }
    public void setStatus(OrderStatus status) { this.status = status; }
    public void setTotal(double total) { this.total = total; }

    // toString excludes any sensitive user data
    @Override
    public String toString() {
        return "Order{id='" + id + "', items=" + items.size() + ", total=" + total + ", status=" + status + "}";
    }
}

enum OrderStatus { PENDING, VALIDATED, PAID, SHIPPED, CANCELLED }

class OrderItem {
    private final String productName;
    private final int quantity;
    private final double price;

    public OrderItem(String productName, int quantity, double price) {
        this.productName = productName;
        this.quantity = quantity;
        this.price = price;
    }

    public String getProductName() { return productName; }
    public int getQuantity() { return quantity; }
    public double getPrice() { return price; }
    public double getSubtotal() { return quantity * price; }
}

13.4 OrderService with Production-Quality Logging

package com.example.orders;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.util.List;

public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    private static final double HIGH_VALUE_THRESHOLD = 1000.0;
    private static final double TAX_RATE = 0.08;
    private static final double DISCOUNT_THRESHOLD = 500.0;
    private static final double DISCOUNT_RATE = 0.10;

    /**
     * Process an order end-to-end with proper logging at every stage.
     */
    public void processOrder(Order order) {
        // Set MDC context for this order -- all subsequent log lines include these values
        MDC.put("orderId", order.getId());
        MDC.put("userId", order.getUserId());

        long startTime = System.currentTimeMillis();

        try {
            // INFO: Business event -- order processing started
            log.info("Order processing started: {} items, total=${}",
                    order.getItems().size(), order.getTotal());

            // Step 1: Validate
            validateOrder(order);

            // Step 2: Apply discounts
            applyDiscounts(order);

            // Step 3: Calculate tax
            calculateTax(order);

            // Step 4: Process payment
            processPayment(order);

            // Step 5: Ship
            shipOrder(order);

            long elapsed = System.currentTimeMillis() - startTime;
            // INFO: Business event -- order completed with timing
            log.info("Order processing completed successfully in {}ms, finalTotal=${}",
                    elapsed, order.getTotal());

        } catch (Exception e) {
            long elapsed = System.currentTimeMillis() - startTime;
            order.setStatus(OrderStatus.CANCELLED);
            // ERROR: Something went wrong -- include the exception for stack trace
            log.error("Order processing failed after {}ms", elapsed, e);
        } finally {
            // CRITICAL: Always clear MDC to prevent thread contamination
            MDC.clear();
        }
    }

    private void validateOrder(Order order) {
        log.debug("Validating order");

        if (order.getItems() == null || order.getItems().isEmpty()) {
            // ERROR: Invalid input -- this should not happen if upstream validation works
            log.error("Order has no items");
            throw new IllegalArgumentException("Order must have at least one item");
        }

        for (OrderItem item : order.getItems()) {
            if (item.getQuantity() <= 0) {
                log.error("Invalid quantity {} for product '{}'",
                        item.getQuantity(), item.getProductName());
                throw new IllegalArgumentException("Quantity must be positive for: " + item.getProductName());
            }
            if (item.getPrice() < 0) {
                log.error("Negative price ${} for product '{}'",
                        item.getPrice(), item.getProductName());
                throw new IllegalArgumentException("Price cannot be negative for: " + item.getProductName());
            }
        }

        order.setStatus(OrderStatus.VALIDATED);
        // DEBUG: Technical detail about validation result
        log.debug("Order validated: {} items passed all checks", order.getItems().size());
    }

    private void applyDiscounts(Order order) {
        double originalTotal = order.getTotal();
        log.debug("Checking discounts for total=${}", originalTotal);

        if (originalTotal >= DISCOUNT_THRESHOLD) {
            double discount = originalTotal * DISCOUNT_RATE;
            order.setTotal(originalTotal - discount);
            // INFO: Business event -- discount applied (operations wants to track this)
            log.info("Discount applied: {}% off ${} = -${}, newTotal=${}",
                    (int)(DISCOUNT_RATE * 100), originalTotal, discount, order.getTotal());
        } else {
            log.debug("No discount applied: total ${} below threshold ${}",
                    originalTotal, DISCOUNT_THRESHOLD);
        }
    }

    private void calculateTax(Order order) {
        double beforeTax = order.getTotal();
        double tax = beforeTax * TAX_RATE;
        order.setTotal(beforeTax + tax);
        // DEBUG: Technical calculation detail
        log.debug("Tax calculated: ${} * {} = ${}, newTotal=${}",
                beforeTax, TAX_RATE, tax, order.getTotal());
    }

    private void processPayment(Order order) {
        // INFO: Business event -- payment attempt
        log.info("Processing payment of ${}", order.getTotal());

        // WARN: Flag high-value orders
        if (order.getTotal() > HIGH_VALUE_THRESHOLD) {
            log.warn("High-value order detected: ${} exceeds threshold ${}",
                    order.getTotal(), HIGH_VALUE_THRESHOLD);
        }

        // Simulate payment processing
        try {
            simulatePaymentGateway(order);
            order.setStatus(OrderStatus.PAID);
            log.info("Payment processed successfully for ${}", order.getTotal());
        } catch (RuntimeException e) {
            // ERROR: Payment failed -- include the exception
            log.error("Payment gateway rejected transaction for ${}", order.getTotal(), e);
            throw e;
        }
    }

    private void simulatePaymentGateway(Order order) {
        // Simulate: orders with total over $5000 fail (for demo purposes)
        if (order.getTotal() > 5000) {
            throw new RuntimeException("Payment declined: exceeds single transaction limit");
        }
        log.debug("Payment gateway returned: APPROVED");
    }

    private void shipOrder(Order order) {
        log.info("Initiating shipment");
        order.setStatus(OrderStatus.SHIPPED);
        log.info("Order shipped to user {}", order.getUserId());
    }
}

13.5 Running the Example

package com.example.orders;

import java.util.List;

public class OrderApp {
    public static void main(String[] args) {
        OrderService service = new OrderService();

        // Scenario 1: Normal order
        System.out.println("=== Scenario 1: Normal Order ===");
        Order normalOrder = new Order("ORD-001", "alice",
                List.of(new OrderItem("Laptop Stand", 1, 45.99),
                        new OrderItem("USB-C Cable", 2, 12.99)));
        service.processOrder(normalOrder);

        System.out.println();

        // Scenario 2: High-value order with discount
        System.out.println("=== Scenario 2: High-Value Order ===");
        Order highValue = new Order("ORD-002", "bob",
                List.of(new OrderItem("MacBook Pro", 1, 2499.00),
                        new OrderItem("AppleCare+", 1, 399.00)));
        service.processOrder(highValue);

        System.out.println();

        // Scenario 3: Order that exceeds payment limit (will fail)
        System.out.println("=== Scenario 3: Failed Payment ===");
        Order tooExpensive = new Order("ORD-003", "charlie",
                List.of(new OrderItem("Server Rack", 3, 2500.00)));
        service.processOrder(tooExpensive);

        System.out.println();

        // Scenario 4: Invalid order (empty items)
        System.out.println("=== Scenario 4: Invalid Order ===");
        Order emptyOrder = new Order("ORD-004", "dave", List.of());
        service.processOrder(emptyOrder);
    }
}

13.6 Expected Output

=== Scenario 1: Normal Order ===
2026-02-28 10:30:00.001 [main] INFO  c.e.orders.OrderService [orderId=ORD-001 user=alice] - Order processing started: 2 items, total=$71.97
2026-02-28 10:30:00.002 [main] DEBUG c.e.orders.OrderService [orderId=ORD-001 user=alice] - Validating order
2026-02-28 10:30:00.002 [main] DEBUG c.e.orders.OrderService [orderId=ORD-001 user=alice] - Order validated: 2 items passed all checks
2026-02-28 10:30:00.002 [main] DEBUG c.e.orders.OrderService [orderId=ORD-001 user=alice] - Checking discounts for total=$71.97
2026-02-28 10:30:00.002 [main] DEBUG c.e.orders.OrderService [orderId=ORD-001 user=alice] - No discount applied: total $71.97 below threshold $500.0
2026-02-28 10:30:00.003 [main] DEBUG c.e.orders.OrderService [orderId=ORD-001 user=alice] - Tax calculated: $71.97 * 0.08 = $5.7576, newTotal=$77.7276
2026-02-28 10:30:00.003 [main] INFO  c.e.orders.OrderService [orderId=ORD-001 user=alice] - Processing payment of $77.7276
2026-02-28 10:30:00.003 [main] DEBUG c.e.orders.OrderService [orderId=ORD-001 user=alice] - Payment gateway returned: APPROVED
2026-02-28 10:30:00.003 [main] INFO  c.e.orders.OrderService [orderId=ORD-001 user=alice] - Payment processed successfully for $77.7276
2026-02-28 10:30:00.003 [main] INFO  c.e.orders.OrderService [orderId=ORD-001 user=alice] - Initiating shipment
2026-02-28 10:30:00.003 [main] INFO  c.e.orders.OrderService [orderId=ORD-001 user=alice] - Order shipped to user alice
2026-02-28 10:30:00.004 [main] INFO  c.e.orders.OrderService [orderId=ORD-001 user=alice] - Order processing completed successfully in 3ms, finalTotal=$77.7276

=== Scenario 2: High-Value Order ===
2026-02-28 10:30:00.005 [main] INFO  c.e.orders.OrderService [orderId=ORD-002 user=bob] - Order processing started: 2 items, total=$2898.0
2026-02-28 10:30:00.005 [main] DEBUG c.e.orders.OrderService [orderId=ORD-002 user=bob] - Validating order
2026-02-28 10:30:00.005 [main] DEBUG c.e.orders.OrderService [orderId=ORD-002 user=bob] - Order validated: 2 items passed all checks
2026-02-28 10:30:00.005 [main] DEBUG c.e.orders.OrderService [orderId=ORD-002 user=bob] - Checking discounts for total=$2898.0
2026-02-28 10:30:00.005 [main] INFO  c.e.orders.OrderService [orderId=ORD-002 user=bob] - Discount applied: 10% off $2898.0 = -$289.8, newTotal=$2608.2
2026-02-28 10:30:00.006 [main] DEBUG c.e.orders.OrderService [orderId=ORD-002 user=bob] - Tax calculated: $2608.2 * 0.08 = $208.656, newTotal=$2816.856
2026-02-28 10:30:00.006 [main] INFO  c.e.orders.OrderService [orderId=ORD-002 user=bob] - Processing payment of $2816.856
2026-02-28 10:30:00.006 [main] WARN  c.e.orders.OrderService [orderId=ORD-002 user=bob] - High-value order detected: $2816.856 exceeds threshold $1000.0
2026-02-28 10:30:00.006 [main] DEBUG c.e.orders.OrderService [orderId=ORD-002 user=bob] - Payment gateway returned: APPROVED
2026-02-28 10:30:00.006 [main] INFO  c.e.orders.OrderService [orderId=ORD-002 user=bob] - Payment processed successfully for $2816.856
2026-02-28 10:30:00.006 [main] INFO  c.e.orders.OrderService [orderId=ORD-002 user=bob] - Initiating shipment
2026-02-28 10:30:00.006 [main] INFO  c.e.orders.OrderService [orderId=ORD-002 user=bob] - Order shipped to user bob
2026-02-28 10:30:00.007 [main] INFO  c.e.orders.OrderService [orderId=ORD-002 user=bob] - Order processing completed successfully in 2ms, finalTotal=$2816.856

=== Scenario 3: Failed Payment ===
2026-02-28 10:30:00.008 [main] INFO  c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Order processing started: 3 items, total=$7500.0
2026-02-28 10:30:00.008 [main] DEBUG c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Validating order
2026-02-28 10:30:00.008 [main] DEBUG c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Order validated: 3 items passed all checks
2026-02-28 10:30:00.008 [main] DEBUG c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Checking discounts for total=$7500.0
2026-02-28 10:30:00.008 [main] INFO  c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Discount applied: 10% off $7500.0 = -$750.0, newTotal=$6750.0
2026-02-28 10:30:00.009 [main] DEBUG c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Tax calculated: $6750.0 * 0.08 = $540.0, newTotal=$7290.0
2026-02-28 10:30:00.009 [main] INFO  c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Processing payment of $7290.0
2026-02-28 10:30:00.009 [main] WARN  c.e.orders.OrderService [orderId=ORD-003 user=charlie] - High-value order detected: $7290.0 exceeds threshold $1000.0
2026-02-28 10:30:00.009 [main] ERROR c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Payment gateway rejected transaction for $7290.0
java.lang.RuntimeException: Payment declined: exceeds single transaction limit
    at com.example.orders.OrderService.simulatePaymentGateway(OrderService.java:112)
    ...
2026-02-28 10:30:00.010 [main] ERROR c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Order processing failed after 2ms

=== Scenario 4: Invalid Order ===
2026-02-28 10:30:00.011 [main] INFO  c.e.orders.OrderService [orderId=ORD-004 user=dave] - Order processing started: 0 items, total=$0.0
2026-02-28 10:30:00.011 [main] DEBUG c.e.orders.OrderService [orderId=ORD-004 user=dave] - Validating order
2026-02-28 10:30:00.011 [main] ERROR c.e.orders.OrderService [orderId=ORD-004 user=dave] - Order has no items
2026-02-28 10:30:00.011 [main] ERROR c.e.orders.OrderService [orderId=ORD-004 user=dave] - Order processing failed after 0ms
java.lang.IllegalArgumentException: Order must have at least one item
    at com.example.orders.OrderService.validateOrder(OrderService.java:70)
    ...

13.7 What This Example Demonstrates

# Concept Where in Code
1 Logger declaration (private static final) OrderService class field
2 MDC for request tracking processOrder() -- MDC.put/MDC.clear
3 MDC cleanup in finally block processOrder() -- prevents thread contamination
4 INFO for business events "Order processing started", "Payment processed", "Order shipped"
5 DEBUG for technical details "Validating order", "Tax calculated", "Payment gateway returned"
6 WARN for recoverable issues "High-value order detected"
7 ERROR with exception "Payment gateway rejected" -- exception passed as last argument
8 Parameterized logging ({}) Every log statement uses {} instead of string concatenation
9 Context in messages Order ID, user ID, amounts, item counts included
10 Performance tracking Elapsed time measured and logged on completion/failure
11 No sensitive data logged toString() excludes user details; no passwords/tokens
12 Separate logback.xml configuration Console + rolling file, package-level filtering, MDC in pattern

14. Quick Reference

Topic Key Point
Recommended stack SLF4J (facade) + Logback (implementation)
Logger declaration private static final Logger log = LoggerFactory.getLogger(MyClass.class)
Parameterized logging log.info("User {} placed order {}", userId, orderId)
Exception logging log.error("Something failed for order {}", orderId, exception) -- exception is always the last argument
Log levels TRACE < DEBUG < INFO < WARN < ERROR. Use INFO for business events, DEBUG for technical details.
MDC MDC.put("requestId", id) in filter/interceptor, %X{requestId} in pattern, MDC.clear() in finally
Configuration file logback.xml in src/main/resources
Production format JSON via logstash-logback-encoder for ELK/Splunk/Datadog
Log rotation SizeAndTimeBasedRollingPolicy with maxFileSize, maxHistory, totalSizeCap
Async logging Logback AsyncAppender or Log4j2 AsyncLogger for high throughput
Never log Passwords, credit cards, SSNs, API keys, session tokens
Never use Log4j 1.x (CVE-2021-44228), System.out.println, string concatenation in log calls



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *