Side-partner architecture

This page covers the runtime architecture of the wes-{partner}-adapter — the per-partner outbound translator FG.AI ships under Path C. It complements the overview: the overview describes Path C as an integration topology; this page describes what the actual adapter looks like, how it is deployed, and what guarantees it provides.

For onboarding and operational concerns, see side-partner-onboarding and side-partner-operations.

Where the adapter sits

Partner WMS                          FG.AI WMS                   wes-{partner}-adapter
─────────────                        ─────────                   (per-(partner, tenant)
                                                                  Helm release)
                                       ┌──────────────────┐
       ▲                               │ pim/v1/skus      │
       │                               │ pim/v1/locations │ ◄─────┐ Partner pushes
  Partner outbound                     │ wms/v1/work-…    │       │ master + WO.
  endpoints                            │ wms-partners/v1/ │       │ UAT bridge: JWT
       ▲                               │   inventory-delta│       │ for everything.
       │                               │   authenticate   │ ◄─────┘ Post-retirement:
       │                               │                  │         mTLS prod / API
       │                               │                  │         key dev for
       │                               │                  │         writes; JWT for
       │                               │                  │         reads only.
       │                               └────────┬─────────┘
       │                                        │
       │                                        ▼
       │                               ┌──────────────────┐
       │                               │ Kafka cluster    │
       │                               │ wms.{tenant}.*   │
       │                               │ wes.{tenant}.*   │
       │                               └────────┬─────────┘
       │                                        │
       │                                        ▼
       │                               ┌──────────────────┐
       └───────────────────────────────│ wes-{partner}-   │
       outbound POST + Authorization:  │   adapter        │
       Bearer {partner-issued token,   │ (one Helm release│
       cached, refreshed on expiry}    │  per tenant)     │
                                       └────────┬─────────┘
                                                │
                                                ▼
                                       ┌──────────────────┐
                                       │ Postgres         │
                                       │ wms_partners DB, │
                                       │ schema per       │
                                       │ (partner, tenant)│
                                       └──────────────────┘

Inbound from the partner (master + work orders + inventory query) hits FG.AI WMS public REST directly. Outbound TO the partner (typically per-WO completion postback + inventory delta postback) is the adapter’s job — it subscribes to FG.AI Kafka, aggregates state, translates payload shapes, and posts.

Deployment topology — one Helm release per (partner, tenant)

Each (partner, tenant) pair is its own Helm release in a shared partner-adapters k8s namespace. For a partner serving N FG.AI tenants, FG.AI runs N pods of wes-{partner}-adapter, each bound to one tenant.

Aspect

Per-(partner, tenant)

Shared

Helm release name

wes-{partner-code}-adapter-{tenant-code}

Pod labels

partner-code={code}, tenant-code={code}

app=wes-{partner-code}-adapter

Kafka consumer-group

wes-{partner-code}-adapter-{tenant_id}

Postgres schema

wes_{partner_code}_adapter_{tenant_code_snake}

wms_partners Postgres instance

k8s namespace

partner-adapters

Partner credentials

SecretsManager entry per tenant

DotID service account

per tenant with wes-{partner}-adapter:* action grants

NetworkPolicy

per release; egress allow to partner host CIDR; ingress none

This matches FG.AI’s overall multi-tenant canon (per-tenant Kafka prefix per WES ADR-0020; per-tenant connector isolation per WMS ADR-0024 §5) and makes the eventual FlexSync migration a 1:1 swap when the partner onboards as a FlexSync connector post-FlexSync-v1 GA.

For a partner reading this: when your engagement adds a new FG.AI tenant, FG.AI provisions an additional Helm release for the new tenant. No code change on either side.

Inheritance chain

Every wes-{partner}-adapter is a Spring Boot 4 service inheriting from a chain of parent POMs:

spring-boot-starter-parent:4.0.6
    │
    ▼ extends
com.flexgalaxy:fgai-service-template-parent          (FG.AI canon: Spring Boot 4,
    │                                                  Java 21, protobuf-java pin,
    │                                                  testing BOM, JaCoCo gate)
    ▼ extends
com.flexgalaxy:wes-partner-adapter-parent            (partner-adapter canon:
    │                                                  Resilience4j, OTel tracing,
    │                                                  wes-partner-adapter-common)
    ▼ extends
{partner-base-package}:wes-{partner}-adapter         (per-partner repo)

Bug fixes in scaffolding classes (token cache, Kafka consumer base, etc.) propagate to all partner adapters via a parent-POM-bump on wes-partner-adapter-parent followed by per-adapter rebuild. Spring Boot version bumps cascade from fgai-service-template-parent through the chain.

Scaffolding inherited from wes-partner-adapter-common

The common library ships these reusable classes — every wes-{partner}-adapter declares it as a dependency and uses or extends them:

Class

Role

PartnerTokenCache (interface)

In-memory + Postgres-backed bearer-token cache. Refreshes proactively before expiry; invalidates on observed 401s and retries once with a fresh token.

PartnerAuthenticationClient (interface)

