Spring Boot
@RefreshScope
@PostConstruct
@PreDestroy
Java Annotations

Spring boot with RefreshScope PostConstruct PreDestroy

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In Spring Boot, @PostConstruct and @PreDestroy are lifecycle callbacks that run after a bean is initialized and before it is destroyed. @RefreshScope (from Spring Cloud) allows beans to be recreated at runtime when configuration changes, without restarting the application. When these annotations are combined, @PostConstruct and @PreDestroy run every time the bean is refreshed, not just on application startup and shutdown. Understanding this interaction is critical for managing resources like database connections, caches, and scheduled tasks in dynamically-refreshable beans.

@PostConstruct and @PreDestroy Basics

java
1import jakarta.annotation.PostConstruct;
2import jakarta.annotation.PreDestroy;
3import org.springframework.stereotype.Component;
4
5@Component
6public class CacheService {
7
8    private Map<String, Object> cache;
9
10    @PostConstruct
11    public void init() {
12        // Runs AFTER dependency injection is complete
13        cache = new ConcurrentHashMap<>();
14        loadCacheFromDatabase();
15        System.out.println("Cache initialized");
16    }
17
18    @PreDestroy
19    public void cleanup() {
20        // Runs BEFORE the bean is removed from the container
21        cache.clear();
22        System.out.println("Cache cleared");
23    }
24
25    private void loadCacheFromDatabase() {
26        // Load initial data
27    }
28}

@PostConstruct runs once after the constructor and all @Autowired fields are set. @PreDestroy runs when the application context shuts down. Both are part of the Jakarta (formerly javax) annotations specification.

@RefreshScope

@RefreshScope marks a bean for dynamic recreation when a refresh event occurs. This is typically triggered by calling the /actuator/refresh endpoint or through Spring Cloud Bus:

java
1import org.springframework.cloud.context.config.annotation.RefreshScope;
2import org.springframework.beans.factory.annotation.Value;
3import org.springframework.stereotype.Component;
4
5@Component
6@RefreshScope
7public class ApiClient {
8
9    @Value("${api.base-url}")
10    private String baseUrl;
11
12    @Value("${api.timeout:5000}")
13    private int timeout;
14
15    public String callApi(String endpoint) {
16        // Uses the current baseUrl and timeout values
17        return httpClient.get(baseUrl + endpoint, timeout);
18    }
19}

When you update api.base-url in your config server and hit /actuator/refresh, Spring destroys the old ApiClient bean and creates a new one with the updated @Value fields.

@RefreshScope with @PostConstruct and @PreDestroy

This is where the behavior becomes important. When a @RefreshScope bean is refreshed, the full lifecycle runs:

java
1@Component
2@RefreshScope
3public class ConnectionPool {
4
5    @Value("${db.pool.size:10}")
6    private int poolSize;
7
8    @Value("${db.url}")
9    private String dbUrl;
10
11    private DataSource dataSource;
12
13    @PostConstruct
14    public void init() {
15        // Runs on startup AND on every refresh
16        System.out.println("Creating pool: size=" + poolSize + ", url=" + dbUrl);
17        dataSource = createDataSource(dbUrl, poolSize);
18    }
19
20    @PreDestroy
21    public void shutdown() {
22        // Runs on application shutdown AND on every refresh (before reinit)
23        System.out.println("Closing pool for: " + dbUrl);
24        if (dataSource != null) {
25            dataSource.close();
26        }
27    }
28
29    private DataSource createDataSource(String url, int size) {
30        HikariConfig config = new HikariConfig();
31        config.setJdbcUrl(url);
32        config.setMaximumPoolSize(size);
33        return new HikariDataSource(config);
34    }
35}

Refresh sequence:

  1. /actuator/refresh is called
  2. @PreDestroy shutdown() runs on the old bean instance
  3. The old bean is destroyed
  4. A new bean is created with updated @Value fields
  5. @PostConstruct init() runs on the new bean instance

Triggering a Refresh

bash
1# Update config in Spring Cloud Config Server, then:
2curl -X POST http://localhost:8080/actuator/refresh
3
4# Response shows which properties changed:
5# ["api.base-url", "db.pool.size"]

Enable the refresh endpoint in application.yml:

yaml
1management:
2  endpoints:
3    web:
4      exposure:
5        include: refresh, health, info

Dependencies required in pom.xml:

xml
1<dependency>
2    <groupId>org.springframework.cloud</groupId>
3    <artifactId>spring-cloud-starter-config</artifactId>
4</dependency>
5<dependency>
6    <groupId>org.springframework.boot</groupId>
7    <artifactId>spring-boot-starter-actuator</artifactId>
8</dependency>

Handling Expensive Initialization

Since @PostConstruct runs on every refresh, avoid expensive operations that do not need to repeat:

java
1@Component
2@RefreshScope
3public class FeatureFlagService {
4
5    @Value("${feature.flags.enabled:true}")
6    private boolean enabled;
7
8    @Value("${feature.flags.cache-ttl:300}")
9    private int cacheTtlSeconds;
10
11    // Static resources initialized once, not per refresh
12    private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
13
14    private ScheduledExecutorService scheduler;
15
16    @PostConstruct
17    public void init() {
18        System.out.println("FeatureFlags init: enabled=" + enabled
19            + ", ttl=" + cacheTtlSeconds);
20        if (enabled) {
21            scheduler = Executors.newSingleThreadScheduledExecutor();
22            scheduler.scheduleAtFixedRate(
23                this::refreshFlags, 0, cacheTtlSeconds, TimeUnit.SECONDS
24            );
25        }
26    }
27
28    @PreDestroy
29    public void destroy() {
30        if (scheduler != null) {
31            scheduler.shutdown();
32            System.out.println("Scheduler stopped");
33        }
34    }
35
36    private void refreshFlags() {
37        // Fetch latest flags from remote service
38    }
39}

The scheduler is properly shut down in @PreDestroy before being recreated in @PostConstruct on refresh. Without the @PreDestroy cleanup, each refresh would leak a scheduler thread.

Common Pitfalls

  • Resource leaks on refresh: If @PreDestroy does not release resources (connections, threads, file handles), each refresh leaks them. Always clean up in @PreDestroy anything that @PostConstruct creates.
  • Expecting @RefreshScope to update non-bean values: Only Spring-managed beans annotated with @RefreshScope are recreated. Static fields, constants, and beans without @RefreshScope keep their original values after a config change.
  • Blocking operations in @PostConstruct: Long-running initialization in @PostConstruct blocks the refresh. Other requests to this bean will wait. Keep initialization fast or move heavy work to a background thread started in @PostConstruct.
  • Missing Spring Cloud dependencies: @RefreshScope requires spring-cloud-context on the classpath. Without it, the annotation is silently ignored and beans are never refreshed.
  • Injecting @RefreshScope beans into singleton beans: A singleton bean holds a reference to the original instance. After refresh, the singleton still points to the old bean. Inject @RefreshScope beans as ObjectProvider<T> or use @Lazy proxy injection to always get the current instance.

Summary

  • @PostConstruct runs after dependency injection; @PreDestroy runs before bean destruction
  • @RefreshScope beans are destroyed and recreated when /actuator/refresh is triggered
  • Both @PostConstruct and @PreDestroy run on every refresh cycle, not just startup/shutdown
  • Always clean up resources in @PreDestroy to prevent leaks during refresh
  • Inject @RefreshScope beans using ObjectProvider or @Lazy to avoid stale references in singleton beans

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

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

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.