Sep 18,2026
Why I Stopped Optimizing Early
A reflection on premature optimization, the projects it slowed down, and what I do differently now.
Software Engineering
Productivity
Published September 27, 2026
JWT is popular, but is it always the right choice? My experience choosing database-backed opaque tokens for a small Spring Boot monolith.
JWT is everywhere.
When building REST APIs, especially with Spring Boot, it's common to find tutorials and projects using JWT for authentication. It's popular, well-documented, and supported by many libraries.
But while working on my own Spring Boot monolith, I started asking myself a simple question:
Do I really need JWT for this project?
I noticed that many small applications use JWT without necessarily needing its main advantage: stateless authentication.
Some implementations also store tokens in localStorage, introduce access and refresh tokens, and add extra complexity to handle token expiration and revocation.
I wanted to understand whether all of that was necessary for my application.
This article is not about declaring JWT bad or opaque tokens universally better. It's about understanding the trade-offs and choosing a solution that fits the architecture.
JWT (JSON Web Token) is a compact format for transmitting claims between parties. A typical JWT contains a header, a payload, and a signature.
For example, a payload might look like this:
{
"sub": "123",
"email": "user@example.com",
"role": "USER",
"iat": 1790500000,
"exp": 1790503600
}The server can validate the token's signature and expiration without querying the database for every request.
That's one of JWT's main advantages.
However, a signed JWT is not necessarily encrypted. Its payload can generally be decoded by anyone who has the token. Sensitive information should not be placed in it unless encryption is deliberately used.
JWT also introduces some questions:
These are not reasons to avoid JWT. They are design decisions that should be made deliberately.
While exploring authentication implementations, I noticed a recurring pattern:
localStorage.This architecture can be appropriate, especially for applications with multiple clients or distributed services.
But for a small monolithic API, I started wondering whether I was introducing complexity before I had a real need for it.
For example, if I store refresh tokens in the database, I already have a database lookup in the authentication flow. If I also maintain a token blacklist for access tokens, I lose some of JWT's statelessness.
There is nothing inherently wrong with those approaches. The important thing is to understand what problem each additional component solves.
Stateless authentication is an architectural property, not a requirement for every REST API.
An opaque token is a random string that contains no meaningful user information.
Unlike JWT, it doesn't contain claims that the client or server can decode.
For example:
Authorization: Bearer 7jF...random-token...xQThe server uses the token to find the associated authentication record.
A simple database table could look like this:
| id | user_id | token_hash | expires_at | last_used_at |
|---|---|---|---|---|
| 1 | 42 | a9f... | 2026-09-28 | 2026-09-27 |
| 2 | 15 | c21... | 2026-09-30 | NULL |
The client receives the original random token, while the database stores only its hash.
When a request arrives, the server:
This is a straightforward approach for a monolithic application.
For my Spring Boot project, I wanted a simple authentication system with login, registration, logout, and a /auth/me endpoint.
I didn't need several independent services to validate tokens without sharing a database.
My application already had PostgreSQL, Spring Data JPA, and Spring Security.
That made database-backed opaque tokens a reasonable choice for my current requirements.
With opaque tokens, revocation is simple.
When the user logs out, I delete the corresponding token record.
The next request using that token fails because the server can no longer find it.
@Transactional
public void revokeToken(String plainToken) {
tokenRepository.deleteByTokenHash(
hashToken(plainToken)
);
}There is no need to maintain a separate JWT blacklist.
Since tokens are stored in the database, I can manage active sessions.
For example, I can:
This is useful when building an application that needs basic session management.
With an opaque token, logout is conceptually simple: revoke the current token.
The server doesn't need to wait for a JWT to expire or maintain a separate denylist to invalidate it.
Of course, this simplicity comes with a trade-off: validating an opaque token requires a database lookup or another shared token store.
Opaque tokens are not free.
For each authenticated request, the application needs to validate the token against its storage.
In a simple implementation, that means querying PostgreSQL.
Optional<PersonalAccessToken> findByTokenHashAndExpiresAtAfter(
String tokenHash,
Instant now
);This introduces database work into the authentication flow.
For a small monolith, this may be entirely acceptable. For a high-traffic application, the cost should be measured rather than assumed.
Caching can reduce database traffic, but it also introduces invalidation concerns. For example, a cached token might remain usable briefly after logout unless revocation is propagated or the cache is invalidated.
The right choice depends on the application's traffic, infrastructure, and security requirements.
Here is a simplified comparison:
| Feature | JWT | Opaque Token |
|---|---|---|
| Token format | Structured and signed | Random string |
| Contains claims | Yes | No |
| Validation | Signature and claims | Storage lookup |
| Database required for each validation | Not necessarily | Usually |
| Immediate revocation | Requires additional design | Straightforward with shared storage |
| Session management | Requires additional design | Naturally supported by token records |
| Stateless validation | Yes, when no lookup is needed | No |
| Best fit | Systems benefiting from distributed validation | Systems needing centralized session control |
Neither approach is universally better.
JWT is useful when independent services need to validate tokens without querying a central session store.
Opaque tokens are useful when centralized session management and straightforward revocation are important.
This is where JWT can become particularly useful.
Imagine an architecture with several independent services:
If every service needs to query the same database to validate an opaque token, authentication becomes dependent on a shared service or storage system.
With JWT, a service can validate the signature and relevant claims locally, provided it has the necessary verification key and trusts the issuer.
This can reduce authentication-related network calls.
However, JWT does not automatically solve every microservices authentication problem. Services still need to handle authorization, key rotation, token expiration, and revocation requirements.
For some distributed architectures, opaque tokens combined with a central authorization server and token introspection are also a valid solution.
The architecture should determine the choice, not the assumption that microservices always require JWT.
Another decision I questioned was storing authentication tokens in localStorage.
Although it is convenient, JavaScript running on the same origin can access values stored there. An XSS vulnerability can therefore expose those tokens.
Using an HttpOnly cookie can prevent JavaScript from reading the cookie, although cookie-based authentication also requires careful CSRF protection and appropriate cookie settings.
There is no universal storage solution for every frontend architecture.
For my API, I focused first on implementing token validation and revocation correctly, rather than assuming that storing a JWT in localStorage was the default answer.
My implementation uses:
The login flow is simple:
For subsequent requests, the client sends:
Authorization: Bearer <opaque-token>Spring Security validates the token and establishes the authentication context.
The /auth/me endpoint can then return the authenticated user's information.
Logout revokes the token in the database.
One important implementation detail is to update last_used_at when a token is successfully used. For high-traffic systems, this update may need to be throttled to avoid writing to the database on every request.
For a small monolithic application, I would first ask:
If I already have PostgreSQL and need centralized session management, opaque tokens are a practical option.
If I need distributed validation and can accept the associated expiration and revocation model, JWT may be a better fit.
I would not introduce refresh tokens, a token blacklist, and additional infrastructure just because a tutorial uses them.
But I also wouldn't reject JWT simply because a database-backed solution works for my current project.
Working on authentication made me realize that choosing a technology is not just about following common practices.
JWT is a useful standard, but it is not automatically the simplest solution for every API.
Opaque tokens provide a different set of trade-offs: database-backed validation, straightforward revocation, and centralized session management.
For my current Spring Boot monolith, I chose opaque tokens because they fit the requirements and the infrastructure I already had.
The most important lesson for me is simple:
Choose an authentication strategy based on your architecture and requirements, not just its popularity.