As a senior software engineer, I’m constantly navigating the balance between code readability, framework conventions, and real-world maintainability. Recently, a minor code review comment turned into a subtle debugging session that reminded me once again: annotations are powerful, but their magic must be understood deeply to avoid surprises.
The Setup: Constructor Injection in a Spring Controller
In a typical Spring Boot project, I had the following straightforward controller implementation:
@RestController
public class MyController {
private MyService myService;
public MyController(MyService myService) {
this.myService = myService;
}
public MyResponse createAccount() {
myService.myMethod();
}
// ...
}
This is clean, explicit constructor injection — something I’ve used and trusted for years. I had also written a full integration test using WebTestClient to verify the entire workflow, and everything passed smoothly.
The Code Review: “Use@RequiredArgsConstructor”
During a code review, a team member pointed out that the company’s coding guideline encourages the use of Lombok’s @RequiredArgsConstructor to reduce boilerplate. I’m personally not a big fan of @RequiredArgsConstructor, mostly because its behavior isn’t always obvious at a glance. That said, I didn’t see any immediate harm and refactored the controller:
@RestController
@RequiredArgsConstructor
public class MyController {
private MyService myService;
public MyResponse createAccount() {
myService.myMethod();
}
// ...
}
No constructor. Just Lombok magic.
The Surprise: NullPointerException in Integration Test
To my surprise, my integration test suddenly failed with a NullPointerException at myService.myMethod(). It was clear: myService was null. But why?
How@RequiredArgsConstructorReally Works
Here’s the catch: @RequiredArgsConstructor only generates a constructor for fields that are either: final, or annotated with @NonNull.
In my refactored code, myService was neither.
So although the annotation was present, no constructor was actually generated by Lombok. Spring Boot silently used the default no-args constructor — and since myService was never initialized, the result was a null pointer.
The Fix
After reviewing the Lombok documentation, I updated my field to be:
private final MyService myService;
Now @RequiredArgsConstructor kicked in, generated the proper constructor, and Spring injected myService as expected. My integration test passed again.
Key Takeaways
- Annotations like @RequiredArgsConstructor can be deceptive if not used carefully. Understanding how and when they generate code is crucial — especially in dependency injection scenarios.
- Constructor injection remains the most explicit and reliable form of wiring dependencies, particularly in Spring-based applications.
- Final fields signal immutability and are necessary for Lombok to do its job with @RequiredArgsConstructor. Final Thoughts Lombok can reduce boilerplate, but it comes with trade-offs. In teams, especially larger ones, annotations like @RequiredArgsConstructor can improve consistency — but only if everyone understands the magic behind the scenes.
So next time you get a null pointer where you least expect it, don’t forget to check whether your “constructor” actually exists.
Top comments (0)