Architecture

Service topology

Four containers, one Docker Compose file, no message broker. Requests fan out from the browser to the API and to Keycloak directly for token issuance.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Browser    │───────▢│  React frontend    β”‚  :3000
β”‚              β”‚        β”‚  (nginx-served)     β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚  Bearer token                     β”‚  REST calls
       β–Ό                                   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Keycloak     │◀──────▢│  Open Liberty     β”‚  :9080
β”‚  (realm:      β”‚  JWT   β”‚  backend           β”‚
β”‚  shelfinity)  β”‚ verify β”‚  (Jakarta EE 10)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  keys  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     :8080                        β”‚  JPA / EclipseLink
                                   β–Ό
                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                          β”‚   PostgreSQL       β”‚  :5432
                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The frontend never talks to Postgres or issues its own tokens β€” it exchanges credentials with Keycloak’s token endpoint directly (Resource Owner Password Credentials grant against the public shelfinity-frontend client) and sends the resulting JWT as a bearer token on every backend call. The backend validates that JWT against Keycloak’s realm keys via MicroProfile JWT; it never sees a password.

Technology choices

Layer Technology Why
Backend runtime Open Liberty 24, Jakarta EE 10 (JAX-RS, JPA/EclipseLink, CDI) Portable Jakarta EE app; also deployable on Payara without code changes
Backend auth MicroProfile JWT against Keycloak-issued tokens No credential storage in the app; RBAC comes straight from Keycloak realm roles
Database PostgreSQL 15 One relational store for users, books, the approval queue, reservations, and email config
Async work CDI @Asynchronous + scheduled jobs, in-process No message broker needed at this scale β€” see the specification’s decision log for when that would change
Frontend React 18, MUI, React Router Component-driven UI with real client-side routing for the growing set of admin views
Identity Keycloak (realm shelfinity) Full OIDC provider: registration, login, password reset, and role management, instead of reimplementing any of that

Domain model

The approval queue is the one modeling decision worth calling out: registration, borrowing, and returns share a single QueueItem table, reviewed through the same PENDING β†’ APPROVED/REJECTED workflow, because they’re the same admin work pattern β€” review, decide, notify. Reservations are not a queue item type in practice, despite a legacy enum value suggesting otherwise: their lifecycle (auto-expiry, fulfillment, promotion to the next person in line) doesn’t fit a binary approve/reject shape, so they’re modeled as their own resource.

Entity Table Role
User users Profile cache keyed by Keycloak subject β€” no password column
Book books Catalog entry with derived availability (available && availableCopies > 0)
QueueItem queue_items The one approval queue: registration, borrow, and return requests
Reservation reservations Hold on an unavailable book, with expiry and fulfillment tracking
EmailConfig email_config Admin-managed SMTP settings; password AES/GCM-encrypted at rest

See Business Rules for the state machines each of these drives, and the repository’s docs/api/SPEC.md for the exact field list.

Testing strategy

Three backend tiers plus two frontend tiers, chosen to get real coverage without needing a full application-server-in-Maven integration harness: