Skip to content

Containers ​

The fifteen deployable units, their ports, and the calls between them (C4 level 2).

Two boundaries. store-core runs once per environment: six containers and one Postgres. store-pod runs once per pod: nine containers, one Postgres and one object store. Every pod is a complete, isolated copy; nothing in a pod is shared with another pod. The shapes follow the legend.

Diagram C2 ​

What the edges mean:

  • console-ui → gateway. The console is served by the gateway's catch-all route and calls the platform APIs on the same origin under /tenancy/**, /billing/**, /pod-registry/** and /uaa/**. It holds no token; the gateway's session does.
  • gateway → tenancy, billing, pod-registry, uaa. GatewayRouteLocatorImpl strips the prefix and relays the signed-in user's token (tokenRelay()). The /uaa/** route alone keeps its prefix and adds X-Forwarded-Prefix: /uaa.
  • gateway → spg. PodClient builds one route per pod at runtime: /spg/** with store= and pod= query parameters, StripPrefix=1, TokenRelay. This is how a merchant edits a product that lives in any pod through one origin. See Gateway routing.
  • spg → pod services and landing-ui. The Caddyfile path-routes /content*, /merchant*, /inventory*, /catalog*, /checkout*, /cua*, /payment*; everything else goes to landing-ui after domain_lookup has injected the store headers. See store-pod and Edge and custom domains.
  • landing-ui → spg (server-side). The storefront's server renders read merchant, content, catalog, checkout and inventory through the pod's own spg (INTERNAL_SPG), so a server-side read and a browser-side call take the same route.
  • pod services → uaa. Every pod service is a JWT resource server that accepts tokens from uaa (staff) and cua (shoppers), and authenticates to its peers with a client_credentials client against uaa. See Authentication.
  • catalog → billing. catalog-core guards product writes with StoreEntitlements from billing-external-api. It is the one call from a pod back into store-core.
  • payment → Stripe, billing → Stripe. Store payments and platform subscriptions; each receives Stripe's webhooks on its own public endpoint.

The fifteen containers ​

ContainerLayerPortRuntimeOwnsFronted by
store-core-gatewaystore-core8000Spring Cloud Gateway (WebFlux)The browser session, the OAuth2 client login against uaa, token relay, the per-pod route tableitself (gateway.com)
uaastore-core8001Spring Boot, embeds Angular uaa-feStaff and merchant identity: OAuth2 authorization server and OIDC provider, users, roles, clients, brokered login, its admin appstore-core-gateway (/uaa/**); its own host uaa.gateway.com
console-uistore-core8011Angular 20 SSRThe merchant and platform-admin consolestore-core-gateway (catch-all)
tenancystore-core8020Spring BootOrganizations, stores, members, sign-up, the store → pod binding, store provisioning through the outboxstore-core-gateway (/tenancy/**)
billingstore-core8021Spring BootPlans, per-store subscriptions, the Stripe webhook, entitlements and store quotasstore-core-gateway (/billing/**)
pod-registrystore-core8022Spring BootThe pod catalog: identity, endpoint, health, capacity, placementstore-core-gateway (/pod-registry/**)
spgstore-pod80 / 443CaddyOn-demand TLS for custom domains, domain → store lookup, path routing into the poditself (the store's host)
landing-uistore-pod8110Next.js 16 / React 19The storefront: one app, every theme, the page cachespg (fall-through)
merchantstore-pod8120Spring BootStore configuration: languages, currency, domains, address; the routing hooks spg callsspg (/merchant*)
contentstore-pod8121Spring BootPages, posts, banners, FAQ, policies, menus, media library, site settings and appearancespg (/content*)
catalogstore-pod8122Spring BootProducts, variants, options, categories, brands, product types and groups, imagesspg (/catalog*)
checkoutstore-pod8123Spring BootCart, orders, customers, countriesspg (/checkout*)
cuastore-pod8124Spring BootShopper identity: a headless OAuth2 authorization server, registration, social loginspg (/cua*, prefix kept)
paymentstore-pod8125Spring BootPayment provider configuration per store, payment execution, provider webhooksspg (/payment*)
inventorystore-pod8126Spring BootStock, prices and reservations, keyed by skuspg (/inventory*)

Ports, hostnames and the fronting gateway (gateway-service-name) are recorded per service in common-config.yml. A service is reachable on its own port only inside its namespace (store-core.cvhome.lcl, store-pod-<id>.cvhome.lcl); from anywhere else it is a path on its gateway, with the prefix stripped (except /cua and /uaa).

Service to service ​

Diagram C2 draws how a request reaches a service. It does not draw what the services then ask each other, and that is a different graph: a request for a cart page fans out to four services before it answers.

Every edge below is one module depending on another domain's -external-api, which is the only sanctioned way a service may call a peer. The interface lives with the provider, the provider's External*Api controller implements it, and the caller consumes it through RestClientBuilder. Nothing calls another service's -core or -service module, so this table is the complete set: a new edge cannot appear without a new dependency in a build.gradle.

Inside a pod ​

Solid is a call the caller waits for; dashed is a signal delivered after the fact, from an outbox or a scheduled job. Shapes follow the legend.

Two things the picture says that the prose should not bury.

merchant is the hub of a pod. Six of the nine services call it, all for the same thing: the store's configuration, through ExternalMerchantStoreService.getStore. It is also what spg asks on every cold request, to turn a host name into a store and to decide whether to issue a certificate (edge and custom domains). Nothing else in a pod is depended on so widely, so merchant being slow is the pod being slow, and merchant being down is the pod being down.

checkout is called back, not just calling. Placing an order reserves stock and starts a payment, and both of those finish later, so inventory and payment call ExternalOrderSignalService on checkout when they do. The order is the thing that survives; the signals move it along. That is why the order path reads as durable rather than as one long transaction (store-pod).

Across the layers ​

A pod reaching into store-core happens in exactly one place. catalog asks billing for a store's entitlements before a product write, which is why lcl-config.yml carries a namespace-qualified alias for the platform gateway. Everything else crossing the boundary goes the other way, from store-core into a pod.

CallerCalleeContractWhat it is for
store-core-gatewaypod-registryExternalPodService.listPodsThe pod list that becomes the /spg/** routes, refreshed every minute (gateway routing)
store-core-gatewaybillingExternalEntitlementService.snapshotStoreBillingGuardFilter answers 402 on a seller write when the store is not entitled
tenancybillingExternalStoreQuotaService.checkStoreCreate, provisionMay this organization create another store, and the new store's subscription. A refusal is a 422
tenancypod-registryExternalPodPlacementService.placeWhich pod a new store is placed in
tenancymerchant in the target podMerchantStorePodClient.createCreates the store inside the chosen pod, driven by the outbox (tenancy and provisioning)
catalogbillingExternalEntitlementService.snapshotThe store's entitlements before a product write. The only pod-to-core call

The calls inside a pod ​

CallerCalleeContractWhat it is for
checkoutcatalogExternalProductService (/cart-lines, /detailed-product)Product data for the lines in a cart
checkoutinventoryExternalProductReservationService (reserve, commit, release)The stock reservation lifecycle behind an order
checkoutpaymentExternalPaymentGatewayService (initiatePayment, status)Starts a payment and reads its outcome
catalogcontentExternalMediaService.resolveMedia assets by id, and their usage
inventorycheckoutExternalOrderSignalService.signalReservationExpiredA reservation expired; the expiry job tells the order
paymentcheckoutExternalOrderSignalService.signalPaymentThe payment outcome reaches the order, delivered from payment's outbox
cuamerchantExternalMerchantStoreService.getStoreWhich store owns the shopper signing in
catalog, checkout, content, inventory, paymentmerchantExternalMerchantStoreService.getStoreThe store's configuration: languages, currency, domains

Every one of these carries a client_credentials token minted by uaa for the calling service, and every one names its failure contract when the client is built, so a peer being unavailable is a typed exception rather than a stack trace (authentication).

Data ​

Each Spring Boot service owns one Postgres schema, named after the application (pod_registry, content, payment, ...), and ships its own DDL as a schema.sql. Foreign keys never cross a schema. store-core shares one Postgres; each pod has its own. content, merchant and payment configure an S3 client for files (MinIO locally; S3 behind CloudFront on AWS), and spg keeps its certificates in an S3 bucket.

Shared libraries ​

store-commons/ is libraries only; nothing in it is deployed. Every service inherits its cross-cutting behavior by depending on these modules:

ModuleWhat it gives a service
commonsThe value objects used everywhere in place of raw strings: StoreMerchantId, ManagerOrgId, PodId, LanguageCode, Pod, PodEndpoint, Theme, ...
errorsThe shared error catalog and ProblemDetail handling
autoconfigureMulti-issuer JWT decoding, the permission evaluator, web clients, and the shared YAML (common-config.yml, lcl-config.yml, fargate-config.yml)
uaa-client, uaa-client-implA typed SDK for uaa's admin API
sso/sso-core, sso/sso-eventsThe authorization-server core that uaa and cua are both built on
secret-crypto/*Encryption of stored secrets (payment keys, social-login credentials): local AES or AWS KMS, with a caching decorator
ecs-commons/*Cloud Map service discovery and ECS task metadata for Fargate; inert locally
test-supportTestcontainers, test JWT signing and the integration-test annotations; never on a production classpath
ui-kitThe Angular component kit console-ui and uaa-fe share

A second module is also called store-commons: store-pod/commons/store-commons is the pod-scoped shared domain. Refer to either by its Gradle path.

Images ​

Container images are named <layer>/<service>: store-core/uaa, store-core/console-ui, store-pod/catalog, store-pod/spg, and so on. Nothing deploys from the application repository's CI; images build in CodeBuild and Terraform applies them. See Deployment: AWS and Deployment: local.


Source of truth: cvhome store-commons/autoconfigure/src/main/resources/common-config.yml, store-pod/spg/Caddyfile, store-core/gateway/gateway-service/src/main/java/com/asrevo/cvhome/gateway/config/GatewayRouteLocatorImpl.java, store-core/gateway/gateway-service/src/main/java/com/asrevo/cvhome/gateway/client/PodClient.java, .claude/skills/project-structure/SKILL.md and references/{store-core,store-pod,shared-libraries,database-schemas,service-to-service}.md, store-pod/catalog/catalog-core (StoreEntitlements), store-pod/landing-ui/libs/services/src/store-context-ssr-utils.ts.