Vue d'ensemble
product-service gère le catalogue de la boutique : les produits et leurs prix.
Stripe est la source de vérité. Le service appelle l’API Stripe pour chaque modification,
puis attend le webhook de Stripe avant de considérer la donnée comme confirmée et de
l’écrire dans PostgreSQL.
Code : core/product-service/. Configuration : voir Variables d’environnement.
Ce qu’il expose
Section intitulée « Ce qu’il expose »| Interface | Port par défaut | Usage |
|---|---|---|
| REST (axum) | 8093 |
Front admin, boutique, hôtes PHP. Routé par Traefik sur api.clairtyx.com/v1/products*. |
| gRPC (tonic) | 50051 |
Hôtes Go, Rust, C, .NET. Non exposé par Traefik. |
| Kafka / Redpanda | — | Publie product.events ; consomme provider.webhooks.raw. |
REST et gRPC sont deux adaptateurs minces sur le même ProductAppService : aucune
logique métier n’est dupliquée, le comportement est identique par construction.
Cycle de vie d’une modification
Section intitulée « Cycle de vie d’une modification »PostgreSQL ne contient que du confirmé. Une mutation n’y écrit jamais directement :
Client ──REST/gRPC──▶ ProductAppService │ 1. appel API Stripe (synchrone) │ 2. résultat provisoire ──▶ Valkey/Redis (TTL) ▼ réponse (confirmation_status = "created")
Stripe ──webhook──▶ webhook-ingestion-service ──▶ Kafka provider.webhooks.raw │ product_event_applier │ 3. écrit la ligne confirmée │ + ligne d'outbox (même transaction) │ 4. évince l'entrée du cache ▼ OutboxRelay ──▶ Kafka product.events- Un produit tout juste créé a
confirmation_status: "created"tant que le webhook n’est pas arrivé (lu depuis le cache), puis"confirmed"(lu en base). GET /v1/productsetGET /v1/products/:idlisent uniquement PostgreSQL : un produit encore provisoire n’y apparaît qu’après son webhook.- Le TTL du cache est un filet de sécurité si le webhook n’arrive jamais, pas le mécanisme normal d’éviction.
- Un produit ou un prix créé directement dans le tableau de bord Stripe est ajouté en base, déjà confirmé, à la réception de son webhook.
- Produit :
name,description,images(clés d’objets),active, plus un JSONBattributespour tous les autres champs Stripe (metadata,tax_code, …). - Prix : un par Prix Stripe (
label,price_amounten entier de la plus petite unité,currency,active, plus un JSONBattributes:recurring,tiers,tax_behavior, …).price_amountvautnullpour un prix sans montant unique (par paliers, montant choisi par le client). - Prendre en charge un nouveau champ Stripe ne demande donc pas de migration.
- Archivage, pas suppression : un produit supprimé passe à
active = false, comme chez Stripe qui interdit de supprimer un produit ayant des prix. Un prix se désactive de la même façon. - Prix immuables : un Prix Stripe ne se modifie pas. Changer le libellé, le montant ou la devise remplace le prix côté Stripe et réaffecte le prix par défaut du produit.
- La commande de création
{ product, prices[] }est validée par un schéma JSON (schema/catalog_create.schema.json), publié surGET /v1/products/schema.
Une photo envoyée au service est stockée dans un bucket S3 compatible (Garage) et reçoit une
clé (products/<uuid>.<ext>). Stripe n’accepte que des URL : la clé lui est envoyée sous
la forme <IMAGE_PUBLIC_BASE_URL>/<clé> (CDN cdn.clairtyx.com). Au retour du webhook,
une URL sous cette base est reconvertie en clé.
Fiabilité
Section intitulée « Fiabilité »| Mécanisme | Rôle |
|---|---|
| Outbox transactionnelle | La ligne d’événement est écrite dans la même transaction que la ligne produit : « enregistré » et « à publier » ne peuvent pas diverger. |
| Relais | LISTEN/NOTIFY plus interrogation toutes les 2 s, par lots de 100. Plusieurs réplicas sont sûrs (FOR UPDATE SKIP LOCKED avec bail). |
| Dead-letter | Après 10 échecs de publication, la ligne n’est plus retentée automatiquement. |
| Idempotence | Chaque mutation exige l’en-tête Idempotency-Key (400 sans lui). La réponse est mémorisée par couple (clé, méthode) : rejouer la requête ne rappelle pas Stripe. |
| Kafka | Offsets validés manuellement, après traitement. |
Événements publiés
Section intitulée « Événements publiés »Topic product.events, types ProductCreated, ProductUpdated, ProductDeleted. Il n’y a
pas d’événement de prix : toute modification d’un prix republie le produit parent complet
sous ProductUpdated. La livraison est au moins une fois : dédoublonner sur event_id.
{ "event_id": "9f0c…", "event_type": "ProductUpdated", "occurred_at": "2026-10-13T09:00:00+00:00", "product": { "id": "prod_…", "stripe_product_id": "prod_Stripe…", "name": "T-shirt", "description": "Coton", "images": ["products/tee.png"], "active": true, "attributes": {}, "prices": [ { "id": "pri_…", "stripe_price_id": "price_…", "label": "M", "price_amount": 1500, "currency": "eur", "active": true, "attributes": {} } ], "created_at": "…", "updated_at": "…" }}Autorisation
Section intitulée « Autorisation »Le service ne gère ni comptes ni mots de passe : il vérifie le JWT d’accès signé par le
backend (clé publique lue sur son JWKS) et applique les règles AUTH_RULES, route par
route. Une route sans règle est refusée (fail closed). Le détail est dans
AUTH_RULES.
Dépendances
Section intitulée « Dépendances »PostgreSQL (produits, prix, outbox, clés d’idempotence), Valkey/Redis (cache provisoire),
Kafka/Redpanda (événements), Stripe (API), un stockage S3 compatible (photos), et le
backend (JWKS).
Organisation du code
Section intitulée « Organisation du code »src/├── routes/ câblage HTTP → handlers, CORS, middleware d'autorisation├── http/handlers/api/ products, images, catalog (schéma), health, metrics├── grpc/ adaptateur gRPC├── config/ variables d'environnement (conf, auth, cors)├── infrastructure/ auth (JWT + règles), idempotence, métriques, logs└── domains/products/ ├── entities/ Product, Price, identifiants ├── repositories/ PostgreSQL, cache Valkey, outbox, dead-letter ├── services/ ProductAppService (crud/), client Stripe, event applier, relais └── events/ enveloppe product.eventsMigrations SQL : core/product-service/migrations/.