Spécialiste européen du reconditionné
120 000 articles suivis à l'unité, chacun tarifé en moins d'une seconde.
3+ ans
en production continue
Millions
de variants en catalogue
<100 ms
p99 sur la recherche
En bref
- Le problème
- Marketplaces, boutique en propre, B2B et opérations entrepôt doivent partager la même vérité sur le stock et le prix en temps réel, faute de quoi ce que le marché voit cesse de correspondre à ce que l'entreprise peut livrer.
- La décision
- Des services spécialisés qui ne partagent aucune base de données et se synchronisent par événements, avec une recherche servie par un index reconstruit chaque jour et des prix calculés à la volée plutôt que stockés.
- Le résultat
- La plateforme alimente chaque canal à son rythme et se contrôle elle-même : une réconciliation périodique compare ce que chaque canal expose à ce que le catalogue prétend avoir transmis, et signale tout écart qui dure.
Le contexte
Notre client opère sur le marché européen de l'électronique reconditionnée, à travers plusieurs canaux : marketplaces, boutique en propre, B2B grands comptes, opérations entrepôt. Chaque canal a son propre rythme, ses propres exigences de formatage, et son propre contrat opérationnel.
La difficulté n'était pas la complexité d'un système, c'était de tenir la même vérité sur le stock et le prix entre plusieurs systèmes, en temps réel.
Au démarrage, l'enjeu n'était pas la complexité technique d'un seul système. C'était la cohérence entre plusieurs systèmes qui doivent partager une même vérité sur le stock et le prix, en temps réel, à l'échelle d'un catalogue de plusieurs millions de variants et d'un parc de plus de cent mille articles série identifiés unitairement.
Nous avons construit la plateforme comme un ensemble de services spécialisés (catalogue, opérations entrepôt, adaptateurs canaux, moteur B2B) qui communiquent via un bus d'événements asynchrone.
Aucun service ne partage de base de données avec un autre, ce qui rend chaque source de vérité inaccessible de l'extérieur.
Aucun service ne partage de base de données avec un autre. La règle est simple : chaque service est seul à pouvoir modifier sa source de vérité, et publie ses changements sous forme d'événements typés que les autres consomment.
Les décisions d'architecture
Le catalogue indexe aussi les variants sans stock, parce qu'une offre épuisée reste une offre que le marché voit et que l'entrepôt doit ré-approvisionner.
Catalogue indexant tous les variants, y compris ceux sans stock, pas Réduire le catalogue aux seuls SKU effectivement stockés. Un variant sans stock reste une offre dormante : la configuration marchand existe, seul l'approvisionnement manque. Les marketplaces affichent l'iPhone « disponible en rouge » même quand le rouge est épuisé. Les équipes Operations doivent savoir quels variants ré-approvisionner. Réduire le catalogue aurait masqué ces deux signaux et créé une décorrélation permanente entre ce que le marché voit et ce que nous gérons.
Trade-off assumé : Plusieurs millions de variants à maintenir, là où seule une fraction correspond à du stock effectif. Amplification mécanique des coûts de synchronisation vers les canaux externes. Mitigation : un cache SKU par adaptateur de canal filtre vers les seules références réellement transmises, avec une discipline de réconciliation périodique pour détecter les dérives.
La recherche est servie par un index reconstruit chaque jour plutôt que calculée à chaque requête, parce qu'un client qui fait défiler une liste n'attend pas plusieurs secondes.
Indice de recherche matérialisé, reconstruit quotidiennement, pas Agrégation live sur la base catalogue à chaque requête. L'agrégation live sur un catalogue de cette taille coûte plusieurs secondes par requête. Inacceptable sur mobile où l'utilisateur scrolle. La vue matérialisée dénormalisée descend la requête sous les 100 ms p99. Elle n'indexe que les variants avec stock disponible, ce qui garde l'index pertinent sans masquer le modèle complet utilisé pour la planification.
Trade-off assumé : Les données de recherche peuvent avoir jusqu'à 24 heures de retard sur les mises à jour de stock. Acceptable parce que la requête de stock définitive se fait au checkout via une API séparée. La recherche est exploratoire, pas transactionnelle. Le coût opérationnel est de pouvoir reconstruire l'index complet en moins d'une heure si besoin de rattrapage.
Le prix n'est pas stocké : seul le coût au repos l'est, et les ajustements se calculent à la demande, si bien qu'un changement de règle tarifaire est un déploiement de code et non une reprise de données.
Prix en deux étapes : coût au repos stocké, ajustements calculés à la volée, pas Pré-calculer et stocker tous les prix finaux par combinaison de règles. Le prix final dépend de plusieurs axes : durée de garantie, accessoires inclus ou non, conditions négociées par client B2B, règles propres au canal de sortie. Pré-calculer toutes les combinaisons crée une explosion combinatoire ingérable à 120 000 articles. La fonction d'ajustement est pure (CPU seul, sans I/O) ; elle coûte environ 0,001 ms par item, soit ~500 ms pour un lot de 120 000. Le bénéfice principal est qu'un changement de règle (par exemple le taux appliqué à une option de garantie qui évolue) ne nécessite aucune invalidation de cache. C'est un déploiement de code, et non une mutation de données.
Trade-off assumé : Chaque requête de prix paie le coût de calcul. Compensé par le regroupement des lectures, et par la garantie que le calcul est CPU seul (il ne provoque jamais d'attente d'I/O). Le code de l'ajustement est versionné comme une dépendance critique : un changement de logique passe par revue et test, jamais par mutation directe en base.
L'exécution
La plateforme tourne sur infrastructure auto-hébergée.
Chaque service vit dans son propre conteneur sur des serveurs dédiés par environnement, et les sauvegardes sont vérifiées chaque mois par une restauration de test.
Chaque service vit dans son propre conteneur, déployé via GitHub Actions sur des VPS dédiés (un par environnement : pré-production, production). Le bus d'événements (NATS JetStream) tourne sur sa propre instance avec persistance disque. Les bases de données (MongoDB pour le catalogue, PostgreSQL pour le B2B) ont leur propre cycle de sauvegarde, vérifié mensuellement par restauration test sur un environnement éphémère.
3 mécanismesTrois mécanismes rendent l'exploitation lisible : journaux structurés, battements de cœur dont l'absence alerte autant que le contenu, et mise de côté des messages en échec pour rejeu.
L'observabilité s'appuie sur trois mécanismes complémentaires. Journaux structurés par service, agrégés sur un serveur central, avec un niveau de verbosité configurable par environnement. Signaux de vie périodiques publiés par chaque adaptateur de canal toutes les 5 minutes : compteurs de synchronisation, taux d'échec, dérive de cache SKU. Ces signaux sont eux-mêmes des messages NATS, ce qui veut dire qu'on peut alerter sur leur absence aussi facilement que sur leur contenu. Files de rebut par flux : chaque message qui échoue trois fois après temporisation exponentielle est mis de côté avec son contexte complet, lisible et rejouable par un humain.
Les déploiements sont continus : une fusion sur la branche principale enchaîne construction, tests et redémarrage des services dans l'ordre qui préserve les invariants du bus.
Une fusion sur main déclenche la construction, les tests, la publication de l'image, puis l'orchestration : chaque service redémarre dans l'ordre qui préserve les invariants du bus (les consommateurs avant les producteurs en cas de changement de schéma d'événement).
Les leçons
Leçon 1Un système qui se synchronise par événements doit pouvoir prouver son état par une voie indépendante, sans quoi il fonctionne sans s'opérer.
La première leçon a porté sur la dérive silencieuse entre le catalogue et les canaux externes. Au démarrage, nous nous appuyions exclusivement sur la diffusion d'événements pour synchroniser le stock vers chaque canal. Cela fonctionne, tant qu'aucun canal ne rejette silencieusement une catégorie d'événements sans erreur visible côté émetteur. Une évolution d'API côté canal suffit à créer cette situation, et rien dans le système émetteur ne la signale.
Nous avons ajouté un mécanisme de réconciliation périodique : une tâche qui compare ce que chaque canal expose effectivement avec ce que le catalogue prétend avoir transmis. Tout écart maintenu plus de 24 heures déclenche une alerte humaine. Plus largement, nous avons appris qu'un système distribué qui s'appuie sur des événements doit toujours pouvoir prouver son propre état par une voie indépendante ; sinon il fonctionne, mais il ne s'opère pas.
Leçon 2Traiter le stock comme une grandeur unique a coûté un remaniement le jour où un second périmètre d'exploitation a démarré avec ses propres règles.
La seconde leçon est venue avec l'arrivée d'un second périmètre d'exploitation. Notre première version traitait le stock comme une grandeur unifiée : un variant était « en stock » globalement. Dès qu'un second périmètre a démarré, avec ses propres règles et ses propres délais d'expédition, nous avons dû repenser le modèle de visibilité : chaque commande doit voir le stock de son périmètre d'abord, puis éventuellement le stock global pour les cas de fulfillment croisé. Cela a touché le catalogue, le pricing, l'API B2B et l'export marketplace. Un refactoring qu'un meilleur modèle initial nous aurait évité.
En production aujourd'hui
La plateforme tourne en continu depuis plus de trois ans. Les volumes : plusieurs millions de variants au catalogue, plus de cent mille articles série tracés unitairement, et un flux continu de mises à jour de stock propagées à travers le bus d'événements. Les marketplaces consomment leurs flux dédiés ; le frontoffice B2B et la boutique en propre consomment leur propre vue du catalogue. Chaque canal a son propre rythme et son propre contrat ; la plateforme s'efforce de ne jamais en perdre un seul de vue.
Le volet facturation et comptabilité de cette plateforme fait l'objet d'une étude de cas distincte : facturation automatique, transferts entre entités et export DATEV.
La feuille de route à court terme finalise un service Operations dédié à l'arrivée de la marchandise (réception, contrôle qualité, mise en linéaire) : service additionnel de la plateforme, qui standardise cette chaîne d'entrée.
Un système à concevoir, déployer ou reprendre en main ?
Avec ou sans cahier des charges : décrivez votre situation, nous répondons par écrit sous 48 h ouvrées.