Building Buy Online, Pick-Up In Store at lululemon
- 01 Jul 2023 |
- 02 Mins read
At lululemon, I led the API-layer team that launched Buy Online, Pick-Up In Store (BOPIS). This feature required real-time inventory synchronization, store location services, and seamless checkout integration.
The Challenge
BOPIS requires:
- Real-time inventory visibility
- Store location and availability
- Seamless checkout flow
- Order management system
- Customer notifications
Architecture
We built:
- Inventory API: Real-time store inventory
- Location Service: Store finder and availability
- Order API: Create and manage pickup orders
- Notification Service: Customer updates
- Store Integration: Connect with POS systems
Inventory Synchronization
class StoreInventoryService {
async getStoreInventory(
productId: string,
storeId: string
): Promise<InventoryStatus> {
// Check cache first
const cached = await this.cache.get(`inventory:${storeId}:${productId}`);
if (cached) {
return cached;
}
// Fetch from store system
const inventory = await this.storeSystem.getInventory(storeId, productId);
// Cache for 5 minutes
await this.cache.set(
`inventory:${storeId}:${productId}`,
inventory,
300
);
return inventory;
}
async reserveInventory(
productId: string,
storeId: string,
quantity: number
): Promise<ReservationResult> {
// Reserve inventory in store system
const reservation = await this.storeSystem.reserve(
storeId,
productId,
quantity
);
// Invalidate cache
await this.cache.delete(`inventory:${storeId}:${productId}`);
return reservation;
}
}
Store Location Service
interface Store {
id: string;
name: string;
address: Address;
coordinates: Coordinates;
hours: StoreHours;
inventory: Map<string, number>;
}
class StoreLocationService {
async findNearbyStores(
location: Coordinates,
radius: number
): Promise<Store[]> {
const stores = await this.storeRepository.findNearby(location, radius);
return stores.map(store => ({
...store,
distance: this.calculateDistance(location, store.coordinates),
})).sort((a, b) => a.distance - b.distance);
}
async getStoresWithProduct(
productId: string,
location: Coordinates
): Promise<Store[]> {
const stores = await this.findNearbyStores(location, 25); // 25 miles
const availableStores = await Promise.all(
stores.map(async (store) => {
const inventory = await this.inventoryService.getStoreInventory(
productId,
store.id
);
return inventory.available > 0 ? store : null;
})
);
return availableStores.filter(Boolean) as Store[];
}
}
Order Creation
class BOPISOrderService {
async createPickupOrder(
customerId: string,
items: CartItem[],
storeId: string
): Promise<PickupOrder> {
// Validate store availability
for (const item of items) {
const inventory = await this.inventoryService.getStoreInventory(
item.productId,
storeId
);
if (inventory.available < item.quantity) {
throw new InsufficientInventoryError(item.productId, storeId);
}
}
// Reserve inventory
const reservations = await Promise.all(
items.map(item =>
this.inventoryService.reserveInventory(
item.productId,
storeId,
item.quantity
)
)
);
// Create order
const order = await this.orderRepository.create({
customerId,
storeId,
items,
type: 'pickup',
status: 'pending',
reservations: reservations.map(r => r.id),
});
// Send notifications
await this.notificationService.sendOrderConfirmation(order);
return order;
}
}
Results
- Successful BOPIS launch
- Real-time inventory accuracy
- Seamless customer experience
- Increased store foot traffic
- Better inventory utilization
"BOPIS bridges online and physical retail."
Lessons Learned
- Real-time inventory is critical
- Cache aggressively but invalidate properly
- Handle inventory conflicts gracefully
- Provide clear customer communication
- Monitor store fulfillment rates