Per-partner implementation wires the partner’s /authenticate endpoint shape (request body, response field extraction, TTL declaration).

PartnerOutboundAuthInterceptor

Spring ClientHttpRequestInterceptor that attaches Authorization: Bearer {tokenCache.currentToken()} to every outbound request and handles 401-then-retry.

KafkaPartnerEventConsumerBase<E, P>

Abstract Kafka consumer enforcing dedup + retry + audit envelope discipline. Every adapter’s @KafkaListener class extends this.

PartnerEventTranslator<E, P> (SPI)

Per-partner implementations translate FG.AI source events to the partner’s outbound payload shape.

PartnerOutboundClient

RestClient wrapper with auth interceptor + retry.

OtelPartnerAdapterContextPropagator

W3C traceparent propagation from inbound Kafka records to outbound REST.

PartnerAdapterProperties

@ConfigurationProperties root bound to partner-adapter.* in application.yml.

ArchUnit gates

Every wes-{partner}-adapter inherits three ArchUnit gates from the common library — they run as part of mvn verify and fail the build on violation:

Rule

Why

No DotID JWT in outbound flow. Classes under ..adapter.outbound.. and ..adapter.auth.. may not depend on org.springframework.security.oauth2.jwt...

Partner outbound uses bearer-token auth via PartnerTokenCache; DotID JWT is for tenant-user surfaces.

Kafka consumers extend the base. Classes annotated @KafkaListener must be assignable to KafkaPartnerEventConsumerBase.

Enforces dedup + retry/DLT + audit envelope discipline for every consumer.

No direct partner host strings outside ..adapter.config...

Partner URLs come from PartnerAdapterProperties only — prevents hardcoded URLs in service code that wouldn’t migrate cleanly across environments.

Event flow — what hits the adapter

The adapter consumes a partner-specific subset of FG.AI Kafka topics:

Topic family

Partner-relevant content

wms.{tenant}.fulfillment.*.v1

Document state transitions on the fulfillment side (Shipper closed, etc.)

wms.{tenant}.movement.v1

Every WMS inventory movement; aggregated for inventory-delta postbacks

wes.{tenant}.confirmation.*.v1

Executor-side confirmations (per-shift summaries, per-task confirmations)

wms.partner-registry.binding.{state}.v1

Lifecycle signals (active, migrating, retired) for the orchestration handler

The specific subset for each adapter is declared in its application.yml under partner-adapter.kafka.subscription-topics. Aggregation state (per-WO line accumulation, per-shift-window inventory transitions) lives in the per-(partner, tenant) Postgres schema. An outbound_dedup table prevents duplicate POSTs after retry. The token_cache table persists the partner bearer token + expiry so JVM restart does not force re-auth.

What the adapter does NOT do

  • Does not receive inbound from the partner. Partner-side pushes (master data + work orders) hit FG.AI WMS public REST (pim/v1/, wms/v1/) directly — the adapter is outbound-only. Inbound auth is stage-specific: during the UAT bridge period, writes use the partner-bearer-JWT (transitional convenience — same JWT covers reads + writes against wms-partners-bridge-service in fg-ai-dev-services). Post-bridge-retirement (when WMS Phase 1 + Phase 2 ship), inbound write auth follows the canonical FG.AI scheme — mTLS in prod-intl + prod-cn, API key bearer in dev — handled at the WMS partner-registry layer, not at the adapter. The wms-partners/v1/ partner-bearer-JWT applies canonically to reads + /authenticate only post-retirement. See side-partner-interface-mapping § Authentication for inbound writes.

  • Does not serve wms-partners/v1/inventory-delta. That endpoint lives in the WMS reporting service, not the adapter.

  • Does not implement reconciliation. FlexSync v1 ships reconciliation as a framework capability; Path C does not. Partners reconcile by polling inventory-delta.

  • Does not round-trip partner pushes back to the partner. Each leg is one-way — partner→FG.AI inbound is not echoed back; FG.AI→partner outbound treats the synchronous HTTP response (status code + response-body business code, per interface-mapping § Error code envelope conventions) as the application-level ack — there is no separate async confirmation message expected from the partner after the POST completes.

  • Does not invent data. If a required outbound field is missing from the FG.AI event (e.g., operator ID is null when the partner spec requires it), the adapter skips that line and emits an audit event. It does not substitute defaults.

Retirement criterion

When the partner onboards as a FlexSync connector post-FlexSync-v1 GA, the wes-{partner}-adapter is decommissioned per the Path C agreement. The partner_tenant_binding row transitions active migrating retired driving the Helm release decommission via an orchestration handler. Per-tenant Postgres schemas are dropped after a backup-confirmation window; per-tenant SecretsManager entries are deleted with a 30-day recovery window; per-tenant Terraform terraform destroy removes the IAM role + Kafka ACLs.

When the last tenant for the partner transitions to retired, the per-partner GitHub repo is archived (not deleted — history preserved) and the MAX_SIDE_PARTNERS=2 budget reclaims the slot (the partner is no longer in PATH_C topology; FlexSync continues serving them).