At some point, a backend outgrows its initial assumptions.
In my case, that assumption was simple:
email = user identity
It worked fine early on. It was convenient, readable, and easy to query. But over time, it started to create real limitations—both in terms of features and security.
This post is about why I moved away from using email as the primary identifier, what had to change, and what improved as a result.
The Problem with Using Email as Identity
Initially, email was used everywhere:
- in JWT subject
- in database queries
- across service layer logic
Typical example:
userRepository.findByEmail(email)
And in JWT:
.setSubject(user.getEmail())
This approach has one fundamental flaw:
Email is not a stable identifier.
The moment you want to support changing email, things start to break:
- JWT tokens become inconsistent
- existing sessions may become invalid
- references across the system lose integrity
In practice, it blocks you from implementing a very basic feature: updating user email.
Why This Matters
Using a mutable field as identity introduces a few issues:
1. Tight coupling
Your entire system depends on a value that can change.
2. Inconsistent authentication
JWT subject no longer represents a stable user reference.
3. Limited extensibility
Features like:
- email change
- social login
- account linking become harder to implement cleanly
The Refactor: Switching to User ID
The core idea was straightforward:
Use
userIdas the single source of identity across the system.
Email is now just a property of the user—not something the system relies on for identification.
Key Changes
1. JWT now stores user ID
.setSubject(user.getId().toString())
.claim("userId", user.getId())
This ensures:
- the identifier is stable
- it maps directly to the database
- it doesn’t change over time
2. Extracting user ID from token
public Long extractUserId(String token) {
Claims claims = extractAllClaims(token);
Object userIdClaim = claims.get("userId");
if (userIdClaim instanceof Number number) {
return number.longValue();
}
if (userIdClaim instanceof String userIdText) {
return Long.parseLong(userIdText);
}
return Long.parseLong(claims.getSubject());
}
This handles multiple formats and keeps backward compatibility with older tokens.
3. Authentication filter now works with ID
Before:
findByEmail(...)
After:
Long userId = jwtService.extractUserId(jwt);
User user = userRepository.findById(userId).orElse(null);
This removes ambiguity and avoids relying on user-controlled fields.
4. CustomUserDetails updated
private final Long id;
private final String email;
The important part is that ID is now always available in the security context.
5. Centralized access via SecurityUtils
public static Long getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new IllegalStateException("No authenticated user in security context");
}
Object principal = authentication.getPrincipal();
if (principal instanceof CustomUserDetails userDetails) {
return userDetails.getId();
}
if (principal instanceof String principalValue) {
return Long.parseLong(principalValue);
}
throw new IllegalStateException("Unsupported principal type");
}
This becomes the standard way to access the current user across the application.
What Improved
More predictable authentication
User identity is now:
- immutable
- consistent across requests
- independent of user input
Better security model
The system no longer relies on fields that users can change.
Every request resolves to a database ID, which must exist.
Easier feature development
Adding support for:
- email change
- multiple authentication methods
- external providers
is now straightforward.
What Was Costly
This wasn’t a small refactor.
It required:
- updating JWT generation and parsing
- rewriting authentication filter logic
- replacing
findByEmailacross the codebase - adjusting services and flows that depended on email
It’s the kind of change that touches a lot of code and needs to be done consistently.
Things to Watch Out For
If you’re considering a similar change:
Old tokens
Tokens using email may stop working. You may need to force re-authentication.
Partial refactors
Mixing email-based and ID-based logic will lead to subtle bugs.
Tests
Without proper test coverage, it’s easy to introduce regressions in authentication flow.
Thanks for reading!
If you'd like to see Finovara development, check out my GitHub!
M4rc1nek
/
finovara-backend
Backend service for a personal finance management application
Finovara
Finovara is a financial management platform designed to help users effectively track analyze, and optimize their income, expenses, and savings The application provides a secure, bank-like experience focused on transparency, financial awareness, and long-term money planning.
🎯 Purpose of the Application
Finovara aims to support users in making better financial decisions by offering clear insights into their financial activity and helping them maintain control over their budgets and savings.
The platform focuses on:
- organizing income and expenses in a structured way
- visualizing financial data through charts and statistics
- supporting saving goals and spending limits
- providing a virtual wallet concept for daily financial management
🚀 Key Features
- Secure user authentication and authorization
- Income and expense tracking
- Categorization of financial operations
- Interactive charts and financial statistics
- Reports summarizing spending and income trends
- Virtual wallet management
- Savings goals (e.g. piggy banks)
- Spending limits and budget control
- Scalable architecture prepared for future financial…
Top comments (0)