Home
Contact
Blog
Playground
BlogThoughtsWhy I Chose Opaque Tokens Over JWT for My Spring Boot API

Published September 27, 2026

Why I Chose Opaque Tokens Over JWT for My Spring Boot API

JWT is popular, but is it always the right choice? My experience choosing database-backed opaque tokens for a small Spring Boot monolith.

ZB
Ziane BadreddineAuthor

Why I Chose Opaque Tokens Over JWT for My Spring Boot API

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.

1. JWT Is Popular, but Popularity Isn't a Requirement

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:

  • How long should an access token remain valid?
  • Do I need refresh tokens?
  • How do I revoke a token before it expires?
  • What happens when a user's role changes?
  • Where should the client store the token?
  • Do I actually need stateless authentication?

These are not reasons to avoid JWT. They are design decisions that should be made deliberately.

2. The Problem With Adding Complexity Too Early

While exploring authentication implementations, I noticed a recurring pattern:

  1. Generate a JWT after login.
  2. Store it in localStorage.
  3. Add an access token and a refresh token.
  4. Build a refresh endpoint.
  5. Add token expiration and renewal logic.
  6. Find a way to revoke tokens.

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.

3. What About Opaque Tokens?

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...xQ

The server uses the token to find the associated authentication record.

A simple database table could look like this:

iduser_idtoken_hashexpires_atlast_used_at
142a9f...2026-09-282026-09-27
215c21...2026-09-30NULL

The client receives the original random token, while the database stores only its hash.

When a request arrives, the server:

  1. Reads the Bearer token from the Authorization header.
  2. Hashes the received token.
  3. Searches for the matching hash in the database.
  4. Checks whether the token has expired.
  5. Loads the associated user.
  6. Builds the authenticated security context.

This is a straightforward approach for a monolithic application.

4. Why I Chose Opaque Tokens for My Monolith

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.

Straightforward revocation

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.

Session management

Since tokens are stored in the database, I can manage active sessions.

For example, I can:

  • List a user's active sessions.
  • Revoke a specific session.
  • Revoke all sessions when needed.
  • Track when a token was last used.
  • Set an expiration date for each token.

This is useful when building an application that needs basic session management.

Simpler logout

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.

5. The Trade-off: Database Lookups

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.

6. JWT vs. Opaque Tokens

Here is a simplified comparison:

FeatureJWTOpaque Token
Token formatStructured and signedRandom string
Contains claimsYesNo
ValidationSignature and claimsStorage lookup
Database required for each validationNot necessarilyUsually
Immediate revocationRequires additional designStraightforward with shared storage
Session managementRequires additional designNaturally supported by token records
Stateless validationYes, when no lookup is neededNo
Best fitSystems benefiting from distributed validationSystems 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.

7. What About Microservices?

This is where JWT can become particularly useful.

Imagine an architecture with several independent services:

  • API Gateway
  • User Service
  • Order Service
  • Payment Service

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.

8. What About localStorage?

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.

9. My Implementation in Spring Boot

My implementation uses:

  • Spring Security for authentication and authorization.
  • PostgreSQL to store token hashes.
  • Spring Data JPA for persistence.
  • BCrypt to hash user passwords.
  • SecureRandom to generate opaque tokens.
  • SHA-256 to hash tokens before storing them.
  • Lombok to reduce boilerplate.
  • MapStruct to map entities to response DTOs.

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.

10. What I Would Choose for a Small Project

For a small monolithic application, I would first ask:

  • Do I need independent services to validate tokens?
  • Do I need immediate token revocation?
  • Do I need to manage multiple active sessions?
  • Is there a real requirement for stateless authentication?
  • What infrastructure do I already have?

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.

Conclusion

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.

On this page

Why I Chose Opaque Tokens Over JWT for My Spring Boot API1. JWT Is Popular, but Popularity Isn't a Requirement2. The Problem With Adding Complexity Too Early3. What About Opaque Tokens?4. Why I Chose Opaque Tokens for My MonolithStraightforward revocationSession managementSimpler logout5. The Trade-off: Database Lookups6. JWT vs. Opaque Tokens7. What About Microservices?8. What About localStorage?9. My Implementation in Spring Boot10. What I Would Choose for a Small ProjectConclusion

Related Blogs

Sep 18,2026

Thoughts

Why I Stopped Optimizing Early

A reflection on premature optimization, the projects it slowed down, and what I do differently now.

Software Engineering

Productivity

Navigation

  • Home
  • About
  • Projects
  • Contact

Explore

  • Experience
  • Education
  • Activities

Blog

  • Why I Chose Opaque Tokens Over JWT for My Spring Boot API
  • Java Interview Questions & Answers — My Prep Notes
  • TanStack Query — The Complete Crash Course
  • Motion (ex Framer Motion): Animations, UX, and Performance
  • Why I Stopped Optimizing Early
  • Is Bun Ready for Production in 2026?
  • React vs Vue in 2026 — An Honest Comparison
  • All posts

Language

Connect

GitHub
LinkedIn
Email

Ziane Badr Eddine.