Spring Boot Debugging with IntelliJ
Debugging Spring Boot apps becomes dramatically easier with IntelliJ IDEA when you combine breakpoints, the Spring plugin views, the Database tool window, and a handful of runtime checks. Let us delve into understanding how to use IntelliJ to debug Spring Boot applications.
1. Introduction
As a Spring Boot developer, I often find myself staring at a console log wondering what went wrong. One late evening, while debugging an application that was failing mysteriously in production, I decided to rely on IntelliJ IDEA’s powerful debugger. In this article, I’ll walk you through how I approached the problem — from starting the debugger, to checking beans and properties, validating database connections, diagnosing transaction issues, and even inspecting JPA entity states.
2. Getting Started with the Debugging
Start your Spring Boot app in debug mode in IntelliJ: open your Application run configuration and click the Debug icon (or use Shift+F9). IntelliJ will attach a debugger and stop on breakpoints. Consider the following code snippet:
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(String name) {
String result = doWork(name);
return "Hello " + result;
}
private String doWork(String name) {
if (name == null || name.isBlank()) {
return "World";
}
return name.toUpperCase();
}
}
How to debug: set a breakpoint on the first line of hello(), run the app in debug mode, then call http://localhost:8080/hello?name=yb. Use the Variables pane to inspect variables, and Step Into (F7) to follow execution into doWork. Tip: Use conditional breakpoints (right-click a breakpoint > More) to stop only when certain conditions are met, e.g. name != null && name.equals("yb").
3. Viewing Loaded Beans at Runtime
When debugging a Spring Boot application, one of the first questions I ask myself is: Is my bean even getting registered? It’s surprisingly common to spend hours looking at code only to realize that the component never made it into the application context. IntelliJ IDEA makes this investigation easier with its built-in Spring support, and Spring Boot Actuator provides a runtime way to list all beans. Let’s go through both approaches.
3.1 Using IntelliJ’s Spring Tool Window
IntelliJ’s Spring tool window (View > Tool Windows > Spring) automatically shows all beans discovered in your project. From here you can:
- See a hierarchical tree of beans, their scopes, and types.
- Double-click a bean to jump directly to its source implementation.
- Quickly confirm if multiple candidates for the same interface are registered.
This view is invaluable when you suspect Spring is wiring the wrong implementation into your service.
3.2 Activating Spring Boot Actuator
For a runtime view of beans (outside IntelliJ), I enabled Spring Boot Actuator. This allows me to query the /actuator/beans endpoint and inspect beans as they are actually running.
# application.properties management.endpoints.web.exposure.include=beans,health,info server.port=8080
Once enabled, I could call the endpoint:
curl -s http://localhost:8080/actuator/beans | jq '.contexts."application".beans | keys | length' # or omit jq to see full JSON
The response looked like this:
{
"contexts": {
"application": {
"beans": {
"helloController": { ... },
"dataSource": { ... },
"entityManagerFactory": { ... }
}
}
}
}
3.3 Why This Matters?
Actuator doesn’t just dump bean names — it also returns scope, type, and dependency relationships. If your application fails to start, you can compare the expected beans with the actual ones loaded. For example:
- Verify that
@Serviceor@Repositoryclasses are actually being picked up by component scanning. - Check if multiple beans are competing for the same injection point.
- Confirm the correct
DataSourceorEntityManagerFactoryis being initialized.
Between IntelliJ’s Spring view for design-time analysis and Actuator’s beans endpoint for runtime inspection, I rarely have to wonder again whether my beans are being created and wired as expected.
4. Inspect the Actual Value of a Property
Debugging configuration issues in Spring Boot can be tricky because properties can come from multiple sources: application.properties, application.yml, environment variables, system properties, or even a remote Config Server. I’ve often run into cases where I was sure a property had one value — only to discover at runtime it was overridden by another source. To avoid such confusion, I rely on Spring Boot Actuator to check the effective value.
4.1 Leveraging the Actuator Environment
By exposing the env endpoint in Actuator, I can query all property sources in the exact order Spring Boot evaluates them. This helps confirm not only the value of a property but also where it came from.
# application.properties management.endpoints.web.exposure.include=env
Once enabled, I ran the following command to inspect the resolved datasource URL:
curl -s http://localhost:8080/actuator/env \
| jq '.propertySources[]
| select(.name|contains("application.properties"))
| .properties["spring.datasource.url"]'
4.2 Output
{
"value": "jdbc:postgresql://localhost:5432/mydb",
"origin": "class path resource [application.properties]:5:34"
}
The response not only revealed the actual value (jdbc:postgresql://localhost:5432/mydb) but also its origin in application.properties. If the property had been overridden by an environment variable or command-line argument, Actuator would have shown that source instead.
4.3 Debugging within IntelliJ
Of course, IntelliJ IDEA also lets me inspect values directly in the debugger. For example, if I inject a property:
@Value("${spring.datasource.url}")
private String dbUrl;
I can pause at a breakpoint and instantly see what dbUrl resolved to. This is especially useful when I want to confirm the effective property value without hitting the Actuator endpoint. Between Actuator’s /env endpoint and IntelliJ’s debugger, I now have complete visibility into configuration values — no more guessing which file or environment variable actually won.
5. Quickly Verify Database Connections
When debugging a Spring Boot application, one of my first instincts is to confirm: Can the app even talk to the database? I’ve lost hours before chasing Hibernate errors that turned out to be simple connection problems. Thankfully, IntelliJ IDEA and a quick in-app check make this verification painless.
5.1 IntelliJ Database Tool Window
IntelliJ’s Database tool window lets me connect directly using the same JDBC URL configured in application.properties. I simply paste the connection string, test it, and instantly know if the database is reachable. From there, I can also browse tables, run test queries like:
SELECT 1;
This often tells me whether the issue lies with connectivity or the application layer.
5.2 Quick In-App Check
Sometimes I prefer to confirm connectivity at runtime, inside the application itself. For this, I wrote a simple CommandLineRunner that attempts to fetch a connection:
@Component
public class DbChecker implements CommandLineRunner {
private final DataSource ds;
public DbChecker(DataSource ds) {
this.ds = ds;
}
@Override
public void run(String... args) throws Exception {
try (Connection c = ds.getConnection()) {
System.out.println("DB OK - metadata: "
+ c.getMetaData().getDatabaseProductName());
} catch (Exception e) {
System.err.println("DB FAIL: " + e.getMessage());
}
}
}
5.3 Output
DB OK - metadata: PostgreSQL
On startup, the runner immediately confirmed that the app could obtain a connection and even printed out the database product name. If it had failed, I would have seen a clear error message instead.
5.4 Debugging Inside IntelliJ
To go one step deeper, I sometimes set a breakpoint inside the try block. From there, IntelliJ’s debugger lets me inspect the DataSource configuration fields — URL, username, pool settings — and verify they match what I expect. This has saved me from situations where a staging app was accidentally pointing at the wrong database instance.
Whether using IntelliJ’s Database tool window or an in-app checker, verifying the database connection early in the debugging process saves a lot of wasted time chasing misleading stack traces.
6. Troubleshooting Transaction Problems
Transaction problems can be some of the trickiest bugs in Spring Boot applications. I’ve often hit situations where my @Transactional annotation was proudly sitting on a method, yet the database writes were never committed. Two of the most common pitfalls are:
- Methods annotated with
@Transactionalnot actually creating a transaction because of proxying limitations. - Calling methods from within the same class (self-invocation), which bypasses the transactional proxy entirely.
6.1 Problematic Example
@Service
public class OrderService {
@Transactional
public void placeOrder(Order o) {
persistOrder(o);
// self-invocation — transaction NOT applied
// if persistOrder is in the same class and not called through proxy
}
@Transactional
public void persistOrder(Order o) {
// write to DB
}
}
At first glance, this looks fine — both methods are annotated with @Transactional. But when placeOrder() calls persistOrder() internally, the call doesn’t pass through the Spring proxy, so no new transaction is applied.
6.2 Debugging the Issue
To confirm what’s happening at runtime, I put breakpoints in both methods and evaluated the following expression in IntelliJ’s debugger:
org.springframework.transaction.support.TransactionSynchronizationManager.isActualTransactionActive()
6.3 Output
false
The result came back false, meaning no transaction was active despite the annotation. This confirmed my suspicion that self-invocation was the culprit.
6.4 Fixing the Problem
Once identified, the fix depends on the scenario:
- Move the method — Place the public transactional method into another bean, so calls go through the Spring proxy.
- Refactor the design — Call the method from outside the class instead of self-invoking.
- Use AspectJ weaving — If you want to keep methods together, enable AspectJ weaving, which applies transactions without relying on proxy-based calls.
By stepping through the debugger and evaluating the transaction state, I could quickly pinpoint why the database writes weren’t sticking. Without this check, it would have been easy to misdiagnose the problem as a persistence or configuration issue.
7. Examine JPA Entity States
Understanding whether a JPA entity is transient, managed (persistent), or detached is crucial when updates don’t persist. For example, in a simple demo with EntityManager, creating a new object like Foo f = new Foo() leaves it transient, but once em.persist(f) is called, it becomes managed (you can confirm with a breakpoint and evaluating em.contains(f), which should return true). Detaching the entity using em.detach(f) makes it detached again (em.contains(f) returns false), and although changes to it won’t be tracked, you can call em.merge(f) to obtain a managed instance back. In the debugger, useful expressions include em.contains(f) (true if the entity is managed), em.getTransaction().isActive() (to confirm an active transaction in non-Spring-managed contexts), and advanced org.hibernate.SessionImplementor calls for low-level inspection if needed. A sample trace might show Persisted id=42, After detach: em.contains(f) = false, and After merge: em.contains(merged) = true. Breakpoints let you evaluate em.contains() directly and IntelliJ’s Variables pane lets you expand the entity object graph to inspect lazy associations — but be careful, as accessing them may trigger unexpected database queries outside a transaction.
8. Spring Debugger Plugin for IntelliJ IDEA
The Spring Debugger plugin for IntelliJ IDEA enhances standard debugging by providing Spring-aware insights directly in the IDE. Unlike the regular debugger, this plugin understands Spring constructs such as beans, application contexts, dependency injection, and AOP proxies, making it easier to trace runtime behavior in Spring Boot applications.
8.1 Key Features
- Bean-aware breakpoints: Set breakpoints that trigger when specific beans are created, injected, or destroyed.
- Context visualization: Inspect the full application context hierarchy, including parent/child contexts, and see which beans are active at runtime.
- Dependency injection tracing: Track exactly which implementation is injected where, helping to debug issues with multiple candidates or miswired beans.
- AOP and proxy support: Understand how Spring proxies affect method calls, useful when debugging transactional or security-related issues.
- Integration with IntelliJ Spring Tool Window: Combine this with the Spring view to jump from a bean in the hierarchy directly to the source code and see runtime values.
8.2 Using the Plugin Across Debugging Scenarios
The true power of the Spring Debugger plugin becomes clear when you apply it to the common debugging cases, such as:
- Breakpoints in Controllers: Instead of setting plain method breakpoints, use bean-aware breakpoints that stop when a specific controller bean is invoked, giving you more control when multiple controllers exist.
- Viewing Loaded Beans: The plugin complements the Spring tool window by showing runtime bean instantiation and wiring directly in the debugger, helping confirm which implementation is injected.
- Inspecting Properties: Combine the debugger with the plugin’s environment view to quickly check which property source supplied a value, without relying only on Actuator endpoints.
- Database Connectivity: When stepping through datasource initialization, the plugin shows how the DataSource bean is wired into your services, clarifying if the wrong one is injected.
- Transactions: With proxy and AOP awareness, the plugin makes it clear when a method is running through a transactional proxy versus being called directly, addressing the self-invocation pitfall.
- JPA Entities: Use the debugger together with plugin context visualization to track the lifecycle of your EntityManager bean and confirm how it’s being injected into repositories or services.
With these integrations, the Spring Debugger plugin is not just an add-on but a cross-cutting enhancement that strengthens all your debugging workflows. By using the Spring Debugger plugin alongside IntelliJ’s standard debugger, you gain a more contextual, Spring-specific view of your application. This reduces the need for excessive logging or runtime inspections and lets you quickly pinpoint wiring, lifecycle, or proxy issues.
9. Conclusion
IntelliJ IDEA + Spring Boot Actuator + Database tools form a powerful environment for root-cause debugging. Use conditional breakpoints, Evaluate Expression, the Spring tool window, and Actuator endpoints to discover what the runtime is actually doing — which beans are loaded, what property values are in effect, whether transactions are active, and the lifecycle state of JPA entities. Put these patterns into practice, and you’ll reduce the time it takes to move from “it doesn’t work” to a minimal reproducible cause. Happy debugging!




