How to Fix NullPointerException in Spring Boot
How do I fix NullPointerException in Spring Boot?
TL;DR
- Bottom line: 90% of Spring Boot NPEs come from 4 root causes: (1) calling
newinstead of letting Spring inject, (2) missing@Component/@Service, (3) calling injected fields before construction completes, (4) accessing a scoped bean outside its scope. - Key tool/command: Check the stack trace for
nullfield access, then verify the class is Spring-managed (notnew-instantiated). - Watch out for:
@Autowiredon fields in classes created withnew MyClass(). - 2026 update: Spring Boot 4.0 / Spring Framework 7.0 (Nov 2025) adopt JSpecify
@Nullable/@NullMarkedannotations across the portfolio — IntelliJ 2025.3+ and NullAway catch potential NPEs at compile time. [src9] - Works with: Spring Boot 2.x-4.x, Spring Framework 5.x-7.x, Java 17+ (Boot 3.x/4.x baseline; Java 25 recommended for full JSpecify/NullAway).
Constraints
- Spring only injects into beans it manages — objects created with
newnever receive injection. [src1] - Spring Boot 3.0+ requires Java 17 minimum and
jakarta.*namespace —javax.injectannotations will not work; Spring Boot 4.0+ dropsjavax.annotationentirely. [src8, src9] - Spring Boot 4.0 / Spring Framework 7.0 (Nov 2025) adopts JSpecify
@Nullable/@NullMarkedacross the portfolio — IDEs and NullAway flag unchecked null usages at compile time. [src9] - AOT/native-image builds (GraalVM) require runtime hints for reflection-based injection — missing hints cause NPE at runtime even when the bean exists. [src1]
- Spring Framework 6.2+ uses a revised autowiring algorithm with stricter generic type matching — injections that worked in 6.1 may fail silently in 6.2. [src8]
@Autowired(required=false)silently sets fields tonullwithout any startup error — never use on required dependencies. [src7]
Quick Reference
| # | NPE Pattern | Likelihood | Signature | Fix |
|---|---|---|---|---|
| 1 | @Autowired field is null |
~40% | this.someService is null |
Class instantiated with new — let Spring manage the bean [src3] |
| 2 | @Autowired field null (bean exists) |
~20% | Bean class has no stereotype annotation | Add @Component/@Service/@Repository [src1] |
| 3 | NPE in constructor | ~10% | NPE in <init> method |
Move to @PostConstruct or constructor injection [src1] |
| 4 | NPE in @Scheduled |
~5% | Method is static or class not a bean |
Ensure @Component and non-static [src2] |
| 5 | NPE accessing Optional.get() |
~8% | NoSuchElementException |
Use Optional.orElseThrow() [src5] |
| 6 | NPE in filter/interceptor | ~5% | Filter outside Spring context | Register via FilterRegistrationBean [src6] |
| 7 | NPE in test | ~8% | Using @Mock without @InjectMocks |
Use @SpringBootTest + @MockBean [src4] |
| 8 | NPE with @Value |
~4% | Property not set or class not a bean | Check application.properties [src2] |
Decision Tree
START
├── Is the NPE on a @Autowired / injected field?
│ ├── YES → Was the object created with "new"?
│ │ ├── YES → ROOT CAUSE: Spring doesn't inject into manually-created objects [src3]
│ │ │ └── FIX: Inject the bean instead of using "new"
│ │ └── NO → Does the class have @Component/@Service/@Repository?
│ │ ├── NO → ROOT CAUSE: Missing stereotype annotation [src1]
│ │ │ └── FIX: Add @Service or @Component to the class
│ │ └── YES → Is the field accessed in the constructor?
│ │ ├── YES → ROOT CAUSE: Fields not yet injected during construction [src1]
│ │ │ └── FIX: Use constructor injection or @PostConstruct
│ │ └── NO → Is class in a package scanned by @SpringBootApplication?
│ │ ├── NO → ROOT CAUSE: Class outside component scan range [src2]
│ │ │ └── FIX: Move class or add @ComponentScan
│ │ └── YES → Check for @Autowired(required=false) or @Conditional
│ └── NO → Is it Optional.get() without isPresent check?
│ ├── YES → FIX: Use Optional.orElseThrow() [src5]
│ └── NO → Is this a GraalVM native image build?
│ ├── YES → FIX: Add @RegisterReflectionForBinding or RuntimeHints [src1]
│ └── NO → Standard Java NPE — check for null references
└── DEFAULT → logging.level.org.springframework=DEBUG
Step-by-Step Guide
1. Read the stack trace carefully
The NPE stack trace tells you exactly which field is null and where. Java 17+ provides enhanced NPE messages with the exact null reference. [src2]
java.lang.NullPointerException: Cannot invoke "com.example.UserService.findById(Long)"
because "this.userService" is null
at com.example.UserController.getUser(UserController.java:25)
This means userService field is null at line 25 of UserController.
2. Check if the class is Spring-managed
// ❌ THIS CAUSES NPE — "new" bypasses Spring DI
UserController controller = new UserController();
controller.getUser(1L); // userService is null!
// ✅ CORRECT — let Spring inject
@RestController // Spring manages this
public class UserController {
private final UserService userService;
public UserController(UserService userService) { // Constructor injection
this.userService = userService;
}
}
Verify: Check that the class has a stereotype annotation (@Component, @Service, @Repository, @Controller, @RestController, @Configuration). [src1]
3. Verify the dependency is a bean
// ❌ Missing @Service — Spring won't create this bean
public class UserService {
public User findById(Long id) { ... }
}
// ✅ CORRECT — @Service marks it as a Spring bean [src1]
@Service
public class UserService {
public User findById(Long id) { ... }
}
Verify: Run with --debug flag and search the auto-configuration report for the bean name.
4. Use constructor injection (recommended over field injection)
// ❌ Field injection — harder to test, NPE if constructed manually
@RestController
public class UserController {
@Autowired
private UserService userService; // null if new UserController()
}
// ✅ Constructor injection — fail-fast, testable [src1]
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService; // guaranteed non-null
}
}
Verify: Application fails to start if the dependency is missing (fail-fast behavior). [src7]
5. Verify component scan coverage
// Main class in com.example
@SpringBootApplication // Scans com.example and sub-packages
public class MyApp { ... }
// ❌ This bean is OUTSIDE the scan range
package com.other; // NOT under com.example
@Service
public class UserService { ... } // Will NOT be discovered
// ✅ FIX: Add explicit component scan
@SpringBootApplication
@ComponentScan(basePackages = {"com.example", "com.other"})
public class MyApp { ... }
Verify: logging.level.org.springframework=DEBUG — look for "Scanning" messages. [src2]
Code Examples
Java: Common NPE scenario and fix
Full script: java-complete-example-common-npe-scenario-and-fix.java (57 lines)
// SCENARIO: @Autowired field is null
// Input: Controller with field injection, instantiated manually
// Output: NullPointerException at runtime
// ❌ BAD — the helper is created with "new", so Spring doesn't inject
@RestController
public class OrderController {
@Autowired private OrderService orderService;
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable Long id) {
OrderValidator validator = new OrderValidator(); // PROBLEM HERE
validator.validate(id); // NPE — validator.orderRepo is null
return orderService.findById(id);
}
}
public class OrderValidator {
@Autowired private OrderRepository orderRepo; // ALWAYS NULL
public void validate(Long id) {
orderRepo.existsById(id); // NullPointerException!
}
}
// ✅ GOOD — inject the validator as a bean
@Component // Now Spring manages it
public class OrderValidator {
private final OrderRepository orderRepo;
public OrderValidator(OrderRepository orderRepo) {
this.orderRepo = orderRepo; // Guaranteed non-null
}
public void validate(Long id) {
if (!orderRepo.existsById(id)) {
throw new IllegalArgumentException("Order not found: " + id);
}
}
}
@RestController
public class OrderController {
private final OrderService orderService;
private final OrderValidator validator; // Injected by Spring
public OrderController(OrderService orderService, OrderValidator validator) {
this.orderService = orderService;
this.validator = validator;
}
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable Long id) {
validator.validate(id); // Works!
return orderService.findById(id);
}
}
Kotlin: Spring Boot NPE patterns
// ❌ BAD — lateinit with field injection, NPE if not initialized
@Service
class NotificationService {
@Autowired
lateinit var emailSender: EmailSender // NPE if accessed before injection
fun notify(user: User) {
emailSender.send(user.email, "Hello") // UninitializedPropertyAccessException
}
}
// ✅ GOOD — constructor injection, null-safe
@Service
class NotificationService(
private val emailSender: EmailSender // Injected, non-null
) {
fun notify(user: User) {
emailSender.send(user.email, "Hello") // Always works
}
}
Java: Spring Boot 3.x with GraalVM native image
// ❌ BAD — reflection-based injection fails in native image without hints
@Service
public class ReportService {
@Autowired
private ReportGenerator generator; // NPE in native image — no reflection metadata
}
// ✅ GOOD — constructor injection + runtime hints for native image
@Service
@RegisterReflectionForBinding(ReportGenerator.class) // Spring Boot 3.x
public class ReportService {
private final ReportGenerator generator;
public ReportService(ReportGenerator generator) {
this.generator = generator; // Works in JVM and native image
}
}
Anti-Patterns
Wrong: Using new to create Spring beans
// ❌ BAD — Spring can't inject into objects you create with new [src3]
OrderService service = new OrderService();
service.processOrder(orderId); // NPE — service.repo is null
Correct: Inject beans through Spring
// ✅ GOOD — let Spring wire everything [src1]
@Service
public class PaymentProcessor {
private final OrderService orderService; // Injected
public PaymentProcessor(OrderService orderService) {
this.orderService = orderService;
}
}
Wrong: Accessing injected fields in the constructor
// ❌ BAD — @Autowired fields are null during construction [src1]
@Service
public class CacheWarmer {
@Autowired private UserRepository userRepo;
public CacheWarmer() {
userRepo.findAll(); // NPE — field not yet injected!
}
}
Correct: Use @PostConstruct for initialization logic
// ✅ GOOD — @PostConstruct runs after injection is complete [src1]
@Service
public class CacheWarmer {
private final UserRepository userRepo;
public CacheWarmer(UserRepository userRepo) {
this.userRepo = userRepo;
}
@PostConstruct
public void warmCache() {
userRepo.findAll(); // Works — injection is complete
}
}
Wrong: Using @Autowired(required=false) for required dependencies
// ❌ BAD — silently null if bean missing, NPE at runtime [src7]
@Service
public class PaymentService {
@Autowired(required = false)
private PaymentGateway gateway; // null without warning
public void charge(Order order) {
gateway.charge(order.getTotal()); // NPE!
}
}
Correct: Let Spring fail fast on missing required beans
// ✅ GOOD — constructor injection fails at startup if bean missing [src1]
@Service
public class PaymentService {
private final PaymentGateway gateway;
public PaymentService(PaymentGateway gateway) {
this.gateway = Objects.requireNonNull(gateway); // Belt-and-suspenders
}
public void charge(Order order) {
gateway.charge(order.getTotal()); // Always works
}
}
Common Pitfalls
newkeyword bypasses Spring DI: The #1 cause of@AutowiredNPEs. Fix: always inject beans through constructors. [src3]- Missing
@Componentscan: Class in a package not scanned by@SpringBootApplication. Fix: ensure same or sub-package. [src2] - Static methods accessing instance fields: Static methods can't access
@Autowiredfields. Fix: make non-static. [src2] - Circular dependencies causing partial init: Fix: use
@Lazyor restructure. [src1] - Test context not loaded:
@Autowiredin tests without@SpringBootTestresults in null. Fix: use@SpringBootTestor@ExtendWith(MockitoExtension.class). [src4] Optional.get()without checking: JPAfindById()returnsOptional. Fix: use.orElseThrow(). [src5]- Spring Framework 6.2 stricter generic matching: Generic type matching is stricter in 6.2+. A bean that matched in 6.1 may not match in 6.2, causing null injection. Fix: verify generic signatures. [src8]
- GraalVM native image missing reflection metadata: Code works on JVM but NPEs in native image. Fix: add
@RegisterReflectionForBindingor provideRuntimeHints. [src1]
Diagnostic Commands
# Enable Spring debug logging to see bean creation
# application.properties:
logging.level.org.springframework=DEBUG
# List all beans in the application context
# Add to main class:
@Bean
CommandLineRunner printBeans(ApplicationContext ctx) {
return args -> Arrays.stream(ctx.getBeanDefinitionNames()).sorted().forEach(System.out::println);
}
# Check component scan base packages
# Look for @SpringBootApplication or @ComponentScan in your main class
# Run with debug to see auto-configuration report
java -jar app.jar --debug
# Check if a specific bean is registered (Spring Boot Actuator)
curl http://localhost:8080/actuator/beans | jq '.contexts[].beans | keys[] | select(contains("UserService"))'
# Verify AOT processing for native image (Spring Boot 3.x)
./gradlew processAot # or: mvn spring-boot:process-aot
# Check generated sources in build/generated/aotSources
Version History & Compatibility
| Spring Version | Status | Key DI Changes | Migration Notes |
|---|---|---|---|
| Spring Boot 4.0 / Spring Framework 7.0 (Nov 2025) | Current | JSpecify @Nullable/@NullMarked across the portfolio; javax.annotation and javax.inject no longer supported; SpringExtension uses test-method scoped ExtensionContext [src9] |
Migrate any remaining javax.* annotations to jakarta.*; add JSpecify deps and @NullMarked packages; verify custom TestExecutionListener for new ExtensionContext scope |
| Spring Boot 3.5 (LTS through Nov 2026) | Maintained | Compatible with Spring Framework 6.2 autowiring algorithm | Final 3.x line; plan upgrade to Boot 4 |
| Spring Boot 3.4 / Spring 6.2 | Maintained | Revised autowiring algorithm, stricter generic matching, @Fallback beans [src8] |
Verify generic type signatures at injection points |
| Spring Boot 3.0-3.3 / Spring 6.0-6.1 | Maintained | Jakarta namespace (javax -> jakarta), Java 17+, AOT compilation |
Update all javax.inject to jakarta.inject |
| Spring Boot 2.7 / Spring 5.3 | EOL (Nov 2023) | Single-constructor injection without @Autowired (since 4.3) |
Upgrade to Boot 3.x for continued support |
| Spring Boot 2.x / Spring 5.x | EOL | Auto-configuration, @ConditionalOnMissingBean |
— |
| Spring 4.3+ | Legacy | Implicit constructor injection for single-constructor beans [src1] | — |
When to Use / When Not to Use
| Use Constructor Injection When | Use Field Injection When | Use Setter Injection When |
|---|---|---|
| Always (recommended default) [src7] | Legacy code only (migration in progress) | Optional dependencies |
| Production code | Never in new code | Circular dependency breaking (with @Lazy) |
| Test-friendly code needed | Quick throwaway prototypes | Reconfigurable beans at runtime |
Important Caveats
- Constructor injection is the Spring team's recommended approach since Spring 4.3. It makes NPEs impossible for required dependencies. [src1]
@Autowired(required = false)can introduce NPEs silently — the field will be null without any startup error. [src7]- Spring Boot 3.x with native compilation (GraalVM) may have different DI behavior — ensure all beans are discoverable at build time. Add
@RegisterReflectionForBindingwhere needed. - Spring Framework 6.2 introduced a revised autowiring algorithm where
@Qualifiermatches now overrule@Priorityranking (reversed from 6.1). This can change which bean gets injected. [src8] - Spring Boot 3.4+ introduces
@Fallbackbeans — a fallback bean is used only when no other bean of that type is defined, which can mask injection issues if not understood. [src8] - Spring Boot 4.0 / Spring Framework 7.0 (Nov 2025) drops
javax.annotationandjavax.injectentirely — any code still relying on@javax.annotation.PostConstructor@javax.inject.Injectwill fail to compile or inject after upgrade. Migrate tojakarta.*equivalents. [src9] - Spring Boot 4.0 portfolio APIs are JSpecify-annotated. If you wire your own beans through generic Spring APIs and your module is
@NullMarked, parameters that used to silently accept null may now require explicit@Nullable— NullAway/IDE will flag them at compile time. [src9]