Loccitane - Replace Legacy Software

From Fragmentation to Fluidity: Building Next-Gen Infrastructure for Global Retail Teams

DATABASESSERVICESGATEWAYCLIENTCEOAPI Gateway / Auth LayerCustomer ServiceMessaging ServiceProduct ServiceOther Services
Customer DB
Customer DB
Messaging DB
Messaging DB
Products DB
Products DB
Other Services DB
Other Services DB
Text is not SVG - cannot display
PROCESS HIGHLIGHTS

PART ONE - EXPLORATION

Challenge

L’Occitane’s in-store employees relied on fragmented, outdated legacy systems that created silos in customer and product data. Staff had to jump between multiple platforms to manage customer journeys, pricing rules, and internal communications—resulting in inconsistent service and reduced efficiency.

Opportunity

This project presented a unique chance to completely reimagine how global retail staff interact with L’Occitane’s digital infrastructure. By consolidating customer service, product management, and messaging into scalable, microservice-based systems, we empowered retail teams to work smarter, not harder.

Timeline

Week 1–2: Research & Discovery

Week 3–4: Design & Stakeholder Review

Week 5–10: Engineering & Iteration

Week 11–12: QA, Integration & Rollout

Disciplines

Software Engineering

Product Design

DevOps

Data Engineering

Systems Architecture

UX Research

Responsibilities

Architected microservices to replace legacy tools

Led ETL development for customer data synchronization

Designed secure file upload architecture for MMS

Created real-time messaging systems with scalable Redis + Twilio integration

Tools

Node.js

Redis

PostgreSQL

AWS

Twilio

Docker

BACKGROUND

The Why

Legacy systems slowed down operations, led to data inconsistencies, and created a disjointed employee experience. We needed a system that was fast, reliable, and scalable—paving the way for seamless customer engagement and operational efficiency.

The Process

Research

Collaborated with store employees

Desk Research

Competitor Analysis

Synthesis

Identified consistent pain themes: data duplication, poor messaging feedback, and hard-to-use tools.

Ideation

Brainstormed modular services: Customer Service, Messaging, and Product Management—each with clear SLAs and scaling autonomy.

Final Designs

Delivered interactive dashboards, direct-to-storage file uploads, and a real-time messaging framework via WebSockets.

Reflection

Looking back, the shift from legacy to modular gave the business resilience, flexibility.

The foundation for future AI integrations

RESEARCH

Desk Research

Based on sources, in particular The Digital Project Manager, here were the three main pain points:

Workflow Interruptions

Needing to switch platforms frequently broke user flow.

Limited Real-Time Collaboration

Messaging lacked urgency and reliability.

Accessibility and Usability Challenges

Systems were not intuitive or optimized for different devices.

RESEARCH

Competitor Analysis

We benchmarked against industry-leading retailers with modern omnichannel platforms:

Sephora

Pros

·

·

Unified Omnichannel Experience: Seamless integration between mobile, online, and in-store shopping, supported by a powerful loyalty program.

·

·

Data-Driven Personalization: Uses customer data to offer tailored product recommendations and content.

Cons

·

·

Complex Backend Systems: The vast ecosystem sometimes leads to slower load times and data inconsistencies.

·

·

Overloaded User Interface: Feature-rich UI can overwhelm new users or employees unfamiliar with tech-heavy platforms.

The Body Shop

Pros

·

·

Strong Ethical Branding: Promotes transparency and social activism, which resonates with customers.

·

·

Simplified Product Information: Offers clean, accessible digital product data across platforms.

Cons

·

·

Limited Real-Time Communication Tools: Lacks robust in-app messaging or collaborative infrastructure for store teams.

·

·

Under-Utilized Customer Data: Doesn’t personalize shopping experiences as deeply as competitors like Sephora.

Lush

Pros

·

·

Minimalist Digital Experience: Prioritizes ease of use with a straightforward site and app layout.

·

·

Transparent Supply Chain Integration: Offers insights into sourcing and sustainability practices.

Cons

·

·

Lack of Customer Support Features: Minimal messaging or live chat options make issue resolution slow.

·

·

Limited Loyalty Infrastructure: No advanced loyalty system to incentivize repeat purchases.

Synthesis

User Persona

I conducted a competitor analysis to understand the pros and cons of current collaboration and design tools, focusing on how they facilitate online teamwork and ideation.

Lucas | He/Him | 28

LOCATION

Los Angeles, CA

EDUCATION

Teacher

EXPERIENCE

+5 Years

Goals

·

·

Improve customer experience

·

·

Access accurate data quickly

·

·

Collaborate with HQ teams

Needs

·

·

Real-time updates on product availability

·

·

Centralized customer insights

·

·

Clear feedback when sending messages

Pain Points

·

·

Struggles to switch between legacy systems

·

·

Messaging often fails silently

·

·

Can’t upload media for customer support interactions

Synthesis

User Journey

Next is the user journey, which focuses on Emma’s current experience as a product designer collaborating remotely, highlighting the challenges she encounters. It examines how fragmented workflows, limited real-time collaboration, and the absence of intuitive 3D tools hinder her ability to effectively create and share engaging solutions.

Lucas

Scenario:

wants to collaborate effectively on a project involving interactive 3D content creation.

Starting

Logs into the legacy system, ready to start a product feedback project.

Trying

Attempts to share updates via SMS to HQ but receives no delivery confirmation.

Conflicting

Tries checking product info via Salesforce, but data is outdated.

Quitting

Frustrated, he emails HQ manually, delaying progress. Then quits.

IDEATION

Developing a Solution

Systems Created

Customer Service

Handling 70k messages weekly, completely replacing legacy messaging service, allows for UX by having better error handling with specific error messages, and allows to send/receive MMS messages.

Messaging Service

Managing over 5 million customer records, ensuring that self and other critical systems are up-to-date with the latest customer information, becoming the new source of truth in the company.

Product Service

Syncing from multiple sources including Salesforce, understand the complex rules set such as promotional pricing, properly save and query the products.

Customer Service

Handling 70k messages weekly, completely replacing legacy messaging service, allows for UX by having better error handling with specific error messages, and allows to send/receive MMS messages.

Messaging Service

Managing over 5 million customer records, ensuring that self and other critical systems are up-to-date with the latest customer information, becoming the new source of truth in the company.

Product Service

Syncing from multiple sources including Salesforce, understand the complex rules set such as promotional pricing, properly save and query the products.

Customer Service

Handling 70k messages weekly, completely replacing legacy messaging service, allows for UX by having better error handling with specific error messages, and allows to send/receive MMS messages.

Messaging Service

Managing over 5 million customer records, ensuring that self and other critical systems are up-to-date with the latest customer information, becoming the new source of truth in the company.

Product Service

Syncing from multiple sources including Salesforce, understand the complex rules set such as promotional pricing, properly save and query the products.

Modular System Architecture for Scalable Enterprise Services:

This architecture improves scalability, flexibility, and security while reducing dependencies between different business functions. It allows for seamless integration of new services and efficient handling of enterprise operations.

DATABASESSERVICESGATEWAYCLIENTCEOAPI Gateway / Auth LayerCustomer ServiceMessaging ServiceProduct ServiceOther Services
Customer DB
Customer DB
Messaging DB
Messaging DB
Products DB
Products DB
Other Services DB
Other Services DB
Text is not SVG - cannot display

Secure File Upload for MMS Messaging:

1) Client Requests to upload

2) Server responds with a presigned url to upload directly to blob storage: a) Avoids processing file on a microservice level. b) Pre-signed URL enforces secure, time-limited access to storage without exposing credentials

Secure File Upload (Pre-Signed URLs) – Python (AWS S3)

import boto3
from datetime import datetime, timedelta
from flask import Flask, jsonify, request

app = Flask(__name__)
S3_BUCKET = "loccitane-mms-uploads"

@app.route('/generate-presigned-url', methods=['POST'])
def generate_presigned_url():
    file_name = request.json.get('filename')
    file_type = request.json.get('filetype')
    
    s3_client = boto3.client(
        's3',
        aws_access_key_id=os.getenv('AWS_ACCESS_KEY'),
        aws_secret_access_key=os.getenv('AWS_SECRET_KEY')
    )
    
    presigned_url = s3_client.generate_presigned_url(
        'put_object',
        Params={
            'Bucket': S3_BUCKET,
            'Key': f"uploads/{datetime.utcnow().isoformat()}_{file_name}",
            'ContentType': file_type
        },
        ExpiresIn=3600  # 1-hour expiry
    )
    
    return jsonify({
        "url": presigned_url,
        "metadata": {"service": "messaging", "upload_id": str(uuid.uuid4())}
    })

# iOS app uploads directly to S3 via this URL, bypassing backend processing

import boto3
from datetime import datetime, timedelta
from flask import Flask, jsonify, request

app = Flask(__name__)
S3_BUCKET = "loccitane-mms-uploads"

@app.route('/generate-presigned-url', methods=['POST'])
def generate_presigned_url():
    file_name = request.json.get('filename')
    file_type = request.json.get('filetype')
    
    s3_client = boto3.client(
        's3',
        aws_access_key_id=os.getenv('AWS_ACCESS_KEY'),
        aws_secret_access_key=os.getenv('AWS_SECRET_KEY')
    )
    
    presigned_url = s3_client.generate_presigned_url(
        'put_object',
        Params={
            'Bucket': S3_BUCKET,
            'Key': f"uploads/{datetime.utcnow().isoformat()}_{file_name}",
            'ContentType': file_type
        },
        ExpiresIn=3600  # 1-hour expiry
    )
    
    return jsonify({
        "url": presigned_url,
        "metadata": {"service": "messaging", "upload_id": str(uuid.uuid4())}
    })

# iOS app uploads directly to S3 via this URL, bypassing backend processing

import boto3
from datetime import datetime, timedelta
from flask import Flask, jsonify, request

app = Flask(__name__)
S3_BUCKET = "loccitane-mms-uploads"

@app.route('/generate-presigned-url', methods=['POST'])
def generate_presigned_url():
    file_name = request.json.get('filename')
    file_type = request.json.get('filetype')
    
    s3_client = boto3.client(
        's3',
        aws_access_key_id=os.getenv('AWS_ACCESS_KEY'),
        aws_secret_access_key=os.getenv('AWS_SECRET_KEY')
    )
    
    presigned_url = s3_client.generate_presigned_url(
        'put_object',
        Params={
            'Bucket': S3_BUCKET,
            'Key': f"uploads/{datetime.utcnow().isoformat()}_{file_name}",
            'ContentType': file_type
        },
        ExpiresIn=3600  # 1-hour expiry
    )
    
    return jsonify({
        "url": presigned_url,
        "metadata": {"service": "messaging", "upload_id": str(uuid.uuid4())}
    })

# iOS app uploads directly to S3 via this URL, bypassing backend processing

Customer Service ETL Pipelines:

1) Customer Service becoming the new source of truth while updating and maintaining data integrity of other core systems (Cegid and Legacy systems)

2) Avoiding parallel Ingress/Egress jobs running by orchestrating and triggering jobs programmatically

INGRESS
INGRESS
API GATEWAY
EGRESS
EGRESS
CUSTOMER SERVICE
INGRESS
INGRESS
INGRESS
INGRESS
INGRESS
INGRESS
EGRESS
EGRESS
SNOWFLAKECEGIDSALESFORCE
Text is not SVG - cannot display

ETL Pipeline Orchestration (Customer Service) – Go

package main

import (
	"context"
	"fmt"
	"time"
	"github.com/go-redis/redis/v8"
)

var rdb = redis.NewClient(&redis.Options{Addr: "redis:6379"})

func triggerETLJob(source string, target string) error {
	ctx := context.Background()
	lockKey := fmt.Sprintf("lock:etl:%s:%s", source, target)
	
	// Acquire distributed lock to prevent parallel jobs
	locked, err := rdb.SetNX(ctx, lockKey, "1", 30*time.Minute).Result()
	if err != nil || !locked {
		return fmt.Errorf("ETL job already running for %s→%s", source, target)
	}
	defer rdb.Del(ctx, lockKey)

	// Example: Sync Salesforce → Customer DB
	if source == "salesforce" {
		if err := syncSalesforceToCustomerDB(ctx); err != nil {
			return err
		}
	}
	return nil
}

func syncSalesforceToCustomerDB(ctx context.Context) error {
	// Pseudocode: Batch process 10K records at a time
	for {
		records := salesforceAPI.Fetch("SELECT Id,Email FROM Contact LIMIT 10000")
		if len(records) == 0 { break }
		
		if err := customerDB.BulkUpsert(records); err != nil {
			rdb.Publish(ctx, "etl_errors", fmt.Sprintf("Salesforce sync failed: %v", err))
			return err
		}
	}
	rdb.Publish(ctx, "etl_logs", "Salesforce→CustomerDB sync completed")
	return nil
}
package main

import (
	"context"
	"fmt"
	"time"
	"github.com/go-redis/redis/v8"
)

var rdb = redis.NewClient(&redis.Options{Addr: "redis:6379"})

func triggerETLJob(source string, target string) error {
	ctx := context.Background()
	lockKey := fmt.Sprintf("lock:etl:%s:%s", source, target)
	
	// Acquire distributed lock to prevent parallel jobs
	locked, err := rdb.SetNX(ctx, lockKey, "1", 30*time.Minute).Result()
	if err != nil || !locked {
		return fmt.Errorf("ETL job already running for %s→%s", source, target)
	}
	defer rdb.Del(ctx, lockKey)

	// Example: Sync Salesforce → Customer DB
	if source == "salesforce" {
		if err := syncSalesforceToCustomerDB(ctx); err != nil {
			return err
		}
	}
	return nil
}

func syncSalesforceToCustomerDB(ctx context.Context) error {
	// Pseudocode: Batch process 10K records at a time
	for {
		records := salesforceAPI.Fetch("SELECT Id,Email FROM Contact LIMIT 10000")
		if len(records) == 0 { break }
		
		if err := customerDB.BulkUpsert(records); err != nil {
			rdb.Publish(ctx, "etl_errors", fmt.Sprintf("Salesforce sync failed: %v", err))
			return err
		}
	}
	rdb.Publish(ctx, "etl_logs", "Salesforce→CustomerDB sync completed")
	return nil
}
package main

import (
	"context"
	"fmt"
	"time"
	"github.com/go-redis/redis/v8"
)

var rdb = redis.NewClient(&redis.Options{Addr: "redis:6379"})

func triggerETLJob(source string, target string) error {
	ctx := context.Background()
	lockKey := fmt.Sprintf("lock:etl:%s:%s", source, target)
	
	// Acquire distributed lock to prevent parallel jobs
	locked, err := rdb.SetNX(ctx, lockKey, "1", 30*time.Minute).Result()
	if err != nil || !locked {
		return fmt.Errorf("ETL job already running for %s→%s", source, target)
	}
	defer rdb.Del(ctx, lockKey)

	// Example: Sync Salesforce → Customer DB
	if source == "salesforce" {
		if err := syncSalesforceToCustomerDB(ctx); err != nil {
			return err
		}
	}
	return nil
}

func syncSalesforceToCustomerDB(ctx context.Context) error {
	// Pseudocode: Batch process 10K records at a time
	for {
		records := salesforceAPI.Fetch("SELECT Id,Email FROM Contact LIMIT 10000")
		if len(records) == 0 { break }
		
		if err := customerDB.BulkUpsert(records); err != nil {
			rdb.Publish(ctx, "etl_errors", fmt.Sprintf("Salesforce sync failed: %v", err))
			return err
		}
	}
	rdb.Publish(ctx, "etl_logs", "Salesforce→CustomerDB sync completed")
	return nil
}

Messaging Service Architecture:

1) Used Twilio as SMS provider

2) Utilized websockets for real time communication with the connected client

3) Used Redis to handle distributed websocket servers so it can route data to the correct websocket connections

API GATEWAYWEBSOCKETS SERVERiOS APPLICATIONBLOB STORAGEMESSAGIN SERVICEMESSAGIN SERVICEMESSAGIN SERVICE
MESSAGING DB
MESSAGING DB
API GATEWAYWEBSOCKETS SERVER
OUTGOING SMS
OUTGOING SMS
API GATEWAYWEBSOCKETS SERVER
INCOMING SMS
INCOMING SMS
TWILIO SMS PROVIDER
TWILIO SMS PROVIDER
Text is not SVG - cannot display

Messaging with Redis & WebSockets – Node.js

const WebSocket = require('ws');
const redis = require('redis');

// Track connected clients (iOS app sessions)
const clients = new Map();

// Subscribe to Redis channel for incoming Twilio messages
const redisSub = redis.createClient({ url: 'redis://redis:6379' });
redisSub.subscribe('twilio_incoming');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws, req) => {
  const userId = req.headers['user-id'];
  clients.set(userId, ws); // Map user ID to WebSocket

  ws.on('close', () => clients.delete(userId));
});

// Relay Twilio SMS to the correct user's WebSocket
redisSub.on('message', (channel, msg) => {
  const { userId, text } = JSON.parse(msg);
  const client = clients.get(userId);
  if (client) {
    client.send(JSON.stringify({
      type: "sms",
      content: text,
      timestamp: Date.now()
    }));
  }
});

// Example Twilio webhook handler (Express.js)
app.post('/twilio-webhook', (req, res) => {
  const msg = {
    userId: req.body.UserId, // Extracted from Twilio payload
    text: req.body.Body
  };
  redisSub.publish('twilio_incoming', JSON.stringify(msg));
  res.status(200).end();
});
const WebSocket = require('ws');
const redis = require('redis');

// Track connected clients (iOS app sessions)
const clients = new Map();

// Subscribe to Redis channel for incoming Twilio messages
const redisSub = redis.createClient({ url: 'redis://redis:6379' });
redisSub.subscribe('twilio_incoming');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws, req) => {
  const userId = req.headers['user-id'];
  clients.set(userId, ws); // Map user ID to WebSocket

  ws.on('close', () => clients.delete(userId));
});

// Relay Twilio SMS to the correct user's WebSocket
redisSub.on('message', (channel, msg) => {
  const { userId, text } = JSON.parse(msg);
  const client = clients.get(userId);
  if (client) {
    client.send(JSON.stringify({
      type: "sms",
      content: text,
      timestamp: Date.now()
    }));
  }
});

// Example Twilio webhook handler (Express.js)
app.post('/twilio-webhook', (req, res) => {
  const msg = {
    userId: req.body.UserId, // Extracted from Twilio payload
    text: req.body.Body
  };
  redisSub.publish('twilio_incoming', JSON.stringify(msg));
  res.status(200).end();
});
const WebSocket = require('ws');
const redis = require('redis');

// Track connected clients (iOS app sessions)
const clients = new Map();

// Subscribe to Redis channel for incoming Twilio messages
const redisSub = redis.createClient({ url: 'redis://redis:6379' });
redisSub.subscribe('twilio_incoming');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws, req) => {
  const userId = req.headers['user-id'];
  clients.set(userId, ws); // Map user ID to WebSocket

  ws.on('close', () => clients.delete(userId));
});

// Relay Twilio SMS to the correct user's WebSocket
redisSub.on('message', (channel, msg) => {
  const { userId, text } = JSON.parse(msg);
  const client = clients.get(userId);
  if (client) {
    client.send(JSON.stringify({
      type: "sms",
      content: text,
      timestamp: Date.now()
    }));
  }
});

// Example Twilio webhook handler (Express.js)
app.post('/twilio-webhook', (req, res) => {
  const msg = {
    userId: req.body.UserId, // Extracted from Twilio payload
    text: req.body.Body
  };
  redisSub.publish('twilio_incoming', JSON.stringify(msg));
  res.status(200).end();
});
IDEATION

Future Explorations - AI

This semester, I’m taking INST728F as the only undergraduate student taking this graduate course focused on Generative AI in UX and how effective prompting can help the UX process. For part of my project, I wanted to further explore how my effective prompting can help me discover how I can create the best all in one tool that will allow the design brainstorming process and team work efforts the most effective it can be.

Prompting Highlights: Drawing from lessons in class on emotion, behavior, and crafting specific prompts for desired results, I wanted to showcase the two main uses I had for ChatGPT: supporting user task flows and assisting with coding in SwiftUI as I transitioned from design to development.

For inspiration here are a few tools I’ve taken inspiration from:

Apple Intelligence

·

·

UI-aware suggestions for contextual task automation

ChatGPT

·

·

Smart documentation and meeting summarization for store managers

IDEATION

Future Explorations - AI

Conversational Interface Layer

·

·

Implement a voice-enabled assistant that can guide new employees through systems with natural language prompts.

AI-powered Inventory Predictions

·

·

Leverage historical data and real-time product views to forecast stock needs at the store level.

Emotion-aware Messaging AI

·

·

Train AI to detect sentiment in messages and suggest empathetic responses for better customer interactions.