Skip to content

0008. items_flat regeneration: kds-owned, triggered per-outlet at publish

  • Status: Accepted
  • Date: 2026-06-12
  • Deciders: POS Integration team

Context

items_flat is a denormalized, per-store read cache that the consumer storefront (store-service) reads — its V2 product query (GetProductByStoreIdV2) INNER JOINs it, and it carries the flattened categories, modifiers, tags, allergens, prices and image URL for each item so the storefront can serve a menu with a single SELECT. It is owned by kds-management-service (schema + the regeneration SQL in database/item.go).

Before this decision, items_flat was regenerated in two unrelated places:

  • kds-management-service regenerated it only as part of the Deliverect menu sync — so menus from other adapters (iSeller, Revel, Cata) never populated it.
  • pos-integration-service carried a local SQL port of the same denormalization and ran it after sync_products and inside the publish repo.

Two copies of the same complex INSERT…SELECT drift over time, and coverage was inconsistent across adapters. We needed a single owner and a single, reliable trigger. The repo already had the building blocks: a Cloud Tasks client with a goroutine fallback (pkg/cloudtasks), the internal-endpoint pattern (InternalExecuteJob), and kds had just exposed an H2H endpoint to regenerate items_flat (POST /v1/items-flat/regenerate, kds PR #503 / INTR-744).

Decision

kds-management-service is the single owner of items_flat regeneration, and pos-integration-service triggers it through one standardized gate: after a menu publish (draft → live), once per published outlet.

  • The publish flow (MenuDraftService.BatchPublishDrafts) calls ItemsFlatRegenService.Submit(tenant, outletUuid) for each outlet after the publish transaction commits — fire-and-forget; a regen failure never fails the publish (it is a read cache; the next publish repairs it).
  • In production it enqueues a Cloud Task on a dedicated, env-prefixed queue ({env}-items-flat-regen) whose target is the kds H2H endpoint; in local dev (no Cloud Tasks client) it falls back to a direct HTTP call in a goroutine.
  • The task calls POST {KDS_MANAGEMENT_URI}/v1/items-flat/regenerate. kds is per-tenant subdomain-routed, so KDS_MANAGEMENT_URI is a template (https://{subdomain}.…/service/kds); the {subdomain} is resolved from the tenant (central.domains, via ProviderRepository.GetTenantDomain).
  • Auth is the per-tenant KDS_API_KEY from tenantdb.settings, sent as the apikey header (+ x-tenant-id); kds's grpc-gateway maps these to gRPC metadata fwd-apikey/fwd-x-tenant-id and validates the key for the kds role against the middleware. No OIDC.
  • sync_products no longer regenerates items_flat; the local SQL port (MySQLProductRepository.RegenerateItemsFlat) is marked Deprecated and is no longer called. The catalog sync only updates items/item_store; the next publish projects the changes into items_flat.

Consequences

Positive

  • One source of truth for the denormalization SQL (kds) — no drift.
  • All adapters populate items_flat, not just Deliverect.
  • Publish is not blocked by the (heavy) regen; per-outlet tasks retry independently and are idempotent (full delete + reinsert per store).
  • Reuses the existing Cloud Tasks + goroutine-fallback pattern.

Negative / costs

  • A cross-service runtime dependency at publish (pos-integration → kds H2H), plus the auth (KDS_API_KEY with kds role per tenant) and a dedicated queue to provision per environment (Terraform).
  • items_flat is a cache: catalog edits made via sync_products are only reflected after the next publish, and there is a brief async window after publish before it is refreshed.
  • The H2H call is authenticated only at the app layer (apikey), not OIDC — see the broader internal-endpoint hardening tracked in INTR-777.

Alternatives considered

  • Keep the local SQL port and run it from pos-integration — rejected: two copies of the denormalization SQL drift, and kds (Deliverect) already owns one.
  • Regenerate on every sync_products — rejected: heavy, couples the cache to catalog sync rather than to what is actually published, and still needs the cross-service call for non-Deliverect adapters.
  • OIDC + Cloud Run IAM for the call — deferred: the per-tenant apikey is the established H2H mechanism; OIDC hardening is tracked separately (INTR-777).

References

  • PRs #129 (one-gate regen via kds H2H), #130 (resolve {subdomain}), #132/#133.
  • kds PR #503 / INTR-744 — the H2H RegenerateItemsFlat endpoint.
  • Code: internal/service/items_flat_regen_service.go, pkg/cloudtasks/client.go (EnqueueItemsFlatRegen), internal/repository/mysql_menu_draft_repository.go (publish trigger).
  • Related: ADR 0007 (image pipeline reuses the same Cloud Tasks pattern), ADR 0009 (the sibling publish-time projection into the V1 category model).