ADR-025: Per-Section GraphQL Queries for Localized Config

Prev Next

Context

LocalizedConfigType is a composite object containing seven independently-managed sections:

Field Typical Size
headers moderate
navigation large
onboardingNavigation small
splashConfig large
genericMessages very large
navigationConfig small
checkoutOffers moderate

A single localizedConfigGet query returning the full object creates several problems:

  • Payload bloat — modules that only need genericMessages still receive navigation, splashConfig, and all other sections.
  • Stale data risk — if a user edits Generic Messages, navigates to Splash Config, the splash data was fetched minutes ago alongside the generic messages. Any concurrent edits by other users are invisible until a full refresh.
  • Mutation coupling — a single monolithic update mutation encourages sending the entire object back, increasing conflict surface.

Since the API is GraphQL, the protocol already supports requesting only the fields a client needs.

Decision

Each section of LocalizedConfigType gets its own dedicated query and mutation, scoped to a single top-level field.

Each section also gets its own Service class, hook, and schema — following the existing module boundary pattern.

localizedConfigGet { genericMessages } → GenericMessageService.get()
localizedConfigGet { splashConfig }    → SplashConfigService.get()
localizedConfigGet { navigation }      → NavigationService.get()
...

Image

The client selects a single field from localizedConfigGet; GraphQL resolves only that projection, which is then consumed by a dedicated service and hook.

The shared response and request types (PartialLocalizedConfigGetResponse<Field>, LocalizedConfigUpdateResponse<Field>) already support this pattern via generic field selection — no type changes required.

Mutations follow the same split: each service sends only its own section inside localizedConfig.

// GenericMessageService.save sends:
{ localizedConfig: { genericMessages: values }, site, languageCode }

// SplashConfigService.save sends:
{ localizedConfig: { splashConfig: values }, site, languageCode }

:::danger Invariant
Each query and mutation must target exactly one top-level field. Combining multiple sections in a single operation is disallowed to preserve isolation guarantees.
:::

Consequences

Enables

  • Each module fetches only the data it needs — significantly smaller payloads
  • Navigating between sections always gets the latest data for that section
  • Independent save operations reduce merge-conflict surface on concurrent edits
  • Each service/hook pair is independently testable with focused mocks
  • Matches the existing module boundary pattern (GenericMessageService is the first implementation)

Restricts

  • No single call to load the entire config — any future "export full config" feature would need to fan out across services or add a dedicated bulk query
  • Each new section requires its own query file, mutation file, service, and hook (mild boilerplate, offset by clarity)

Trade-offs accepted

  • Slightly more GraphQL document files in the codebase (7 queries + 7 mutations vs. 1+1)
  • If multiple sections need to load simultaneously, the client makes parallel requests instead of one — acceptable given individual payload reduction

Notes

  • GenericMessageService and useGenericMessage are the reference implementation for this pattern.
  • The shared types PartialLocalizedConfigGetResponse<Field> and LocalizedConfigUpdateResponse<Field> were designed for this — no new type plumbing needed.
  • Remaining sections (splash, navigation, headers, etc.) should follow the same service/hook/schema structure as they are built out.