-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Technical overview of the Customer Management service: the runtime stack, how a request flows through the filter chain, the main internal flows (SPA serving, auth, rate limiting), the data layer, and the code layout.
| Layer | Tech |
|---|---|
| HTTP server | Reactor Netty (non-blocking) |
| Framework | Spring Boot 3.5, Spring WebFlux (reactive) + Kotlin coroutines (suspend handlers) |
| Security | Spring Security resource server — HS256 JWT (Nimbus), BCrypt |
| Persistence | Spring Data R2DBC (reactive) on in-memory H2 |
| Migrations | Flyway (runs over JDBC, same H2) |
| Rate limiting | Token bucket — in-memory or Redis |
| API docs | springdoc-openapi + Swagger UI |
| Frontend | React 19 + TypeScript + MUI, bundled into the jar and served same-origin |
Everything — API and the SPA — is one Spring Boot app, one container, one origin.
flowchart TD
C["Browser / API client"] -->|HTTPS| CR["Cloud Run (TLS termination)"]
CR -->|"HTTP + X-Forwarded-*"| N["Reactor Netty"]
N --> FH["ForwardedHeaderTransformer (pre-filter)"]
FH --> SPA["SpaWebFilter"]
SPA --> RL["RateLimitWebFilter"]
RL --> SEC["Spring Security (CORS + JWT)"]
SEC --> HM{"Handler mapping"}
HM -->|"/api/**"| CT["Controllers -> Services -> R2DBC -> H2"]
HM -->|"SPA routes"| RS["Static handler -> index.html"]
HM -->|"/swagger-ui, /v3/api-docs"| DOC["springdoc / Swagger UI"]
ForwardedHeaderTransformer runs at the framework level before the WebFilter chain; the rest are WebFilters run in @Order.
| # | Filter | Order | Purpose |
|---|---|---|---|
| 0 | ForwardedHeaderTransformer |
pre-filter (framework) | Applies X-Forwarded-Proto/Host so generated URLs use the external https host. Deployed profiles only (server.forward-headers-strategy=framework). |
| 1 | SpaWebFilter |
HIGHEST_PRECEDENCE |
Forwards SPA navigation routes to /index.html (see below). |
| 2 | RateLimitWebFilter |
-110 |
Per-IP token bucket; just ahead of security so abusive traffic is dropped early. |
| 3 | Spring Security (WebFilterChainProxy) |
-100 |
CORS, then JWT bearer validation. |
| 4 | Handler mapping | — | Controllers, the static resource handler (SPA), or springdoc. |
The React SPA is built and bundled into the jar under static/, so the backend serves both UI and API from one origin (no CORS between them). SpaWebFilter decides how each GET is handled:
flowchart TD
G["GET request"] --> Q{"reserved prefix?<br/>/api /actuator /swagger /v3/ /webjars"}
Q -->|yes| PASS["pass through (API / docs)"]
Q -->|no| EXT{"path has a file extension?"}
EXT -->|"yes (e.g. /assets/app.js)"| STATIC["served as a static file"]
EXT -->|"no (e.g. /customers)"| REWRITE["rewrite path to /index.html -> SPA shell"]
So a refresh on /customers returns index.html and React Router renders the page; /assets/*.js is served as-is; /api/* reaches the API untouched. Because ES module scripts are fetched in CORS mode (they send an Origin even same-origin), deployed profiles enable forward-headers-strategy and add the service's own origin to the CORS allow-list so those requests aren't rejected.
sequenceDiagram
participant C as Client
participant A as AuthController / AuthService
participant J as JwtService
participant R as Resource server (JwtDecoder)
C->>A: POST /api/auth/login {username, password}
A->>A: load user, verify BCrypt hash (off the event loop)
A->>J: mint HS256 JWT (sub, roles, iss, exp)
J-->>C: { accessToken, tokenType, expiresIn, role }
Note over C,R: every subsequent request
C->>R: GET /api/me (Authorization: Bearer <token>)
R->>R: verify signature + issuer + expiry; map "roles" claim to ROLE_*
R-->>C: 200 (or 401 missing/invalid token, 403 wrong role)
- Login is the only entry point (no registration); accounts are seeded by Flyway.
- The token carries
sub(username) androles; the resource server mapsroles→ROLE_USER/ROLE_ADMIN. - Per-endpoint roles are enforced with method-level
@PreAuthorizeon the controllers (ADMIN writes; USER + ADMIN read).
-
Key =
auth:orgeneral:+ client IP (remoteAddress, or the firstX-Forwarded-Forentry whenratelimit.trust-forwarded-for=true). -
Tier:
/api/auth/**→ strict (low capacity); everything else → general. Docs/health are excluded. -
RateLimiter.tryConsume(key, capacity, refillPeriod)against a token bucket — in-memory (local) or Redis (deployed). Returns429+Retry-Afterwhen empty, addsX-RateLimit-Remainingto every response, and fails open if the backend is unavailable.
- The app reads/writes via R2DBC (reactive) on in-memory H2.
-
Flyway owns the schema + seed and runs over JDBC against the same H2 instance (shared DB name +
DB_CLOSE_DELAY=-1keeps it alive for the JVM). Migrations:db/migration/V1__init.sql(tables + audit trigger) andV2__seed_users.sql. -
Auditing: Spring Data R2DBC auditing stamps
created_by/last_modified_by/ timestamps; an H2 trigger (CustomerAuditTrigger) writes every customer update/delete intoaudit_log.
src/main/kotlin/com/allica/customermanagement/
├── CustomermanagementApplication.kt # Spring Boot entry point
├── auth/ # login + self-service password change
│ └── AuthController, AuthService, AuthDtos
├── security/ # security wiring
│ ├── SecurityConfig # filter chain, CORS, JWT decoder, method security
│ ├── JwtService # mints HS256 tokens
│ ├── JwtProperties # secret / issuer / expiry
│ └── AppUserDetailsService # loads users by username
├── user/ # AppUser entity + AppUserRepository
├── customer/ # Customer entity, repository, service, controller, DTOs
├── audit/ # AuditLog entity/repo + CustomerAuditTrigger (H2 trigger)
├── ratelimit/ # RateLimiter (interface) + InMemory/Redis impls,
│ # RateLimitWebFilter, RateLimitProperties, TokenBucket
├── web/ # MeController (/api/me), AdminController (/api/admin)
└── config/ # OpenApiConfig, SpaWebFilter, AutoAuthSwaggerIndexTransformer, AuditingConfig
frontend/src/
├── api/ # axios client + auth/customers calls
├── auth/ # AuthContext (token in localStorage)
├── components/ # Layout, ProtectedRoute, ConfirmDialog, AllicaLogo
├── pages/ # Login, Customers, CustomerFormDialog, Profile
└── App.tsx, main.tsx, theme.ts, types.ts
| Component | Responsibility |
|---|---|
AuthService |
Verify credentials, mint tokens (via JwtService), change password |
JwtService |
Build and sign HS256 JWTs |
AppUserDetailsService |
Load AppUser by username |
CustomerService |
Customer CRUD + search/pagination/sort; 404 / 409 handling |
RateLimiter (InMemory / Redis) |
Token-bucket consume for a key |
RateLimitWebFilter |
Tier selection, client-IP derivation, 429 + headers |
SpaWebFilter |
Route SPA navigations to index.html
|
AutoAuthSwaggerIndexTransformer |
Inject the Swagger "auto-apply token after login" snippet |
CustomerAuditTrigger |
H2 trigger writing audit_log on update/delete |
AuditingConfig |
Enable R2DBC auditing (@CreatedBy / @LastModifiedBy) |
One container (multi-stage Docker: frontend build → jar → JRE) runs on Cloud Run (scale-to-zero). Spring profiles select the rate-limit backend (in-memory locally, Redis in deployed envs) and the forwarded-headers behavior. See Setup to run it, and terraform/README.md for the Cloud Run + CI/CD setup.
Getting started
How it works
Operations
Reference