perf(api): make relation inclusion opt-in on entity list endpoint (fixes #130) - #135
perf(api): make relation inclusion opt-in on entity list endpoint (fixes #130)#135mmornati wants to merge 2 commits into
Conversation
Code Coverage OverviewLanguages: Java Java / code-coverage/jacocoThe overall line coverage in commit 4f84a5f in the Show a line coverage summary of the most impacted files.
Updated |
|
About this: " feat(api)!: update entity output for having relations grouped (#90) caused every item returned by the list endpoint to eagerly resolve and serialize its full relation graph, not just its properties — previously this endpoint returned properties-only for list views. This is a measured regression, not a pre-existing gap, identified while benchmarking IDP v2 against the app-referential POC (see idp-v2-vs-app-referential-analysis.md, §12)." The Following #90, we achieve the same result by using the new centralized As a result, the performance difference compared to the previous version likely stems from migrating to the Graph service from the paginated getRelationsAsTarget method. The migration was a deliberate trade-off in favor of simplicity and code factorization. More importantly, it lays the groundwork for supporting configurable relation depths in paginated responses. Consequently, this PR introduces a flag to exclude relations from the response, avoiding unnecessary relations mapping after receiving By the way, for a change in the API contract, the PR would need to also update the |
brandPittCode
left a comment
There was a problem hiding this comment.
a change in the API contract needs to update the docs/src/static/swagger.yaml and the user documentation. `docs/src/concepts/entities.md
Fixes #130 feat(api)!: update entity output for having relations grouped (#90) caused GET /api/v1/entities/{templateIdentifier} to eagerly resolve and serialize the full relation graph for every item in a paginated/filtered list, regardless of whether the caller needed relations. This regressed list latency 1.5-2.3x and payload size +33% (see idp-v2-vs-app-referential-analysis.md, section 12). Add an include_relations query parameter (default false). The default path now returns properties only, skipping relation-graph resolution entirely via a new EntityDtoOutMapper#fromEntitiesPageToDtoPageWithoutRelations. include_relations=true preserves the previous eager-relations behavior exactly for callers that need it. Get-by-id and /search endpoints are unaffected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ties guide Adds the include_relations boolean parameter to the checked-in OpenAPI spec (docs/src/static/swagger.yaml) and to the entities concept guide (docs/src/concepts/entities.md), matching the generated /v3/api-docs/internal spec produced by the running app. Addresses PR review feedback requesting these two files be kept in sync with the API contract change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
a3eb4ba to
4f84a5f
Compare
|
Thanks for the correction — you're right, and I've updated the PR description to reflect it. For the record, the corrected causal explanation: the list endpoint already returned first-level relations before #90, split into Use case/context: this came out of benchmarking IDP v2 against the Also done: |
|



What
GET /api/v1/entities/{templateIdentifier}now accepts aninclude_relationsquery parameter, defaulting tofalse. The default path returns properties only, skipping relation-graph resolution entirely. Passinginclude_relations=truepreserves the exact previous (eager) behavior for callers that need per-item relations. Get-by-id (GET /api/v1/entities/{templateIdentifier}/{entityIdentifier}) andPOST /api/v1/entities/searchare unaffected by this PR.Closes #130.
Why
Correction from review: the list endpoint already returned first-level relations before #90 — split into two sections,
relations(outbound) andrelations_as_target(inbound), populated via a dedicated paginatedgetRelationsAsTargetlookup.feat(api)!: update entity output for having relations grouped (#90)didn't newly introduce relations to list responses; it replaced that mechanism with the sharedentityGraphService.getEntityGraphPageByTemplate(depth=1)graph-traversal service (the same one used by the dedicated graph endpoint), unifying the output into a singlerelationsmap. Functionally equivalent at depth 1, but with materially worse performance characteristics for the list use case — that's the actual regression, and the measured numbers below are unaffected by this correction, only the causal explanation is.This was found while benchmarking IDP v2 against the
app-referentialPOC for an AI/LLM-consumption use case: an agent enumerating/filtering entities by template only needs identifiers and properties for most of its calls, and fetching relations for every row of every page — regardless of whether the caller asked for them — showed up directly in that benchmark as the dominant list-latency and payload cost (seeidp-v2-vs-app-referential-analysis.md, §12).This is P0 of a 5-part prioritized performance plan derived from that analysis:
/search, mock-auth flag disabledP0 was picked first because it reverses a real regression (not a pre-existing architectural cost), is small/low-risk, and directly addresses the concurrency-ceiling root cause identified in the analysis's load testing (§13).
Evidence
Before (root cause) — from the original analysis, re-benchmarked 2026-08-17 after #90 landed
subdomain=ORDER(18 of 953 products)type=DECATHLON_API(1,042 of 3,972 components)Payload for a single entity + 145 relations: 29,555 B → 39,323 B (+33%) after #90.
After this PR — measured against the same live seeded dataset (~3,972 entities / 6,501 relationships, mirroring app-referential's real data), warm requests
type=DECATHLON_API, 200 rows)subdomain=ORDER)include_relations=truereproduces the exact previous 133 KB payload for the high-selectivity case — confirms this is a true opt-in, not a behavior change for existing consumers that explicitly need relations.Per-item relation overhead breakdown (why opt-in, not always-on)
Nearly every listed entity has some relation, so this isn't "rare data" being skipped — it's that unconditionally attaching it to every row in every page multiplies cost with result-set size regardless of whether the caller needs it. Most list/search consumers (filtered UIs, LLM-driven lookups) only need identifiers/properties and can fetch relations for a specific entity via the by-id endpoint when actually required.
Testing
EntityControllerTest(120 tests) — updated 3 relation-specific list assertions to passinclude_relations=trueto preserve their intent; all pass.EntityGraphServiceTest,EntityGraphControllerTest,EntityService*Test) — 200 tests, all pass.mainand adding the doc updates below (154 tests, all pass).Backward compatibility
This is additive: existing callers get a faster, smaller default response (properties only). Any caller that depends on relations being present in list responses must add
include_relations=true— flagging this as a contract change for API consumers/frontends that currently rely on relations being present by default. As requested in review, this PR also updates the API contract artifacts for the new parameter:docs/src/static/swagger.yaml— addedinclude_relations(boolean, defaultfalse) to theGET /api/v1/entities/{templateIdentifier}operation, verified against the app's own generated/v3/api-docs/internaloutput.docs/src/concepts/entities.md— documented the parameter and default/opt-in example response shapes under "List Entities by Template".