perf(core): cut ORM/REST serialization overhead for entity template and graph endpoints - #136
perf(core): cut ORM/REST serialization overhead for entity template and graph endpoints#136mmornati wants to merge 1 commit into
Conversation
…nd graph endpoints - Cache entity templates (Caffeine) in EntityTemplateService to avoid redundant DB round-trips + full-object rehydration/mapping on every request; cache is evicted on template create/update/delete. - Add a lightweight EntityIdentity projection (JPQL constructor expression) and use it in EntityGraphService.getEntityGraph for the graph root, instead of hydrating the full Entity aggregate when only identity fields (id, templateIdentifier, identifier, name) are needed to build the graph response. Closes #131 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
|
About the EntityTemplateService.getEntityTemplateByIdentifier caching: Caffeine is an in-memory, per-instance cache, which means it cannot guarantee consistency across a horizontally scaled, stateless deployment like we intend for IDP-Core. For example: Instance A caches template-a. An admin updates template-a through Instance B. Instance B evicts its local entry. Instance A continues serving the stale template-a until its TTL expires or the pod restarts. To support caching reliably, we would need to evaluate a distributed cache (e.g., Redis) or a messaging mechanism (e.g., Pub/Sub) to broadcast invalidation events. We also need to ensure the architecture remains compatible with dynamic platforms like Kubernetes and serverless managed services like Cloud Run. |
Agree. For the moment it is a single instance in the test I made (both for the KG and for Identity Access). As it is only a cache, in a Hexagonal way, we can propose both drivers (or even more). |



Why
Closes #131.
Profiling flagged that entity-template lookups and the entity-graph endpoint were spending far more time in ORM hydration / mapping than in the actual SQL, which was identified as the root cause of the concurrency ceiling described in #131 (10–30x the real SQL cost).
Two concrete hotspots were found and fixed:
EntityTemplateService.getEntityTemplateByIdentifierhad no caching. Templates are read-heavy and rarely change, but every call re-fetched and re-mapped the full entity/attribute graph from the DB.EntityGraphService.getEntityGraphhydrated the full rootEntityaggregate (all attributes, relations, audit fields, etc.) just to read 4 identity fields (id,templateIdentifier,identifier,name) needed to build the graph response.What changed
spring-boot-starter-cache+caffeine, and a newCacheConfiguration(infrastructure layer) defining theENTITY_TEMPLATES_CACHE.EntityTemplateServicewith@Cacheable/@CacheEvict/@Cachingso template reads are served from cache, with eviction wired into create/update/delete so callers never see stale data.EntityIdentityrecord plusfindIdentityByTemplateIdentifierAndIdentifieronEntityRepositoryPort/JpaEntityRepository(JPQL constructor expression selecting only identity columns) /PostgresEntityAdapter.EntityGraphService.getEntityGraphto fetch the graph root via the new lightweight identity query instead of the fullEntityhydration path.EntityGraphServiceTestaccordingly (24 stubs migrated to the new port method); no other tests required changes — everything else mocks at a layer this doesn't touch.No API/contract changes — this is a pure internal performance optimization. Scope intentionally kept to these two low-risk wins only (does not include the separate work from PR #135; this branch was built directly off
main).Test coverage
@SpringBootTestintegration classes fail locally for an unrelated, pre-existing environment reason (Testcontainers/Ryuk cannot access the Docker socket under Rancher Desktop on this machine — reproducible onmaintoo, unaffected by this change).Performance validation
Built two apps against the same already-seeded local Postgres (3,972 entities / 3,632 relations): one from clean
origin/main(baseline), one from this branch (template cache + lightweight root fetch). Ran identical k6 scripts against each, back-to-back.Get-by-id (
GET /entities/{template}/{id}), VU=10, 30sGraph traversal (
/graph?depth=N), VU=10, 15-30sZero request failures in any run (0%
http_req_failed).Caveat: shared, non-dedicated test machine
System load average swung from ~6 to ~15 during testing (confirmed by re-running the same baseline scenario twice and getting 269 req/s vs. 111 req/s purely from ambient load). So:
Bottom line: real, positive, low-risk improvement, most pronounced on the common case (shallow get-by-id) — no regressions, no test failures.
Additional check: real OAuth2/JWT validation overhead
Since all the above used the app's local mock-security bypass, we also validated request-level auth overhead by running the same suite against this branch with real OAuth2 resource-server JWT validation enabled (production JWKS, RS256), to make sure it doesn't mask or amplify the improvement in production-like conditions:
JWT signature verification adds a small, consistent ~2ms on cheap single-request calls, but is within noise once request cost grows or under concurrency — confirms this PR's gains hold up with real auth enabled, and that auth overhead isn't a factor for this optimization's scope.
🤖 Generated with the help of GitHub Copilot CLI.