Current-state technical overview for sharing. Behavior and contracts are source-backed; transient deployment and pipeline snapshots are intentionally omitted. Historical implementation intent remains available as
RECOMMENDATION-PLAN.md and
RECOMMENDATION-PLAN.html.
1. What BlueShop Delivers
BlueShop has a dedicated recommendation-service bounded context. It serves explainable product recommendations at four storefront placements:
Placement
Customer experience
Context sent to Recommendation
HOME
Popular or personalized discovery
Recently viewed products and, when authenticated, server-side purchase history
CART
Complementary products before checkout
Current cart product IDs
ORDER_CONFIRMATION
Relevant next products after purchase
Product IDs preserved from the completed cart
PDP_SIMILAR
Semantically similar products
The current product ID
The system combines five strategies:
CO_PURCHASE: products observed together in non-cancelled orders.
POPULARITY: time-decayed demand with category and featured-product signals.
PERSONAL_HISTORY: purchase-history fallback through affinity, category popularity, then semantic similarity.
CURATED_RELATION: catalog-manager cross-sell and upsell relations.
SIMILAR_CONTENT: multilingual semantic similarity using local ONNX inference and PostgreSQL pgvector.
Serving rereads Recommendation's local eligibility projection after the candidate cache. A present row is accepted only when it is published and its availability is not OUT_OF_STOCK; a missing row passes, and UNKNOWN also passes. The check therefore removes known unpublished or OUT_OF_STOCK candidates, but it is not fail-closed and event lag remains a serving risk.
2. Three Decision Layers
BlueShop does not use one universal recommendation score. It combines three
decision layers with different scopes, inputs, and meanings:
Layer
Question answered
Current inputs
Current strategies
Global
What is broadly relevant now?
Decayed order demand, category and featured-product facts
Purchase history and Catalog-persisted recently viewed products
Interleaved PERSONAL_HISTORY and view-seeded SIMILAR_CONTENT on authenticated HOME
Swipe to inspect diagram →
Global layer
The global layer is customer-independent. It provides a safe baseline for
anonymous traffic, cold starts, and fallback capacity. Popularity is derived
from order facts with time decay rather than from page-view volume.
Product-to-product layer
This layer starts from one or more seed products. It can express observed
co-purchase behavior, explicit catalog-manager intent, or semantic similarity.
It powers cart, order-confirmation, and PDP contexts and also contributes to
HOME personalization.
Customer-to-product layer
This layer combines long-term and short-term customer context:
Purchase history is projected inside Recommendation and drives
PERSONAL_HISTORY.
Authenticated PDP views are persisted by Catalog, with the 30 most recent
products retained per customer.
The storefront reloads that server-side viewed history and supplies up to 20
viewed product IDs as HOME seeds.
A zero-order customer receives semantic recommendations related to recent
browsing before curated and global fallback results.
A customer with purchase history receives an alternating candidate pool from
PERSONAL_HISTORY and view-seeded SIMILAR_CONTENT, preserving long-term
preference and short-term intent without comparing their incompatible scores.
Purchase and view candidates remain in the pool until exclusions, the local
eligibility-policy read, and diversity run; the result limit is applied afterward.
Customer-level decisioning does not put customer identity into measurement
events. Recommendation uses the authenticated UUID only for serving, while
served/clicked CloudEvents remain anonymous and no-PII.
Item-grain served/clicked projections and CTR views
Recommendation never becomes authoritative for product, stock, order, or customer data. It projects the minimum facts needed to make a decision and rereads local eligibility before returning a result. That read rejects a present unpublished or OUT_OF_STOCK row, but missing and UNKNOWN state pass.
4. Public And Internal Contracts
The gateway exposes the service under /api/recommendation.
Operation
Access
Purpose
GET /v1/recommendations
Public; optional JWT
Serve one placement using up to 20 seed product IDs and at most 12 results
POST /v1/recommendations/clicks
Public
Accept a client-reported click beacon; request shape is validated, but membership in the served set is not
POST /internal/v1/recommendations/recompute
admin role
Refresh known catalog snapshots, embeddings, and derived projections
A recommendation response carries:
An anonymous recommendationSetId for measurement correlation.
Placement and generation time.
Product ID, score, strategy, and a customer-readable reason for each item.
The API intentionally returns product IDs rather than duplicating Catalog DTOs. The storefront preserves recommendation order and hydrates cards through Catalog's bulk endpoint.
5. Event Ingestion And Derived Data
Swipe to inspect diagram →
Two views of a purchase
Model
Grain
Question it answers
order_product_facts
One order and one product
What happened in this exact transaction?
customer_product_purchases
One customer and one product
What is this customer's accumulated purchase relationship with the product?
Delivery semantics
Kafka consumers use the shared transactional inbox pattern under the recommendation service's RLS context.
CloudEvent envelope IDs are the preferred idempotency identity.
Aggregate keys preserve ordering where the source contract requires it.
Score tables are derived projections and can be recomputed from retained recommendation facts.
Catalog reconciliation repairs known projection drift every six hours.
PostgreSQL backup remains necessary for full historical recovery; Kafka is transport with finite retention, not an infinite event store.
Data schema evolution
Recommendation owns eight Flyway migrations:
Baseline schema and service RLS.
Order-product facts and purchase profile.
Eligibility and curated relations.
Affinity and popularity scores.
Transactional inbox.
Separate catalog and availability freshness clocks.
vector(384) product embeddings and HNSW index.
Recommendation measurement outbox.
The vector PostgreSQL extension is installed by GitOps with elevated platform privileges; application Flyway only owns service schema objects.
6. Actual Serving Strategy
The historical plan contains several candidate chains considered during design. The deployed chains are the following code-backed behavior:
Strategy priority is preserved before score ordering. Scores from different strategies are not treated as one normalized scale.
Authenticated HOME requests with purchases alternate purchase-history and
recently-viewed candidates before lower-priority fallback buckets.
The alternation preserves overflow candidates so eligibility filtering can
continue filling from customer-level signals before using global fallback.
Candidates inside a strategy are sorted by descending score and then product ID for deterministic ties.
Curated relations may replace the same product produced by an earlier strategy, preserving manager intent.
Duplicate products are removed.
Seed/context products are excluded.
Products purchased by the authenticated customer during the recent-purchase window are excluded.
Diversity accepts at most three products from one category.
The candidate cache lasts 60 seconds, then each candidate's local eligibility
projection is read. Present unpublished or OUT_OF_STOCK rows are removed;
missing rows and UNKNOWN availability pass.
Engine or serving failures return an empty recommendation set rather than breaking the containing storefront page.
Personal history
PERSONAL_HISTORY is a fallback sequence, not a weighted blend:
Co-purchase neighbours of recent purchases.
Customer-category popularity if no affinity result exists.
Semantic neighbours of the purchase seeds if the first two lanes are empty.
7. Curated Recommendation Flow
Swipe to inspect diagram →
Catalog owns the relation and its language. Recommendation owns serving and measurement. A curated target is removed when its local projection is present and says unpublished or OUT_OF_STOCK. Missing eligibility or UNKNOWN availability passes, so projection lag can still expose an unavailable target.
8. Semantic Recommendation Flow
Swipe to inspect diagram →
Runtime model
Base model: sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2.
Conversion: Xenova quantized ONNX model pinned at revision 2c4055b12046f11709e9df2c122e59ffbdc2f900.
Model checksum: 66fc00f5f29afcaff34092e1bdd20008ca3918265a82fb9695a551e510cc4ebc.
License: Apache-2.0.
Inference: DJL tokenizer plus ONNX Runtime, entirely local to the service.
Vector shape: 384 finite dimensions, mean-pooled and L2-normalized.
Execution: dedicated semantic-embedding Reactor scheduler; inference does not run on the WebFlux event loop.
Minimum similarity: 0.55 by current configuration.
Default inference concurrency: 1.
Semantic text prioritizes structured product tags when present. Otherwise it combines localized name, description, short description, SKU, brand, and localized attribute values.
Semantic cold start does not require order history. A newly published product can receive an embedding from its Catalog event and immediately participate in PDP_SIMILAR once its snapshot and availability are current.
9. Storefront Integration
Swipe to inspect diagram →
The Angular implementation follows BlueShop's frontend boundary rules:
Components consume signals and never call recommendation HTTP APIs directly.
Product cards are rendered only when at least three recommendations hydrate successfully.
Product lookup errors and recommendation errors hide the strip without blocking product, cart, checkout, or order pages.
Click measurement uses a navigation-safe keepalive request.
API order is preserved after Catalog hydration.
10. Measurement And BI
Swipe to inspect diagram →
Measurement contract
RecommendationServed records the anonymous set ID, placement, anonymous/authenticated request context, seed product IDs, ranked items, environment, traffic class, and generation time.
RecommendationClicked records the set ID, placement, product, one-based rank, strategy, environment, traffic class, and click time. The public request supplies the set ID, placement, product, rank, and strategy; the public traffic-class header is also caller-controlled and defaults to LIVE when absent or malformed. The service owns only environment and receipt time.
Neither event includes customer identity, contact data, address data, cost, or margin. The click endpoint validates field shape and enum parsing, but does not look up the served set or verify that the product, rank, placement, or strategy belonged to it. A conforming caller can therefore forge a tuple that ClickHouse later joins to a served tuple. These events are useful client instrumentation, not trustworthy proof of a human choice, causal impact, or customer identity.
Analytical model
Topic: recommendation.events.
Producer: service-local transactional outbox and scheduled relay.
Consumer: analytics-service.
Raw fact table: analytics.raw_recommendation_interactions.
Deduplicated interaction view plus analytics.recommendation_product_performance at product grain.
Dashboard: Metabase dashboard 22, route recommendation-performance-ctr, with a product-performance table rather than only aggregate CTR.
Product fields: product ID, current product name and SKU, placements, strategies, impressions, clicks, CTR, average rank, last served time, and last clicked time.
Filters: deployment environment and traffic class; product rows retain all observed placements and strategies.
Traffic classes: LIVE, SYNTHETIC, and retained LEGACY events.
Product clicks are joined to impressions on recommendation-set ID, product ID,
and rank. This analytical join does not repair the missing server-side
validation at ingestion.
Served measurement is deliberately non-blocking for the customer response: a measurement persistence error is logged but does not prevent recommendations from rendering. Click persistence is an explicit accepted request path, but its values remain client-reported.
11. Security, Privacy And Operations
Security
Public recommendation reads and click writes use permitAll.
Personalization activates only when a JWT subject parses as a UUID.
Internal recompute requires ROLE_admin.
Every recommendation table uses forced PostgreSQL RLS with service identity context.
Vault supplies separate dynamic runtime and migration database roles.
Kafka uses the recommendation service's Keycloak/OIDC workload identity and least-privilege ACLs.
The public measurement payload is no-PII by construction.
Operational cadence
Affinity and popularity recompute: every 10 minutes by default.
Catalog/embedding reconciliation: every 6 hours by default.
Measurement outbox relay: every 1 second by default.
Candidate cache: 60 seconds.
Catalog WebClient timeout: 5 seconds with retry and circuit breaker policies.
These five players stay together as one Recommendation gallery and play directly
from the local share package. Every film uses burned-in scene narration, so its
business story remains understandable without audio:
PERSONALIZATION LOOP / 42.12S
Checkout to personalized recommendations
Shows a customer add a product, keep checkout customer-visible through address entry, place the order, receive clickable next-product recommendations that exclude the purchase, then return to a HOME strip recomposed from purchase history and recently viewed intent.
The film visibly shows the real order, a later product view, and the resulting HOME strip. Its executable E2E response assertion, rather than labels on the product cards, verifies one PERSONAL_HISTORY result, one SIMILAR_CONTENT result, and exclusion of both starting products.
Shows product-level Recommendation BI: product name and SKU alongside impressions, client-reported clicks, CTR, average rank, placement, strategy, and engagement gaps, then isolates one accepted click tuple. It demonstrates instrumentation, not a trustworthy human choice.
Shows a catalog manager curating two related products, a stock adjustment projecting known OUT_OF_STOCK state and removing that candidate, and a customer following the eligible recommendation to its product page. It demonstrates the operational recommendation loop without an E2E-only BI diagnostic.
Visible UI evidence shows a newly eligible product receiving a "Similar products" strip. SIMILAR_CONTENT strategy, its explanation, and operation without purchase history are source- and BDD-backed behavior, not claims established by the film itself.
Local projection is reread after cache lookup; missing and UNKNOWN eligibility pass
Missing or lagging eligibility
Current serving risk
The policy is not fail-closed, so event lag can expose an unavailable candidate
Served/client-reported click measurement and product-level Recommendation BI
Delivered with trust limit
No-PII pipeline is useful for instrumentation; click tuples are not verified against served sets
Gorse learned engine and A/B routing
Deferred by design
Current client-reported click data is not a trustworthy learned-engine gate
Elasticsearch More-Like-This adapter
Not built
pgvector shipped directly
Semantic fallback in CART
Not implemented
Actual chain is co-purchase, curated, popularity
Non-semantic fallback on PDP
Not implemented
Actual PDP chain is semantic-only; thin results hide the strip
Weighted blending across all strategies
Not implemented
Current engine uses ordered priority fill; only the two customer-level HOME signals are deterministically alternated
Infinite event replay as disaster recovery
Not claimed
Full historical recovery requires PostgreSQL backup plus retained Kafka replay
14. Result
The delivered BlueShop recommendation capability is not a single algorithm or UI carousel. It is a complete bounded context:
Domain-owned serving policy and explainable strategies.
Event-fed projections from authoritative commerce services.
Catalog-manager curation and filtering of known local OUT_OF_STOCK state, with an explicit event-lag risk.
Customer-level personalization combining long-term purchase history with short-term browsing intent.
Multilingual semantic cold start without external inference infrastructure.
Reactive storefront integration at four customer moments.
Anonymous, no-PII served and client-reported click instrumentation through Kafka and ClickHouse.
Protected BI that demonstrates the instrumentation path, not causal impact or trustworthy customer action.
Phase 5 remains intentionally unbuilt. The current click stream cannot gate a learned engine because callers control the click tuple and traffic class. Such a gate requires server-side served-set validation, abuse controls, and experiment-quality evidence first.