← Engineering Notes

Authentication & Backend Architecture

JWT + Redis Session Validation

How moving session validation from PostgreSQL to a Redis-backed cache reduced latency while preserving a database fallback and enabling shared authentication across backend services.

The problem

On MarketMate, authentication runs through SpringMate, a Spring Boot API. The client uses JWT-based authentication; the API verifies the token and also checks whether the server-side session for that login should still be accepted. That model was already in place and working.

Initially, sessions lived in PostgreSQL. SpringMate treated the database as the source of truth: on each request that needed session validation, the service checked session validity directly against PostgreSQL. That was functionally correct. The pain point was latency — repeatedly querying PostgreSQL on a frequently executed path added up.

The problem was not JWT itself, and it was not that PostgreSQL was wrong for storing sessions. The problem was where and how session validation happened on every request. JWT still answers whether the token is valid; the session layer answers whether that login should still be allowed. We needed that session check to stay correct while making the common validation path faster.

The original approach

Before Redis, the conceptual path for session validation through the auth backend looked like this:

PostgreSQL remained the authority: if the session record there said the login was valid, the request could proceed. The approach was straightforward, but session validation sat on a hot path, and every check paid the cost of a database read.

Why Redis entered the picture

The change was not to replace PostgreSQL or to make Redis the source of truth. Redis was introduced as a session cache — a fast layer that holds session state so the common validation path does not always hit the database.

On login, SpringMate still persists the session in PostgreSQL. The session is also written to Redis so later requests can validate against the cache first. PostgreSQL stays the persistent source of truth; Redis optimizes access to session state that has already been established there.

The new validation path

When a request needs session validation, the service checks Redis first. If the session is present in Redis and valid, validation can continue without querying PostgreSQL for that step. If the session is not in Redis, that is a cache miss — not an automatic rejection. The flow falls back to PostgreSQL, which still decides whether the session is valid. Only an invalid or missing session according to the source of truth should result in authentication failure.

Treating a Redis miss as “logged out” would confuse cache behavior with authority. Absence from Redis might mean eviction, expiry, or a cold cache — PostgreSQL is still where validity is decided on fallback. I am not documenting here whether a successful PostgreSQL read repopulates Redis; that is an implementation detail outside this note.

Redis as a shared authentication layer

MarketMate also runs a NestJS chat service that must authenticate users. SpringMate remains the authentication authority and owns the primary session model. PostgreSQL remains the persistent source of truth. Redis sits between them as a shared, fast session-validation layer that other services can read when validating a session.

Without that shared cache, validating a user in the chat service conceptually chains through the auth API and the database every time. With Redis holding session state that SpringMate already wrote, the chat service can validate against Redis on the common path instead of calling SpringMate for every authentication check.

Without Redis (conceptual)

NestJS Chat
SpringMate
PostgreSQL
Session validation

With Redis (common path)

NestJS Chat
Redis
Session validation

Redis does not replace SpringMate as the auth authority, and it does not eliminate PostgreSQL. It handles the common validation path. When the session is not available in Redis, the source-of-truth fallback remains available — the same PostgreSQL-backed model SpringMate already owned.

Redis isn’t just a cache

Redis was introduced because of a latency problem on the session-validation path. Working through it changed how I thought about the service layout, not only about read speed.

The same cached session state that reduced PostgreSQL lookups for SpringMate also let another backend service validate users without routing every auth decision through the main API. I started thinking about Redis less as “something that makes reads faster” and more as a layer for state that multiple services need to access quickly — while still respecting a real source of truth elsewhere.

Server-side session control still matters: logout and invalidation are about updating the authoritative session state and what the cache reflects, not about pretending a JWT expiry is the only lever. Redis did not replace that idea; it changed where the frequent read happens.

Trade-offs

PostgreSQL-only session validation

  • Simple source of truth
  • No cache consistency concerns
  • Fewer moving parts

The cost is a database query on the validation path, higher latency for repeated checks, and more load on the primary database when every request validates there.

Redis + PostgreSQL

  • Fast common-path validation
  • Lower pressure on PostgreSQL for session reads
  • Shared session validation across services
  • Source-of-truth fallback when Redis misses

The cost is additional infrastructure, explicit cache-miss handling, thinking about consistency between Redis and PostgreSQL, and more moving parts overall.

Neither layout is universally better. The right choice depends on how often session validation runs, how many services need it, and how much complexity you are willing to carry for a faster common path.

What I took away

The interesting part was not simply “add Redis.” It was noticing that PostgreSQL could answer session validation correctly but was doing that job on almost every request, and that another service would soon need the same read pattern. Redis could handle the common path; PostgreSQL could stay the source of truth; the same cached session state could serve SpringMate and the NestJS chat service on the paths that mattered most.

Authentication is still two questions: is the token valid, and should this session still be accepted? JWT handled verification; the session layer needed a data path that matched how often it ran. I am still learning how to balance cache behavior, authority, and service boundaries — but this project left me designing validation flows around common path and fallback instead of assuming every check must hit the database first.