Why Two Methods for JWT Token Generation? Understanding Single Responsibility Principle
"Why do we need both generateToken() (public) and createToken() (private) instead of just one method?" Because separation of concerns makes your code extensible. The public method handles what claims to add (business logic), while the private method handles how to build the JWT (technical details). This lets you add new token types without repeating code. Let's understand why this design pattern is essential.
The Code in Questionβ
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration:86400000}")
private long expirationTime;
private Key getSigningKey() {
return Keys.hmacShaKeyFor(secretKey.getBytes());
}
// Public method: Generate JWT token for a user
public String generateToken(String email) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, email);
}
// Private method: Create the actual JWT token
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
}
Your question:
"Why do you think we need both
generateToken()(public) andcreateToken()(private) as separate methods instead of just one?"
Hint: Think about future features... what if you wanted to add custom data to certain tokens? π€
The Problem: One Method Designβ
What It Would Look Likeβ
// β Bad Design: Everything in one method
public String generateToken(String email) {
return Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
Looks simple, right? But wait...
The Real-World Requirementβ
What if you need tokens for different purposes?
- Standard login token - Just email
- Admin token - Email + role
- Password reset token - Email + expiration (1 hour instead of 24)
- API token - Email + permissions
With One Method: Code Duplicationβ
// Login token
public String generateLoginToken(String email) {
return Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// Admin token (needs role)
public String generateAdminToken(String email, String role) {
return Jwts.builder()
.claim("role", role) // β Only difference!
.setSubject(email)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// Password reset token (1 hour expiration)
public String generatePasswordResetToken(String email) {
return Jwts.builder()
.claim("type", "password_reset")
.setSubject(email)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // β Only difference!
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// API token (needs permissions)
public String generateApiToken(String email, List<String> permissions) {
return Jwts.builder()
.claim("permissions", permissions) // β Only difference!
.setSubject(email)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
Problems:
- β Code duplication - Same 6 lines repeated 4 times
- β DRY violation - Don't Repeat Yourself principle broken
- β Hard to maintain - Change algorithm? Update 4 places!
- β Error-prone - Easy to forget to update one method
- β Harder to test - Must test same logic 4 times
The Solution: Separation of Concernsβ
Good Design: Two Methodsβ
// Public method 1: Regular login
public String generateToken(String email) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, email);
}
// Public method 2: Admin token
public String generateAdminToken(String email, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", role); // β Business logic: What to add
return createToken(claims, email); // β Reuse technical details
}
// Public method 3: Password reset
public String generatePasswordResetToken(String email) {
Map<String, Object> claims = new HashMap<>();
claims.put("type", "password_reset");
return createTokenWithShortExpiration(claims, email, 3600000); // 1 hour
}
// Public method 4: API token
public String generateApiToken(String email, List<String> permissions) {
Map<String, Object> claims = new HashMap<>();
claims.put("permissions", permissions);
return createToken(claims, email);
}
// Private method: Reusable token builder (technical details)
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
Benefits:
- β DRY - Token-building logic written once
- β Easy to extend - Add new token types without duplicating code
- β Easy to maintain - Change algorithm in ONE place
- β Clear separation - Business logic vs technical details
- β Easy to test - Test token building once, test claim variations separately
The Restaurant Kitchen Analogyβ
Think of JWT token generation like a restaurant kitchen:
β Bad Way: No Separationβ
Chef makes Burger:
1. Cut bread
2. Cook meat
3. Add lettuce
4. Add tomato
5. Add sauce
6. Wrap it up
Chef makes Sandwich:
1. Cut bread
2. Cook chicken
3. Add lettuce
4. Add tomato
5. Add mayo
6. Wrap it up
Chef makes Wrap:
1. Cut tortilla
2. Cook meat
3. Add lettuce
4. Add tomato
5. Add sauce
6. Wrap it up
Problem: Repeating "Cut, Cook, Add, Wrap" for each dish!
What if the restaurant changes wrapping paper?
- Update burger recipe β
- Update sandwich recipe β
- Update wrap recipe β
- 3 places to change!
β Good Way: Reusable Assembly Lineβ
General Assembly Line (private method):
1. Get base (bread/wrap/tortilla)
2. Cook protein (meat/chicken)
3. Add lettuce
4. Add tomato
5. Add toppings (custom)
6. Wrap it up
Public methods (what customers order):
- makeBurger() β calls assembly line with (bread, meat, sauce)
- makeSandwich() β calls assembly line with (bread, chicken, mayo)
- makeWrap() β calls assembly line with (tortilla, meat, sauce)
- makeTaco() β calls assembly line with (tortilla, beef, salsa)
Benefits:
- β One reusable process
- β Change wrapping paper? Update assembly line ONCE
- β Add new menu item? Just call assembly line with different ingredients
Same concept with JWT:
createToken() = Assembly line (private, reusable)
generateToken() = Customer order 1 (public, calls assembly)
generateAdminToken() = Customer order 2 (public, calls assembly)
generateApiToken() = Customer order 3 (public, calls assembly)
How It Works: The Current Codeβ
Step-by-Step Breakdownβ
// What you (the caller) provide
public String generateToken(String email) {
Map<String, Object> claims = new HashMap<>(); // β Empty claims (basic token)
return createToken(claims, email); // β Call reusable builder
}
Visual flow:
generateToken("test@example.com")
β
Create empty claims map: {}
β
Pass to createToken({}, "test@example.com")
β
createToken() builds JWT:
- Claims: {}
- Subject: "test@example.com"
- IssuedAt: 2025-10-19T10:00:00
- Expiration: 2025-10-20T10:00:00 (24 hours)
- Sign with HS256
β
Return: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
What createToken() Does (Reusable)β
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims) // Custom data (roles, permissions, etc.)
.setSubject(subject) // User identifier (email)
.setIssuedAt(new Date(System.currentTimeMillis())) // When token created
.setExpiration(new Date(System.currentTimeMillis() + expirationTime)) // When expires
.signWith(getSigningKey(), SignatureAlgorithm.HS256) // Cryptographic signature
.compact(); // Convert to JWT string
}
This method doesn't care WHAT claims you add - it just builds the token!
Future: Easy Extensionβ
Scenario: Add Admin Token with Roleβ
Old way (no separation):
// β Have to write entire method again
public String generateAdminToken(String email, String role) {
return Jwts.builder()
.claim("role", role)
.setSubject(email)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
New way (with separation):
// β
Just add what's different, reuse everything else
public String generateAdminToken(String email, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", role); // β Only new part!
return createToken(claims, email); // β Reuse existing builder
}
4 lines instead of 8! And no duplication.
Scenario: Add API Token with Permissionsβ
// β
Easy to add
public String generateApiToken(String email, List<String> permissions) {
Map<String, Object> claims = new HashMap<>();
claims.put("permissions", permissions);
return createToken(claims, email); // β Same reusable builder
}
Scenario: Add Password Reset Token (Short Expiration)β
// β
Create variant with custom expiration
private String createTokenWithCustomExpiration(
Map<String, Object> claims,
String subject,
long customExpiration
) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + customExpiration)) // β Custom
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public String generatePasswordResetToken(String email) {
Map<String, Object> claims = new HashMap<>();
claims.put("type", "password_reset");
return createTokenWithCustomExpiration(claims, email, 3600000); // 1 hour
}
All these use the same pattern! β
The Two Responsibilitiesβ
Public Method: Business Logic (What)β
public String generateAdminToken(String email, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", role); // β Decision: "Admins need role in token"
return createToken(claims, email);
}
Responsibility: Decide what claims to add based on business requirements.
Questions it answers:
- What custom data should this token contain?
- Who is this token for?
- What permissions should it grant?
Private Method: Technical Details (How)β
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256) // β How to sign
.compact(); // β How to serialize
}
Responsibility: Handle how to build the JWT token technically.
Questions it answers:
- How to structure the JWT?
- What algorithm to use?
- How to set expiration?
- How to sign the token?
Single Responsibility Principle (SRP)β
This is one of the SOLID principles:
A class should have only one reason to change.
Applied to Our Codeβ
generateToken() changes when:
- Business requirements change (e.g., "Admins need roles in token")
- New token types needed (e.g., "Add API tokens")
createToken() changes when:
- Technical requirements change (e.g., "Switch from HS256 to RS256")
- JWT library updates (e.g., "Migrate from io.jsonwebtoken to another library")
Separation ensures:
- β Business changes don't touch technical details
- β Technical changes don't touch business logic
- β Each method has ONE clear responsibility
Real-World Example: Complete Implementationβ
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration:86400000}") // 24 hours default
private long expirationTime;
private Key getSigningKey() {
return Keys.hmacShaKeyFor(secretKey.getBytes());
}
// 1. Standard login token (email only)
public String generateToken(String email) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, email);
}
// 2. Admin token (email + role)
public String generateAdminToken(String email, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", role);
claims.put("permissions", Arrays.asList("READ", "WRITE", "DELETE"));
return createToken(claims, email);
}
// 3. Password reset token (email + type, short expiration)
public String generatePasswordResetToken(String email) {
Map<String, Object> claims = new HashMap<>();
claims.put("type", "password_reset");
return createTokenWithCustomExpiration(claims, email, 3600000); // 1 hour
}
// 4. API token (email + API key + permissions)
public String generateApiToken(String email, String apiKey, List<String> permissions) {
Map<String, Object> claims = new HashMap<>();
claims.put("apiKey", apiKey);
claims.put("permissions", permissions);
return createToken(claims, email);
}
// 5. Refresh token (email only, long expiration)
public String generateRefreshToken(String email) {
Map<String, Object> claims = new HashMap<>();
claims.put("type", "refresh");
return createTokenWithCustomExpiration(claims, email, 604800000); // 7 days
}
// Private helper: Standard token builder
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// Private helper: Token builder with custom expiration
private String createTokenWithCustomExpiration(
Map<String, Object> claims,
String subject,
long customExpiration
) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + customExpiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
}
Notice:
- 5 public methods for different token types
- 2 private methods for reusable token building
- Business logic (what claims) in public methods
- Technical details (how to build) in private methods
If you need to change from HS256 to RS256:
- β Change 2 private methods (technical details)
- β DON'T change 5 public methods (business logic)
Testing Benefitsβ
With Separation: Easy to Testβ
@Test
public void testGenerateToken() {
String token = jwtUtil.generateToken("test@example.com");
Claims claims = jwtUtil.extractAllClaims(token);
assertEquals("test@example.com", claims.getSubject());
assertNotNull(claims.getIssuedAt());
assertNotNull(claims.getExpiration());
}
@Test
public void testGenerateAdminToken() {
String token = jwtUtil.generateAdminToken("admin@example.com", "ADMIN");
Claims claims = jwtUtil.extractAllClaims(token);
assertEquals("admin@example.com", claims.getSubject());
assertEquals("ADMIN", claims.get("role")); // β Test custom claim
}
@Test
public void testGeneratePasswordResetToken() {
String token = jwtUtil.generatePasswordResetToken("user@example.com");
Claims claims = jwtUtil.extractAllClaims(token);
assertEquals("password_reset", claims.get("type"));
// Test expiration is 1 hour (not 24)
}
Each public method tests business logic. Private methods tested indirectly through public methods.
Without Separation: Harder to Testβ
@Test
public void testGenerateLoginToken() {
// Test entire token building + claim logic
}
@Test
public void testGenerateAdminToken() {
// Test entire token building + claim logic (duplicated test logic!)
}
@Test
public void testGeneratePasswordResetToken() {
// Test entire token building + claim logic (duplicated test logic!)
}
More duplication, harder to maintain.
Maintenance Example: Algorithm Changeβ
Scenario: Switch from HS256 to RS256β
With separation:
// Before: HS256
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256) // β Change here
.compact();
}
// After: RS256
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getRsaPrivateKey(), SignatureAlgorithm.RS256) // β Only change
.compact();
}
Changed 1 line in 1 method! β
All 5 token types automatically updated! β
Without separation:
// Have to update HS256 β RS256 in:
generateToken() β
generateAdminToken() β
generatePasswordResetToken() β
generateApiToken() β
generateRefreshToken() β
5 methods to update, 5 places to make mistakes!
Summaryβ
The Questionβ
"Why do you think we need both
generateToken()(public) andcreateToken()(private) as separate methods instead of just one?"
The Answerβ
Because separation of concerns provides:
| Benefit | With Separation | Without Separation |
|---|---|---|
| Code reuse | β Token building logic once | β Repeated 5+ times |
| Extensibility | β Add new token types easily | β Duplicate entire logic |
| Maintainability | β Change algorithm in 1 place | β Update 5+ methods |
| Testing | β Test business logic separately | β Test everything together |
| Clarity | β Clear responsibilities | β Mixed concerns |
| Follows SOLID | β Single Responsibility | β Violates SRP |
| Follows DRY | β Don't Repeat Yourself | β Lots of duplication |
The Two Responsibilitiesβ
// Public method: WHAT (Business Logic)
public String generateAdminToken(String email, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", role); // β Decision: What to include
return createToken(claims, email);
}
// Private method: HOW (Technical Details)
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder() // β Implementation: How to build
.setClaims(claims)
.setSubject(subject)
// ... technical JWT building details
.compact();
}
Key Takeawaysβ
generateToken()handles business logic - What claims to addcreateToken()handles technical details - How to build JWT- Separation enables extensibility - Add new token types without duplication
- Follows Single Responsibility Principle - Each method has one reason to change
- Follows DRY principle - Don't Repeat Yourself
- Restaurant analogy - Assembly line (reusable) vs menu items (custom orders)
The Bottom Lineβ
One method = Simple now, painful later
Two methods = Slightly more complex now, easy to extend forever
This is good software design - thinking ahead about future requirements! π―
Now you understand that separating generateToken() and createToken() isn't just "fancy coding" - it's essential for maintainability and extensibility as your application grows! π‘
