Spring Boot Logging Complete Guide: SLF4J, Logback, MDC, and Real-World Examples

Spring Boot Logging Complete Guide: From Basics to Production-Level Logging

Imagine you are working on a payment gateway that processes thousands of transactions every minute.

A customer reports that ₹5,000 was deducted from their wallet, but the merchant never received the payment confirmation.

You open the application, check the database, inspect the payment service, and search through multiple microservice logs. But there is one problem: you cannot easily identify which logs belong to that particular transaction.

Now think about another situation. Your application is running perfectly in development, but after deployment, a payment API starts returning HTTP 500. You have no idea which method failed, what the request was doing, or where the exception originated.

This is where proper application logging becomes extremely important.

Logging is not simply about printing messages in the console. It is one of the most important observability practices in production-grade Java applications.

In this tutorial, we will build a practical understanding of Spring Boot logging using a real-world payment processing example. We will start with the fundamentals and gradually move towards the techniques used by experienced backend developers.

1. What Is Logging in Java?

Logging is the process of recording application events, execution details, warnings, and errors while an application is running.

It helps developers understand what happened inside an application without attaching a debugger to a production server.

Consider a simple payment operation:

public void processPayment() {
    System.out.println("Payment processing started");

    // Payment business logic

    System.out.println("Payment processing completed");
}

This approach works for a small demonstration. But it is not suitable for production applications.

Why?

  • You cannot easily control different log levels.
  • You cannot configure proper log formatting.
  • You cannot easily manage log files and rotation.
  • You cannot integrate a consistent logging framework.
  • You have limited control over filtering and monitoring.

Professional Java applications use logging frameworks to solve these problems.

2. Understanding SLF4J and Logback

One common interview question is: What is the difference between SLF4J and Logback?

Let's understand them separately.

SLF4J

SLF4J stands for Simple Logging Facade for Java.

It provides a common logging API that allows your application code to communicate with a logging implementation without being tightly coupled to it.

For example:

private static final Logger log =
        LoggerFactory.getLogger(PaymentService.class);

log.info("Payment processing started");

Your application uses the SLF4J API, while the underlying logging provider handles the actual logging operations.

Logback

Logback is a logging implementation that works with SLF4J.

It is the default logging implementation in a standard Spring Boot starter configuration.

Logback provides:

  • Console logging.
  • File-based logging.
  • Rolling log files.
  • Log level configuration.
  • Custom log patterns.
  • Logger configuration.

How do they work together?

Spring Boot Application SLF4J API Logback Implementation Console Files Monitoring
Figure: SLF4J is the logging abstraction, whereas Logback is one possible logging implementation.

Important: SLF4J is the logging abstraction, whereas Logback is one possible logging implementation.

3. Add Logging Dependencies in Spring Boot

If you are using Spring Boot with the standard starter, logging support is already included through spring-boot-starter-logging, which is brought in by spring-boot-starter-web.

Maven dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Gradle dependency

implementation 'org.springframework.boot:spring-boot-starter-web'

You generally do not need to add SLF4J or Logback separately when using the standard Spring Boot starter.

To verify your logging dependencies in Maven:

mvn dependency:tree

For Gradle:

./gradlew dependencies --configuration runtimeClasspath

4. Understanding Different Logging Levels

Logging levels help us decide which messages should be recorded and how important they are.

Spring Boot commonly uses these logging levels:

Level Purpose Real-world example
TRACE Very detailed execution information. Tracing internal calculation steps.
DEBUG Detailed diagnostic information. Checking payment validation decisions.
INFO Important application events. Payment successfully processed.
WARN Unexpected situations that do not necessarily stop execution. External vendor response is slow.
ERROR Failures requiring investigation. Payment processing failed.
OFF Disables logging for a logger. Disabling a specific logger.

Example: Choosing the right logging level

log.trace("Entering payment calculation");

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

log.info("Payment request received");

log.warn("Payment vendor response is delayed");

log.error("Payment processing failed", exception);

Senior developer tip: Do not log everything as INFO or ERROR. If every normal event is an ERROR, your production monitoring becomes noisy and important failures become harder to identify.

5. Create a Real-World Spring Boot Payment Logging Project

Let's build a simple payment processing flow.

Our application will contain:

  • PaymentController: Receives the payment request.
  • PaymentService: Executes the business logic.
  • PaymentRepository: Represents the persistence layer.
  • SLF4J Logger: Records application events.

