DEV Community

Luke Tong
Luke Tong

Posted on

2

The Subtle Pitfall of @RequiredArgsConstructor: A Lesson from Integration Testing

Image description

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();
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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();
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Survey image

Shape the Future of Cloud Dev - Win Big!

Share your cloud dev experience in the Developer Nation Survey. Win AI credits, gear & more!

Start Now

Top comments (0)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →