Spring-Boot
log4j2
logging
Java
application-logging

Spring-Boot logging with log4j2?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Spring Boot defaults to Logback, but it routes application logging through SLF4J, so swapping in Log4j2 is a supported and common setup. Teams usually make the switch for asynchronous logging, richer appender options, or better control over structured output.

The key is to change both the dependencies and the configuration file. If you only add Log4j2 without removing the default starter, you often end up with multiple logging implementations on the classpath.

Replace Boot's Default Logging Starter

For Maven, exclude spring-boot-starter-logging from the starter you already use and add spring-boot-starter-log4j2:

xml
1<dependencies>
2  <dependency>
3    <groupId>org.springframework.boot</groupId>
4    <artifactId>spring-boot-starter-web</artifactId>
5    <exclusions>
6      <exclusion>
7        <groupId>org.springframework.boot</groupId>
8        <artifactId>spring-boot-starter-logging</artifactId>
9      </exclusion>
10    </exclusions>
11  </dependency>
12
13  <dependency>
14    <groupId>org.springframework.boot</groupId>
15    <artifactId>spring-boot-starter-log4j2</artifactId>
16  </dependency>
17</dependencies>

The Gradle equivalent is the same idea:

gradle
1implementation("org.springframework.boot:spring-boot-starter-web") {
2    exclude group: "org.springframework.boot", module: "spring-boot-starter-logging"
3}
4
5implementation("org.springframework.boot:spring-boot-starter-log4j2")

After changing dependencies, run a clean build and inspect the dependency tree if logging behaves strangely.

Use log4j2-spring.xml, Not Just log4j2.xml

If your application is a Spring Boot app, put the configuration in src/main/resources/log4j2-spring.xml. The -spring variant lets Boot participate in initialization and enables useful features such as Spring-profile sections.

Here is a practical starting point:

xml
1<?xml version="1.0" encoding="UTF-8"?>
2<Configuration status="WARN">
3    <Appenders>
4        <Console name="Console" target="SYSTEM_OUT">
5            <PatternLayout
6                pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t] %c{1} - %msg%n"/>
7        </Console>
8
9        <RollingFile name="File"
10                     fileName="logs/app.log"
11                     filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz">
12            <PatternLayout pattern="%d %-5p %c - %m%n"/>
13            <Policies>
14                <TimeBasedTriggeringPolicy/>
15                <SizeBasedTriggeringPolicy size="20 MB"/>
16            </Policies>
17        </RollingFile>
18    </Appenders>
19
20    <Loggers>
21        <Logger name="com.example" level="debug" additivity="false">
22            <AppenderRef ref="Console"/>
23            <AppenderRef ref="File"/>
24        </Logger>
25
26        <Root level="info">
27            <AppenderRef ref="Console"/>
28            <AppenderRef ref="File"/>
29        </Root>
30    </Loggers>
31</Configuration>

This gives you readable console output in development and a rolling file for longer-lived environments.

Keep Application Code on the Logging Facade

Even after switching implementations, your application code normally stays on the SLF4J API. That keeps your code portable and avoids hard-coding a specific backend everywhere.

java
1package com.example.demo;
2
3import org.slf4j.Logger;
4import org.slf4j.LoggerFactory;
5import org.springframework.stereotype.Service;
6
7@Service
8public class PaymentService {
9    private static final Logger log = LoggerFactory.getLogger(PaymentService.class);
10
11    public void charge(String orderId) {
12        log.info("Charging order {}", orderId);
13        log.debug("Detailed payment diagnostics for {}", orderId);
14    }
15}

That code works because Boot wires the SLF4J calls to the Log4j2 backend you configured.

Profile-Specific Logging

One reason to prefer log4j2-spring.xml is Spring-aware sections. Development often wants console-heavy debug output, while production wants cleaner info-level logs and maybe file or JSON appenders.

xml
1<SpringProfile name="dev">
2    <Loggers>
3        <Root level="debug">
4            <AppenderRef ref="Console"/>
5        </Root>
6    </Loggers>
7</SpringProfile>
8
9<SpringProfile name="prod">
10    <Loggers>
11        <Root level="info">
12            <AppenderRef ref="File"/>
13        </Root>
14    </Loggers>
15</SpringProfile>

This keeps environment-specific behavior in one place instead of scattering logging changes across multiple files.

When Async Logging Helps

Log4j2 supports asynchronous appenders and asynchronous loggers. That can reduce request-thread overhead in high-throughput services, but it is not a free performance switch. Async logging changes queueing behavior and failure characteristics, so you should measure it under load rather than enabling it blindly.

A common JVM property for async loggers is:

bash
-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

Use it when log volume is large enough to matter and when the team understands the operational tradeoffs.

Common Pitfalls

The most common mistake is leaving Logback on the classpath. If both backends remain present, startup warnings and confusing behavior follow quickly.

Another mistake is putting Spring-specific sections into log4j2.xml and expecting them to work. Use log4j2-spring.xml when you need Spring profile support.

Teams also overuse root-level DEBUG in production. That generates noise, increases I/O, and often hides the messages that actually matter during incidents.

Finally, do not switch to Log4j2 and then call the backend directly from application code unless you truly need backend-specific features. Most applications should keep logging through SLF4J.

Summary

  • Replace Boot's default logging starter with spring-boot-starter-log4j2.
  • Put the configuration in log4j2-spring.xml so Spring-specific features work.
  • Keep application code on the SLF4J API.
  • Use rolling appenders and targeted package loggers instead of global debug logging.
  • Treat async logging as a measured optimization, not a default checkbox.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.