Our expected flow is:

Client Request Payment Controller Payment Service Payment Repository Database
Figure: A typical payment request flow through controller, service, repository, and database.

Step 1: Create PaymentRequest DTO

public class PaymentRequest {

    private String transactionId;
    private BigDecimal amount;
    private String currency;

    // Getters and setters
}

Step 2: Create PaymentController

@RestController
@RequestMapping("/api/payments")
public class PaymentController {

    private static final Logger log =
            LoggerFactory.getLogger(PaymentController.class);

    private final PaymentService paymentService;

    public PaymentController(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    @PostMapping
    public ResponseEntity<String> processPayment(
            @RequestBody PaymentRequest request) {

        log.info("Payment API request received. TransactionId={}",
                request.getTransactionId());

        paymentService.processPayment(request);

        log.info("Payment API completed. TransactionId={}",
                request.getTransactionId());

        return ResponseEntity.ok("Payment processed successfully");
    }
}

Notice that we are using parameterized logging with placeholders instead of string concatenation.

Prefer this:

log.info("Payment amount: {}", amount);

Instead of:

log.info("Payment amount: " + amount);

Parameterized logging avoids unnecessary string construction when the corresponding log level is disabled.

Step 3: Create PaymentService

@Service
public class PaymentService {

    private static final Logger log =
            LoggerFactory.getLogger(PaymentService.class);

    public void processPayment(PaymentRequest request) {

        log.info("Starting payment processing. TransactionId={}",
                request.getTransactionId());

        try {

            validatePayment(request);

            // Call payment vendor here.

            log.info("Payment successfully processed. TransactionId={}",
                    request.getTransactionId());

        } catch (IllegalArgumentException ex) {

            log.warn("Payment validation failed. TransactionId={}",
                    request.getTransactionId());

            throw ex;

        } catch (Exception ex) {

            log.error("Unexpected payment processing failure. TransactionId={}",
                    request.getTransactionId(), ex);

            throw ex;
        }
    }

    private void validatePayment(PaymentRequest request) {

        if (request.getAmount() == null ||
                request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {

            throw new IllegalArgumentException(
                    "Payment amount must be greater than zero");
        }
    }
}

In a real application, you should also use a global exception handler and define clear exception types for validation, vendor failures, and unexpected system errors.

Step 4: What does the output look like?

2026-11-08 10:30:10 INFO  PaymentController
Payment API request received. TransactionId=TXN10001

2026-11-08 10:30:10 INFO  PaymentService
Starting payment processing. TransactionId=TXN10001

2026-11-08 10:30:11 INFO  PaymentService
Payment successfully processed. TransactionId=TXN10001

2026-11-08 10:30:11 INFO  PaymentController
Payment API completed. TransactionId=TXN10001

Now we can understand the execution flow by reading the logs.

6. Configure Logging Using application.properties

Spring Boot allows us to configure logging without writing Java code.

Open your application.properties file.

# Root logging level
logging.level.root=INFO

# Application package logging
logging.level.com.example.payment=DEBUG

# Spring framework logging
logging.level.org.springframework=WARN

# Hibernate logging
logging.level.org.hibernate=ERROR

# Console log pattern
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n

# Log file
logging.file.name=logs/payment-service.log

Let's understand the important properties.

Property Purpose
logging.level.root Sets the default logging level.
logging.level.package Sets logging level for a particular package.
logging.file.name Writes logs to a specified file.
logging.pattern.console Customizes console output.
logging.pattern.file Customizes file output.

7. Different Logging Levels for Different Environments

A common mistake is using DEBUG logging everywhere, including production.

Development and production environments have different requirements.

application-dev.properties

logging.level.root=DEBUG
logging.level.com.example.payment=DEBUG

application-prod.properties

logging.level.root=INFO
logging.level.com.example.payment=INFO
logging.level.org.springframework=WARN

Activate the production profile using:

spring.profiles.active=prod

Or set the environment variable:

SPRING_PROFILES_ACTIVE=prod

This keeps unnecessary diagnostic logs out of the production environment.

8. Configure Logback Using logback-spring.xml

When an application becomes larger, application.properties may not provide enough control over logging configuration.

For more advanced requirements, we can use logback-spring.xml.

Create this file inside:

src/main/resources/logback-spring.xml

Example production-oriented configuration:

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

    <property name="LOG_DIR" value="logs"/>

