Multi-tenant Spring Boot microservices

Keycloak OIDC, Kafka, Postgres per service, Redis — running live on this page. Built around three defects this architecture produces by default, each reproduced, measured and fixed.

Run the N+1 comparison, live

This fetches a real token from Keycloak, seeds orders into tenant-a, then requests the same 20-row page three ways. The endpoint reports its own cost through Hibernate statistics — these numbers are measured on this server, now.

1 · The N+1 that survives its own fix

Adding join fetch to a Pageable query is the fix most people ship. It drops the statement count — and quietly loads the entire table to return twenty rows, because Hibernate cannot apply LIMIT to a joined collection. Paging the ids first is flat at any size.

2 · Cross-tenant reads

Tenancy is a Hibernate @TenantId discriminator resolved from the validated JWT, never a method parameter. Hand-written JPQL is filtered too, and a thread with no tenant bound reads nothing rather than everything.

3 · Cross-tenant cache keys

Redis sits outside Hibernate, so @TenantId cannot protect it. The tenant is part of the cache key, and the read shares one key expression with its eviction — otherwise the evict deletes a key that was never written.

Explore it yourself

  1. Open the Swagger UI and press Authorize.
  2. Sign in as alice / alice (tenant-a) or bob / bob (tenant-b).
  3. Call GET /api/orders as each. Same tables, same query — neither sees the other's rows.
  4. readonly / readonly holds orders:read only, so POST /api/orders returns 403.

Placing an order publishes to Kafka, inventory reserves stock and replies, and the order moves to RESERVED — usually within a second.

Shared demo, so requests are rate limited and the seed endpoint is capped. The admin console is disabled. For the unthrottled version, clone the repo and run make up && make demo.

Source, architecture notes and the full test suite: github.com/YuvarajAravindan-AI-Agent/spring-boot-keycloak-kafka-multitenant