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
genericMessagesstill receivenavigation,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()
...

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 (
GenericMessageServiceis 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
GenericMessageServiceanduseGenericMessageare the reference implementation for this pattern.- The shared types
PartialLocalizedConfigGetResponse<Field>andLocalizedConfigUpdateResponse<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.