    <appender name="CONSOLE"
              class="ch.qos.logback.core.ConsoleAppender">

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

    </appender>

    <appender name="FILE"
              class="ch.qos.logback.core.rolling.RollingFileAppender">

        <file>${LOG_DIR}/payment-service.log</file>

        <rollingPolicy
            class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">

            <fileNamePattern>
                ${LOG_DIR}/archive/payment-%d{yyyy-MM-dd}.%i.log.gz
            </fileNamePattern>

            <maxFileSize>20MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>1GB</totalSizeCap>

        </rollingPolicy>

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

    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>

</configuration>

This configuration provides:

  • Console logging.
  • File-based logging.
  • Daily log file rollover.
  • File rollover when the configured size is reached.
  • Compressed archived logs.
  • Maximum history and total archive size limits.

Production note: Log rotation does not automatically make logs durable or searchable. In containerized environments, consider writing structured logs to stdout and letting the infrastructure collect, retain, and forward them to a centralized logging platform.

9. MDC: The Most Important Logging Concept for Microservices

Now let's discuss something that separates basic logging from production-level logging.

Imagine your payment gateway contains these microservices:

  • API Gateway
  • Wallet Service
  • Payment Service
  • Notification Service
  • Vendor Integration Service

A single transaction may travel through all five services.

Without a common identifier, identifying all log entries belonging to the same request can be difficult.

This is where MDC comes into the picture.

What is MDC?

MDC stands for Mapped Diagnostic Context.

It allows us to store contextual information, such as transaction IDs, request IDs, and user identifiers, so that the logging framework can automatically include them in log messages.

Step 1: Create a correlation ID filter

@Component
public class CorrelationIdFilter extends OncePerRequestFilter {

    private static final String HEADER_NAME = "X-Correlation-ID";

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain)
            throws ServletException, IOException {

        String correlationId = request.getHeader(HEADER_NAME);

        if (correlationId == null || correlationId.isBlank()) {
            correlationId = UUID.randomUUID().toString();
        }

        try {

            MDC.put("correlationId", correlationId);

            response.setHeader(HEADER_NAME, correlationId);

            filterChain.doFilter(request, response);

        } finally {

            MDC.remove("correlationId");
        }
    }
}

We generate a correlation ID if the incoming request does not contain one.

The finally block is important because it prevents stale MDC data from leaking into another request when threads are reused.

Step 2: Update the logging pattern

logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [correlationId=%X{correlationId}] %logger{36} - %msg%n

Step 3: Write normal logs

log.info("Payment request received");

log.info("Wallet balance validated");

log.info("Vendor payment response received");

Now the output can look like this:

INFO [correlationId=TXN-10001]
Payment request received

INFO [correlationId=TXN-10001]
Wallet balance validated

INFO [correlationId=TXN-10001]
Vendor payment response received

All these logs can be searched using the same correlation ID.

Important senior-level consideration: MDC is normally thread-local. When work moves to another thread through @Async, executors, or asynchronous processing, the context does not automatically follow in every setup. You must explicitly propagate it using an appropriate TaskDecorator, context-propagation mechanism, or tracing library.

For Kafka and other messaging systems, propagate the correlation ID through message headers and establish MDC context in the consumer before processing.

10. How to Log Exceptions Correctly

One common mistake made by Java developers is logging only the exception message.

Consider:

try {

    processPayment();

} catch (Exception ex) {

    log.error(ex.getMessage());
}

This is not sufficient for debugging production failures because it does not include the complete stack trace.

Prefer:

try {

    processPayment();

} catch (Exception ex) {

    log.error("Payment processing failed", ex);

}

Passing the exception object allows the logging framework to record the stack trace.

Also avoid logging the same exception at every layer. Usually, handle it at the appropriate boundary, preserving the original cause when rethrowing or wrapping exceptions.

11. Logging Sensitive Information: A Critical Security Practice

Payment applications process sensitive information. Logging complete request and response objects without checking their contents can create serious security problems.

Never log:

