🔄 Deep Dive: Refresh Tokens in Spring Boot Microservices (Java 25 + PostgreSQL + JWT)
👨💻 I'm Praveen Gupta, a Senior Backend Engineer with over 10+ years of experience in designing scalable backend systems using Java, Spring Boot, Microservices, and AWS Cloud. I specialize in building cloud-native applications, integrating DevOps practices, and mentoring teams in writing clean, production-grade code. I created this blog to share real-world backend architecture tips, Spring Boot patterns, and practical coding solutions that help developers grow faster.
When building secure microservices, access tokens are your first line of defense — but they’re short-lived. What happens when they expire? Do we keep asking users to log in again?
That’s where refresh tokens come in — the silent heroes that keep users authenticated without friction.
🧠 Understanding Access vs Refresh Tokens
| Token Type | Purpose | Lifespan | Storage |
| Access Token | Grants access to protected APIs | Short (e.g., 15 min) | Usually stored in memory or cookies |
| Refresh Token | Generates new access tokens without logging in again | Long (e.g., 7 days or 30 days) | Stored securely (DB or HTTP-only cookie) |
⚙️ Token Flow Diagram
🖼️ “JWT Refresh Token Flow in Microservices” simple Flow -

🧩 Token Structure Breakdown
Both Access and Refresh tokens are JWTs (JSON Web Tokens) composed of three parts:
To know what JWT is? Follow the “understanding-jwt-json-web-tokens-the-developer-friendly-guide” link.
Header.Payload.Signature
Example:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwcmF2ZWVuIiwiaWF0IjoxNzM4OTQwMDAsImV4cCI6MTczODk0MDA5fX0.V8fjE2M0T8uCSy8...
| Part | Description |
| Header | Algorithm (e.g., HS256) |
| Payload | User info (claims) like sub, exp, roles |
| Signature | Hash that ensures data integrity |
🏗️ Project Setup (Java 25 + Spring Boot 3.4 + PostgreSQL)
1. Dependencies (Maven)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
2. application.properties
server.port=8081
spring.datasource.url=jdbc:postgresql://localhost:5432/authdb
spring.datasource.username=postgres
spring.datasource.password=admin
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
jwt.secret=mySuperSecretKey123456
jwt.access.expiration=900000 # 15 min
jwt.refresh.expiration=604800000 # 7 days
🧍♂️ User & Token Entities
UserEntity.java
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String role;
}
RefreshTokenEntity.java
@Entity
@Table(name = "refresh_tokens")
public class RefreshTokenEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String token;
private Instant expiryDate;
@OneToOne
@JoinColumn(name = "user_id", referencedColumnName = "id")
private UserEntity user;
}
🔐 Token Utility Class
JwtUtil.java
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.access.expiration}")
private long accessTokenExpiry;
@Value("${jwt.refresh.expiration}")
private long refreshTokenExpiry;
public String generateAccessToken(UserEntity user) {
return Jwts.builder()
.subject(user.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + accessTokenExpiry))
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
.compact();
}
public String generateRefreshToken(UserEntity user) {
return Jwts.builder()
.subject(user.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + refreshTokenExpiry))
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
.compact();
}
public String extractUsername(String token) {
return Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
public boolean isTokenValid(String token) {
try {
Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException e) {
return false;
}
}
}
🔄 Refresh Token Service
RefreshTokenService.java
@Service
public class RefreshTokenService {
@Autowired
private RefreshTokenRepository repository;
@Autowired
private UserRepository userRepository;
@Autowired
private JwtUtil jwtUtil;
public RefreshTokenEntity createRefreshToken(String username) {
UserEntity user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
RefreshTokenEntity token = new RefreshTokenEntity();
token.setUser(user);
token.setExpiryDate(Instant.now().plusMillis(604800000)); // 7 days
token.setToken(jwtUtil.generateRefreshToken(user));
return repository.save(token);
}
public String refreshAccessToken(String refreshToken) {
if (!jwtUtil.isTokenValid(refreshToken))
throw new RuntimeException("Invalid refresh token");
String username = jwtUtil.extractUsername(refreshToken);
UserEntity user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
return jwtUtil.generateAccessToken(user);
}
}
🚀 Auth Controller
AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RefreshTokenService refreshTokenService;
@Autowired
private UserRepository userRepository;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> loginRequest) {
String username = loginRequest.get("username");
String password = loginRequest.get("password");
UserEntity user = userRepository.findByUsername(username)
.filter(u -> u.getPassword().equals(password))
.orElseThrow(() -> new RuntimeException("Invalid credentials"));
String accessToken = jwtUtil.generateAccessToken(user);
RefreshTokenEntity refreshToken = refreshTokenService.createRefreshToken(username);
return ResponseEntity.ok(Map.of(
"access_token", accessToken,
"refresh_token", refreshToken.getToken()
));
}
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestBody Map<String, String> request) {
String refreshToken = request.get("refresh_token");
String newAccessToken = refreshTokenService.refreshAccessToken(refreshToken);
return ResponseEntity.ok(Map.of("access_token", newAccessToken));
}
}
🧱 PostgreSQL Schema Snapshot
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
role VARCHAR(50)
);
CREATE TABLE refresh_tokens (
id SERIAL PRIMARY KEY,
token TEXT NOT NULL,
expiry_date TIMESTAMP NOT NULL,
user_id INT REFERENCES users(id)
);
🧰 Microservice Deployment Design
Auth Service → Handles
/loginand/refreshendpointsResource Service → Consumes
access_tokenfor API accessTokens shared via JWT claims and validated at Gateway
🛡️ Security Best Practices
✅ Use HTTP-only cookies for token storage
✅ Keep refresh tokens short-lived and rotated
✅ Store refresh tokens in PostgreSQL or Redis
✅ Invalidate old tokens during logout
✅ Use TLS (HTTPS) everywhere
🏁 Wrapping Up
Refresh Tokens are essential for a stateless, secure, and user-friendly authentication flow.
By combining JWT, Spring Boot 3.4, and PostgreSQL, you’ve got a production-ready, scalable approach to authentication for microservices.
🚀 Coming Next
In the next post, we’ll integrate this Auth Service with:
Spring Cloud Gateway (Java 25) for token validation
User Service + Product Service
Centralized Config + Docker Compose
💬 Feedback?
Found this useful?
Leave a comment or connect with me on LinkedIn.
You can find the code here:
🔗 GitHub – /blog-refresh-token-java25
