Luxury Retail Clienteling Platform

Overview

A cloud-native, multi-brand clienteling platform built for one of Europe's largest luxury goods conglomerates. The platform is used by Client Advisors (CAs) — in-boutique sales staff — across several luxury Maisons to manage and deepen long-term relationships with their most valuable clients. Think of it as a CRM meets a luxury concierge app: every phone call, WhatsApp message, store visit, or gifting moment gets logged, every client gets a 360° view, and every CA knows exactly where a relationship stands.

The backend is a microservices system of 18 services running on GCP Kubernetes, following an inner source model where LVMH's platform team publishes shared NestJS libraries that each brand team adopts and extends independently.

DomainLuxury Retail / Clienteling (B2B2C)
RoleSenior Backend Engineer (Consulting)
Team6+ Engineers across platform & brand teams
StackNestJS, TypeScript, Salesforce, GCP, Kubernetes, Istio, FluxCD, Redis, Elasticsearch, Firestore, Adyen
Scale18 microservices · 93 API endpoints · 10+ interaction types · multi-brand deployment

System Architecture

The platform is a microservices architecture deployed on GCP GKE. All external traffic passes through an Istio ingress gateway that enforces TLS termination and JWT validation. Services communicate internally via Kubernetes DNS secured by Istio mTLS. Salesforce is the primary system of record for all CRM data.

Service Catalog

ServiceDomainKey Integrations
client360360° client view: header, KPIs, timelineSalesforce, other microservices
outreachInteractions, templates, newsletters, outreach channelsSalesforce
clientsClient profile CRUD and searchSalesforce, Redis, Bynder
usersCA profiles, store assignments, widgetsSalesforce, Appointments svc, Tasks svc
appointmentsAppointment bookingSalesforce, Booxi, Bambuser
consultationVirtual and in-store consultationsSalesforce, Bambuser, SightCall, Redis
curationsPersonalized product setsSalesforce, Firestore, GCS
ordersPay-by-link ordersSalesforce, Adyen, Redis
transactionsPurchase historySalesforce, SFCC (Demandware)
productsProduct catalog searchElasticsearch, Redis
tasksCA task managementSalesforce
eventsBrand eventsSalesforce
dashboardsCA performance dashboardsSalesforce, GCS
notificationsPush notificationsFirebase, Firestore
npsNet Promoter Score surveysSalesforce, Transactions svc
translationsUI translation stringsSalesforce, GCS

Inner Source Model

The most distinctive architectural decision in this project is the inner source model — applying open-source contribution principles to internal platform libraries. The LVMH platform team maintains shared NestJS libraries that each brand team installs as versioned NPM packages and extends with brand-specific implementations.

Each inner source repository is a NestJS monorepo with two layers:

lvmh-svc-outreach/
├── apps/
│   └── lvmh-svc-outreach/      # Reference implementation (not published)
│       └── src/
└── libs/
    └── is-svc-outreach/        # Publishable NPM library (is- = inner source)
        └── src/
            ├── interaction/
            ├── newsletter/
            ├── outreachChannel/
            └── template/

Why inner source over a shared microservice?

ApproachShared MicroserviceInner Source Library
Blast radiusA bug affects all brands simultaneouslyEach brand runs its own isolated binary
Upgrade cadenceForced — all brands on same versionEach brand upgrades at their own pace
Brand customizationHard — feature flags proliferateClean — brand overrides specific layers
Operational overheadOne shared deployment to maintainN brand deployments, but fully autonomous

Plugin Architecture — Extensibility Without Forking

I contributed to designing and implementing the plugin/registration system inside the inner source outreach library. The OutreachCoreModule.register(config) pattern lets any brand replace any layer (controller, repository, mapper, interaction processor) using NestJS dependency injection — without touching the core code.

A brand's AppModule looks like:

OutreachCoreModule.register({
  interactionFindAllRepository: {
    provide: INTERACTION_FIND_ALL_REPOSITORY,
    useClass: BrandCustomInteractionRepository,
  },
  interactionProcessors: [BrandWatchDeliveryProcessor],
})

Everything not overridden falls through to the library's default Salesforce implementation.


Outreach Service — Deep Dive

The Outreach service is the domain I worked on most deeply. It manages every recorded touchpoint between a CA and a client: calls, messages, store visits, product shares, consultations, and more.

Interaction Type System

Every interaction has a context discriminator field that determines which processor handles enrichment when the interaction is read back. This is the core abstraction that allows interactions to carry typed, context-specific metadata.

Interaction Channels

Call · SMS · Email · WhatsApp · WeChat · Line · LineWorks · Kakao · Postal · Action

Salesforce Storage Model

Interactions are stored using Salesforce standard and custom objects:

Create Interaction Flow

Read Interactions with Enrichment


API Versioning Strategy

The platform manages breaking changes through explicit API versioning in the URL path:

VersionStatusResponse Format
v1Deprecated, kept for compatibilityJSend wrapper: { status, data: { result } }
v2Current (most endpoints)Raw JSON payload
v3Latest (inner source library)Raw JSON, full RESTful CRUD

V1 endpoints are implemented as thin wrappers that call V2 internally and wrap the response — no duplicated business logic.


Contract-First Development

All APIs are defined in OpenAPI YAML specs before implementation. Models are generated from the contracts, and runtime validation enforces the spec on every request and response.

Each brand maintains its own contracts submodule. This means brands can enable or disable features simply by including or excluding endpoints from their spec. The generated TypeScript models and the runtime validator both derive from the same source of truth.


Authentication & Authorization

Key design choices:

DecisionRationale
JWT validation at Istio, not in servicesServices don't need to implement auth; one place to update if Okta config changes
TrustedJwtAuthGuardTrusts that Istio already validated the token; extracts CA identity from forwarded headers — no redundant crypto overhead
OAuth 2.0 PKCE for mobilePrevents authorization code interception attacks; no client secret needed in the mobile app binary
Istio mTLS between servicesZero plain-text traffic inside the cluster; service-to-service calls are mutually authenticated without application code changes

Infrastructure & GitOps

The entire deployment lifecycle is managed through GitOps using FluxCD. Git is the single source of truth for what runs in every environment.

Environments

EnvironmentGit BranchTrigger
devdevelopAuto-deploy on every merge
stagingmainAuto-deploy on merge to main
productiongit tag vX.X.XManual promotion via tag
sandboxfeature branchPer-developer sandboxes

Per-Service Kubernetes Resources

Each of the 18 microservices is deployed with:

  • Deployment — multi-stage Docker build (Node alpine), node dist/main
  • HPA — auto-scales from 2 to 5 replicas on CPU/memory
  • ExternalSecret — pulls credentials from GCP Secret Manager; zero secrets in Git
  • Istio VirtualService — per-route JWT auth policies and traffic rules
  • CronJobsproduct-catalog-sync (nightly) and translations-compilation (hourly)

Tech Stack

LayerTechnology
Backend frameworkNestJS (Node.js, TypeScript)
Primary data storeSalesforce CRM (SOQL via jsforce)
CachingRedis (ioredis)
Product searchElasticsearch
Real-time / eventsFirestore, Firebase Cloud Messaging
PaymentsAdyen
Video / appointmentsBambuser, Booxi, SightCall
AuthOkta (OAuth 2.0, PKCE), Istio JWT validation
Service meshIstio (mTLS, JWT policies, traffic management)
Container orchestrationKubernetes (GKE), FluxCD GitOps
SecretsGCP Secret Manager + External Secrets Operator
CI/CDGitHub Actions (lint, test, Docker build, NPM publish)
API validationexpress-openapi-validator, openapi-generator-cli
SF batchingsf-composite-call
Loggingcloud-pine, nestjs-pino (structured JSON)

Retrospective — What I Would Do Differently

1. Event-Source Interactions from Day One

What we have: Interactions are Salesforce Task records — mutable, overwritten on update.

The problem: No audit trail. When a timeline entry ended up in an unexpected state, debugging meant correlating Salesforce logs and service logs across the async enrichment pipeline.

What I'd do now: Event-source the interaction aggregate. Emit InteractionCreated, InteractionValidated, InteractionDeleted events and derive current state from the stream. Trivial audit log, time-travel debugging, and unlocks event-driven downstream consumers (analytics, recommendations).

2. Decouple Enrichment from the Read Path

What we have: When a CA reads their interaction timeline, the service synchronously calls Products, Curations, and Orders services to enrich each interaction — coupling read latency to N downstream service response times.

The problem: If Products is slow, the entire timeline is slow. Under load, this amplifies tail latencies.

What I'd do now: Enrich interactions at write time, not read time. When an interaction is created, publish an InteractionCreated event. An async enrichment worker resolves the metadata and writes it back. The read path is then a single Salesforce query — fast and isolated.

3. Consumer-Driven Contract Testing

What we have: Brands' OpenAPI contracts are validated at runtime, but there are no automated contract tests between the mobile app and the backend services.

The problem: A backend service can change a response field, the contract gets updated, but the mobile app — which hasn't updated yet — breaks silently in production.

What I'd do now: Implement consumer-driven contract testing (Pact). The mobile app publishes what it expects from each endpoint. CI fails on the backend if a change would break any registered consumer — before it ever reaches staging.

4. GraphQL or BFF for the 360° View

What we have: The client360 service orchestrates calls to 6+ downstream services, assembles the response, and returns one big payload.

The problem: The mobile app sometimes only needs a subset of the 360 view (just the KPI cards, or just the timeline). But it always gets the full payload, wasting bandwidth and increasing cold-start latency on the app screen.

What I'd do now: A lightweight Backend for Frontend (BFF) layer or a GraphQL gateway that lets the mobile app request exactly the fields it needs. Reduces payload size, enables parallel field resolution, and makes the client360 service stateless and composable.

5. Observability Before the First Deploy

What we have: Structured logging (pino) was added, but distributed tracing across the enrichment pipeline and cross-service calls was an afterthought.

The problem: A slow interaction read involves 4+ services. Finding which hop added latency required manually correlating trace IDs across separate log streams.

What I'd do now: OpenTelemetry instrumentation from day one — automatic trace propagation through HTTP calls and Salesforce composite calls. A single Jaeger/Cloud Trace view showing the full request waterfall, with per-hop latency highlighted immediately.


Impact & Key Metrics

  • Delivered across 18 microservices with 93 API endpoints serving multiple luxury brands simultaneously
  • Designed and implemented the interaction type processor system — a plugin architecture supporting 12 distinct interaction contexts with zero coupling between types
  • Built the inner source library contribution pipeline: from monorepo structure to versioned NPM publication and brand-side integration
  • Implemented Salesforce composite call batching — reducing a 5-round-trip interaction create to a single HTTP call to Salesforce
  • Architected multi-brand support where each Maison independently controls its feature set via OpenAPI contracts — enabling or disabling features without touching shared code
  • Platform runs with HPA auto-scaling (2–5 replicas per service) and zero secrets in Git via GCP Secret Manager + External Secrets Operator

Built for the people who make luxury feel personal.