  • Passwords.
  • OTP values.
  • PINs.
  • Authentication tokens.
  • Full card numbers or CVV.
  • Bank account credentials.
  • Encryption keys.
  • Unmasked personal information.

Bad logging practice

log.info("Payment request: {}", request);

This might expose sensitive information if the DTO contains credentials or personal data.

Better logging practice

log.info(
    "Payment request received. transactionId={}, amount={}, currency={}",
    request.getTransactionId(),
    request.getAmount(),
    request.getCurrency()
);

Log only the fields required for troubleshooting, subject to your organization's security and data-retention policies.

Example: Masking a phone number

public static String maskPhoneNumber(String phone) {

    if (phone == null || phone.length() < 4) {
        return "****";
    }

    return "******" + phone.substring(phone.length() - 4);
}

Output:

Original: 4234567890
Masked:   ******7890

Masking should be applied before information enters the logging pipeline. It is not a substitute for avoiding sensitive data in logs in the first place.

12. JSON Logging for Microservices

In modern distributed systems, plain text logs can become difficult to search and analyze.

Structured logging solves this problem by recording events as JSON fields.

For example:

{
  "timestamp": "2026-11-08T10:30:10.120Z",
  "level": "INFO",
  "service": "payment-service",
  "correlationId": "TXN-10001",
  "transactionId": "TXN-10001",
  "message": "Payment processed successfully"
}

These fields can be searched independently in centralized log platforms.

Common tools used for log collection and analysis include:

  • Elasticsearch
  • Logstash
  • Kibana
  • Grafana Loki
  • Fluent Bit
  • Splunk

For Spring Boot applications, JSON logging can be configured using an appropriate Logback JSON encoder or a structured logging solution compatible with your Spring Boot version.

Do not assume that adding a JSON logging dependency automatically produces safe or useful logs. Define the fields, masking rules, retention policy, and ingestion pipeline deliberately.

13. Dynamic Logging Level Using Spring Boot Actuator

Sometimes production debugging requires temporarily enabling DEBUG logs for one package without restarting the application.

Spring Boot Actuator provides a loggers endpoint for this purpose.

Add Actuator dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Configure application.properties

management.endpoints.web.exposure.include=health,info,loggers

To inspect a logger:

GET /actuator/loggers/com.example.payment

To change its logging level:

POST /actuator/loggers/com.example.payment
Content-Type: application/json

{
  "configuredLevel": "DEBUG"
}

To restore the inherited logging level:

POST /actuator/loggers/com.example.payment
Content-Type: application/json

{
  "configuredLevel": null
}

Security warning: Never expose management endpoints publicly without proper authentication and authorization. Restrict access to trusted operators and internal networks.

14. Common Logging Mistakes in Spring Boot

Mistake Better approach
Using System.out.println() Use SLF4J.
Logging every message at INFO Choose meaningful log levels.
Logging complete request objects Log only approved fields.
Ignoring stack traces Pass the exception object.
Using DEBUG in production everywhere Enable targeted diagnostic logging.
No correlation ID Use request IDs and distributed tracing.
No log rotation Configure retention and archival.
Logging the same exception repeatedly Log at the appropriate handling boundary.

15. Logging vs Monitoring vs Distributed Tracing

These three concepts are related, but they solve different problems.

Concept Purpose Example
Logging Records individual application events. Payment failed due to vendor timeout.
Monitoring Tracks system health and metrics. Payment API error rate is 5%.
Distributed tracing Tracks requests across multiple services. Gateway → Wallet → Payment → Vendor.

For a mature microservices architecture, these capabilities should work together.

OpenTelemetry is a widely used option for collecting traces, metrics, and related telemetry across distributed systems.

16. Important Spring Boot Logging Interview Questions and Answers

Q1. What is the difference between SLF4J and Logback?

SLF4J is a logging abstraction that provides a common API. Logback is a logging implementation that processes and writes the log messages.

Q2. What is the default logging framework in Spring Boot?

Logback is the default logging implementation when using Spring Boot's standard logging starter configuration.

Q3. Why should we use SLF4J instead of System.out.println()?

SLF4J supports configurable log levels, structured logging integrations, multiple logging providers, and production-friendly logging practices.

Q4. What is MDC in Java logging?

Mapped Diagnostic Context stores contextual information such as correlation IDs in the logging context. Logback can include these values in log output automatically.

Q5. What is the difference between DEBUG and INFO?

DEBUG is intended for diagnostic details, while INFO records significant normal application events.

Q6. What is the difference between ERROR and WARN?

ERROR indicates a failure that requires attention. WARN indicates an unexpected condition that may require investigation but does not necessarily mean the operation has failed.

Q7. How do you configure logging in Spring Boot?

Logging can be configured using application.properties, application.yml, or a Logback configuration file such as logback-spring.xml.

Q8. What is the purpose of logback-spring.xml?

It provides advanced Logback configuration, including appenders, patterns, rolling policies, filters, and profile-specific logging configuration.

Q9. What is a RollingFileAppender?

It writes logs to files and rolls over to new files based on configured time or size policies, helping control disk usage.

Q10. Why should we avoid logging passwords and OTPs?

Logs may be accessed by multiple systems and retained for long periods. Sensitive information in logs can cause security and compliance issues.

Q11. How do you trace a request across multiple microservices?

Use correlation IDs or distributed tracing context. Propagate the identifiers through HTTP headers and messaging headers, and include them in application logs.

Q12. Does MDC automatically propagate to another thread?

No. Standard MDC implementations are generally thread-local. For asynchronous processing, context propagation must be configured explicitly.

Q13. How can you change logging levels without restarting Spring Boot?

Spring Boot Actuator provides a loggers endpoint that can change the configured logging level dynamically, provided the endpoint is enabled and secured.

Q14. What is the difference between logging and distributed tracing?

Logging records individual events, whereas distributed tracing follows a request across services and records the timing and relationships between operations.

Q15. How would you design logging for a high-volume payment gateway?

I would use asynchronous or appropriately managed logging where suitable, structured JSON output, correlation IDs, controlled log levels, sensitive-data masking, centralized log aggregation, retention policies, and monitoring. I would also avoid excessive logging in high-throughput paths and measure the performance impact.

Q16. What happens if logging itself fails?

Logging failures should not normally bring down the payment business operation. I would configure appropriate appenders, monitor logging infrastructure, manage disk usage, and ensure the application has a reliable telemetry delivery strategy.

Q17. Why should we avoid duplicate exception logging?

Logging an exception at every layer creates noisy logs, duplicates stack traces, and makes production troubleshooting difficult. Log it at the appropriate handling boundary and preserve its cause.

Q18. Can we use Log4j2 instead of Logback in Spring Boot?

Yes. Spring Boot supports alternative logging implementations. We can exclude the default logging starter and configure the appropriate Log4j2 starter, ensuring that only the intended logging provider is active.

Q19. How do you investigate a slow payment API using logs?

I would search using the correlation ID, inspect the timestamps of each operation, identify the slow dependency, compare application and vendor response times, and correlate the logs with metrics and distributed traces.

Q20. Should we log every database query in production?

No. Logging every query can generate enormous volumes of data, expose sensitive values, and affect performance. I would enable targeted database diagnostics when needed and use appropriate database monitoring tools.

17. Senior Java Developer Production Logging Checklist

Before deploying a Spring Boot microservice, I would review the following points:

  • Use SLF4J with a properly configured logging implementation.
  • Configure appropriate log levels for each environment.
  • Use correlation IDs for request tracking.
  • Propagate diagnostic context across asynchronous operations.
  • Mask sensitive information before logging.
  • Include complete exception details when appropriate.
  • Configure log rotation and retention.
  • Prefer structured logs for centralized analysis.
  • Protect Actuator management endpoints.
  • Monitor logging volume, disk usage, and ingestion failures.
  • Integrate logging with monitoring and distributed tracing.
  • Avoid unnecessary logging inside high-throughput loops.

18. Final Thoughts

Logging is one of those engineering practices that developers often underestimate until something goes wrong in production.

Writing a few log statements is easy. Designing a logging strategy that helps a team investigate a failed payment across multiple microservices is a completely different challenge.

As a Java backend developer, you should understand not only how to write logs but also how to make them useful, searchable, secure, and reliable.

My recommendation is to start with SLF4J and Logback, then learn MDC, correlation IDs, structured logging, and distributed tracing. These concepts will make a significant difference when you start working on enterprise-level Spring Boot applications.

Remember: Good logging does not mean generating more logs. It means generating the right logs at the right time with the right context.

Test Your Spring Boot Logging Knowledge

Let's finish with one practical interview question.

Question: Your Spring Boot application processes 10,000 requests per minute. You are using MDC to store correlation IDs. What is the biggest concern when the application processes requests using a reusable thread pool?

  1. MDC automatically generates duplicate IDs.
  2. MDC values may leak between requests if they are not cleared.
  3. SLF4J cannot work with multiple threads.
  4. Logback disables all INFO messages.

Correct Answer: B

Explanation: MDC is generally thread-local. Since thread pools reuse threads, failing to clear MDC values in a finally block can cause diagnostic context from one request to appear in another request's logs.

No comments: