system-design52 min read

How to Design an E-Commerce Product Catalog & Search System — A Senior+ Guide | Ayodhyya

How to Design an E-Commerce Product Catalog & Search System

Building an Amazon-Scale System — Catalog, Search, Filtering, Pricing, Inventory, and Recommendations

Senior+ System Design Guide 10,000+ Words 20 Deep-Dive Sections C# · Mermaid · Real-World Case Studies

1. Introduction & Why E-Commerce Search is Hard

E-commerce product catalog and search systems are among the most complex and revenue-critical systems in modern technology. Amazon processes over 3 billion search queries per day, maintains a catalog of hundreds of millions of products across dozens of categories, and generates roughly 35% of its total revenue directly from search and browse experiences. A single poorly-ranked search result or a slow page load can cost millions in lost sales annually. At this scale, every millisecond of latency and every percentage point of relevance matters.

The challenge of designing such a system is multifaceted. Unlike a simple database query, an e-commerce search must balance dozens of competing concerns: textual relevance of product titles and descriptions, brand affinity, price competitiveness, inventory availability, seller ratings, delivery speed, personalization based on user history, and promotional placement. The system must return results in under 200 milliseconds while scoring and ranking millions of candidate products in real time.

Consider the diversity of a platform like Amazon. A single search for "headphones" must return results spanning over-ear, in-ear, wireless, wired, noise-cancelling, gaming, and studio varieties from thousands of brands at wildly different price points. The system must understand that "noise-cancelling" modifies "headphones," that "Sony" is a brand, that "$29.99" is a price, and that the user who searched previously bought Apple products might prefer AirPods Max. It must also factor in which products are in stock, which have Prime delivery, and which sellers have the best ratings.

Key Insight: An e-commerce search system is not just a search engine — it is a real-time decision engine that combines full-text search, faceted filtering, personalization, inventory checks, pricing logic, promotion rules, and machine learning ranking into a single query that must complete in under 200ms. The complexity rivals that of any large-scale distributed system.

The business impact of search quality cannot be overstated. Research from Google and Microsoft shows that the first result on a search page receives 30-40% of all clicks, while results beyond the first page receive less than 1% combined. For e-commerce, poor search relevance directly translates to lost revenue. Amazon has publicly stated that every 100ms of additional latency costs them 1% in sales. A system that serves billions of queries per day with sub-second latency and high relevance is therefore not a nice-to-have — it is the foundation of the business.

The Core Technical Challenges

Several technical dimensions make e-commerce search uniquely challenging compared to general web search. First, the product catalog is semi-structured: products have varying attributes depending on their category (a laptop has RAM and CPU specs; a dress has size and color), requiring a flexible schema that can accommodate millions of attribute combinations. Second, the data is highly dynamic: prices change hundreds of times per day for competitive items, inventory fluctuates in real time as warehouses fulfill orders, and new products are added continuously by thousands of sellers.

Third, the ranking function must combine hundreds of signals in real time. Traditional web search can rely heavily on link analysis (PageRank), but e-commerce search must consider product popularity, conversion rate, review score, delivery speed, seller reliability, stock levels, margin, and personalization — all within a tight latency budget. Fourth, the system must handle extreme traffic spikes: Black Friday and Prime Day can see 10-50x normal traffic within minutes, requiring the system to scale horizontally without degrading search quality.

Finally, there is the challenge of catalog consistency. When a seller updates a product description, changes a price, or reports a stock-out, the change must propagate to the search index quickly enough that users do not see stale data — but fast enough that the index is not constantly thrashing. The eventual consistency model must be carefully tuned to balance freshness against index stability, and the system must handle contradictions gracefully (for example, a product appears in search results but is out of stock by the time the user clicks through).

Scale in Perspective

To appreciate the scale, consider Amazon's public disclosures and industry estimates. The Amazon product catalog contains over 350 million active products across categories ranging from books to automobiles. On a typical day, the platform handles over 3 billion search and browse requests. During peak events like Prime Day, this can spike to over 10 billion requests in a 48-hour period. The search index itself is several terabytes, and the underlying product data (including images, descriptions, and attributes) exceeds a petabyte.

The search indexing pipeline must process millions of product updates per hour — price changes, inventory updates, new product listings, description edits, and review score recalculations. These updates must be reflected in the search index within seconds to minutes, depending on the type of change. The system must also handle the deletion of millions of products per month (discontinued items, policy violations, seller departures) while maintaining index integrity.

Interview Tip: When asked to design an e-commerce search system, immediately establish the scale. Ask the interviewer whether you are designing for a small marketplace (10M products, 100K QPS) or an Amazon-scale platform (350M products, 1M+ QPS). The architectural decisions differ dramatically — a small marketplace can run on a single Elasticsearch cluster, while Amazon-scale requires custom-built distributed indices, real-time feature stores, and ML-based ranking pipelines.

2. Functional & Non-Functional Requirements

Functional Requirements

  1. Product Search: Users can search for products using free-text queries. The system must return relevant results ranked by a combination of relevance, popularity, price, and personalization signals. Search should support natural language queries, typos, synonyms, and partial matches.
  2. Browse & Category Navigation: Users can browse products by category and subcategory hierarchies. The system must support multi-level category trees (Electronics → Computers → Laptops → Gaming Laptops) with smooth drill-down and breadcrumb navigation.
  3. Faceted Filtering: Users can refine results using faceted filters including price range, brand, rating, delivery speed, seller, color, size, and category-specific attributes (for example, RAM for laptops, screen size for TVs). Filters must dynamically update based on the current result set.
  4. Product Detail Pages: Users can view comprehensive product information including title, description, images, price, availability, seller information, reviews and ratings, specifications, and related products.
  5. Inventory Visibility: The system must display real-time or near-real-time stock status. Users should see whether a product is in stock, how many units remain (for low-stock items), and estimated delivery dates.
  6. Price & Promotion Display: The system must display current pricing including regular price, sale price, discount percentage, coupon availability, and promotional badges. Pricing must be accurate and consistent across search results, product pages, and cart.
  7. Recommendations: The system must provide "Customers who viewed this also viewed," "Frequently bought together," "Similar items," and personalized "Recommended for you" product suggestions.
  8. Review & Rating System: Users can leave star ratings, written reviews, and upload photos. The system must aggregate ratings, display rating distributions, and highlight verified purchases.
  9. Cart & Wishlist: Users can add products to a shopping cart or wishlist. The cart must validate inventory availability and pricing at checkout time.
  10. Order Management: Users can place orders, view order history, track shipments, and initiate returns. The system must coordinate inventory deduction, payment processing, and fulfillment.

Non-Functional Requirements

RequirementTargetRationale
Search Latency (p99)< 200msUsers abandon search results pages that take longer than 500ms. Amazon targets sub-200ms for search queries.
Product Detail Page Latency (p99)< 300msPDP is the most revenue-critical page. Latency directly impacts conversion rates.
Throughput100K+ QPS peakMust handle traffic spikes during sales events (Black Friday, Prime Day) with 10-50x normal load.
Availability99.99% (52 min/year downtime)E-commerce downtime translates directly to lost revenue. Every minute of outage costs significant sales.
Search Index Freshness< 30 seconds for price/stock changesUsers must not see stale pricing or stock information. Price changes and stock-outs must propagate quickly.
Data Durability99.999999999% (11 nines)Product catalog data is the business's core asset. Loss is unacceptable.
Consistency ModelEventual consistency for search; strong consistency for cart/checkoutSearch can tolerate seconds of staleness; cart and payment cannot tolerate inconsistencies.
PersonalizationResults personalized per user within 50ms overheadPersonalization improves conversion by 15-30% but must not significantly impact latency.
Design Principle: In e-commerce, "correct" means different things for different operations. Search results can be slightly stale (eventual consistency), product pricing must be accurate at display time (strong read consistency), and cart/checkout must be strongly consistent with inventory reservation. Design your consistency model per operation, not globally.

3. Capacity Estimation & Back-of-Envelope

Traffic Estimation

Let us estimate the capacity requirements for a large-scale e-commerce platform similar to Amazon. These numbers serve as the foundation for all subsequent architectural decisions.

MetricDailyPer Second (avg)Per Second (peak, 10x)
Search Queries3 billion35,000350,000
Product Detail Page Views5 billion58,000580,000
Category Browse Requests2 billion23,000230,000
Cart Operations500 million5,80058,000
Order Placements100 million1,16011,600

Storage Estimation

The product catalog is the largest data store. With 350 million active products and an average of 10 KB of structured data per product (title, description, attributes, pricing, seller info), the raw catalog data is approximately 3.5 TB. Adding product images at an average of 500 KB per image with 5 images per product gives us 875 TB of image data. The search index, which stores tokenized and analyzed versions of product data with additional computed fields, adds another 2-5 TB depending on the number of analyzers and stored fields.

Text
Product Catalog Data:
  350M products x 10 KB avg = 3.5 TB (structured data)
  350M products x 5 images x 500 KB = 875 TB (image data)
  Search index (tokenized + analyzed) = 2-5 TB
  Review data: 1B reviews x 2 KB = 2 TB
  User profiles: 300M users x 1 KB = 300 GB
  Order history: 5B orders x 500 B = 2.5 TB

Total storage estimate: ~900 TB (mostly images)
Total structured data: ~12 TB

Bandwidth Estimation

The outgoing bandwidth is dominated by product images and search result pages. If each search result page loads 20 product thumbnails (20 KB each) and 1 full product image (500 KB), the bandwidth per search query is approximately 900 KB. At 35,000 queries per second average, this gives us 31.5 GB/s of outgoing bandwidth, or roughly 250 Gbps. At peak (10x), this becomes 2.5 Tbps — a significant CDN cost that must be optimized through aggressive image compression, responsive image sizing, and edge caching.

Compute Estimation

Search query processing requires significant CPU for text analysis, scoring, and ranking. A typical search query involves tokenization, spell correction, query expansion, candidate retrieval from inverted indices, feature extraction, and ML model scoring. Each query may touch 50-200 CPU milliseconds on the ranking pipeline. At 350K QPS peak, this translates to roughly 17,500-70,000 CPU cores dedicated to search ranking alone.

C#
// Back-of-envelope capacity calculator
public class CapacityEstimator
{
    public CatalogCapacity Calculate(int totalProducts, int dailySearches, int dailyPageViews)
    {
        var catalogSizeGb = (totalProducts * 10_000L) / (1024 * 1024 * 1024);
        var imageSizeTb = (totalProducts * 5L * 500_000) / (1024L * 1024 * 1024 * 1024);
        var searchIndexGb = totalProducts / 100_000L;
        var avgQps = dailySearches / 86_400.0;
        var peakQps = avgQps * 10;
        var cpuCoresNeeded = (long)(peakQps * 0.15);

        return new CatalogCapacity
        {
            CatalogSizeGb = catalogSizeGb,
            ImageStorageTb = imageSizeTb,
            SearchIndexGb = searchIndexGb,
            AverageQps = avgQps,
            PeakQps = peakQps,
            CpuCoresForSearch = cpuCoresNeeded,
            OutgoingBandwidthGbps = peakQps * 900_000 * 8 / 1_000_000_000
        };
    }
}

public class CatalogCapacity
{
    public long CatalogSizeGb { get; set; }
    public long ImageStorageTb { get; set; }
    public long SearchIndexGb { get; set; }
    public double AverageQps { get; set; }
    public double PeakQps { get; set; }
    public long CpuCoresForSearch { get; set; }
    public double OutgoingBandwidthGbps { get; set; }
}
Scale Tip: When estimating capacity for an interview, always separate "normal" from "peak" traffic. E-commerce platforms experience extreme traffic variance — Black Friday can see 50x normal traffic. Your architecture must handle peak gracefully, either through pre-provisioned capacity or auto-scaling that can respond within minutes. Amazon famously uses "thundering herd" mitigation patterns where traffic is smoothed across availability zones during peak events.

4. Data Model & Storage Schema

The data model for an e-commerce platform must accommodate the extreme heterogeneity of products. A laptop has RAM, CPU, and storage specifications. A dress has size, color, and fabric. A book has ISBN, author, and page count. The challenge is to design a schema that is flexible enough to handle millions of attribute types while remaining performant for queries and indexing.

Core Entities

C#
public class Product
{
    public string ProductId { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public string Brand { get; set; }
    public string CategoryId { get; set; }
    public List<string> CategoryPath { get; set; }
    public List<ProductImage> Images { get; set; }
    public List<ProductVariant> Variants { get; set; }
    public Dictionary<string, string> Attributes { get; set; }
    public decimal BasePrice { get; set; }
    public string SellerId { get; set; }
    public double AverageRating { get; set; }
    public int ReviewCount { get; set; }
    public int SalesRank { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
    public ProductStatus Status { get; set; }
    public Dictionary<string, object> SearchMetadata { get; set; }
}

public class ProductVariant
{
    public string VariantId { get; set; }
    public string ProductId { get; set; }
    public Dictionary<string, string> Options { get; set; }
    public decimal Price { get; set; }
    public decimal? CompareAtPrice { get; set; }
    public int StockQuantity { get; set; }
    public string Barcode { get; set; }
    public decimal Weight { get; set; }
    public bool IsActive { get; set; }
}

public class ProductImage
{
    public string ImageId { get; set; }
    public string Url { get; set; }
    public string AltText { get; set; }
    public int SortOrder { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }
    public ImageType Type { get; set; }
}

Category Schema (Adjacency List with Materialized Path)

C#
public class Category
{
    public string CategoryId { get; set; }
    public string Name { get; set; }
    public string Slug { get; set; }
    public string ParentId { get; set; }
    public string MaterializedPath { get; set; }
    public int Depth { get; set; }
    public List<CategoryAttributeDefinition> FacetDefinitions { get; set; }
    public int ProductCount { get; set; }
    public bool IsActive { get; set; }
    public int SortOrder { get; set; }
}

public class CategoryAttributeDefinition
{
    public string AttributeName { get; set; }
    public string DisplayName { get; set; }
    public AttributeType Type { get; set; }
    public bool IsFacet { get; set; }
    public bool IsRequired { get; set; }
    public List<string> AllowedValues { get; set; }
}

Storage Technology Decisions

Data TypeStorage TechnologyReasoning
Product Catalog (primary)Amazon DynamoDB / CosmosDBKey-value access pattern, massive scale, single-digit ms latency. Products are primarily accessed by ID.
Product Catalog (secondary index)ElasticsearchFull-text search, faceted queries, and relevance ranking. The primary search interface.
Category HierarchyPostgreSQL / MongoDBHierarchical queries, moderate size (thousands of categories), strong consistency needed for tree operations.
Product ImagesS3 + CloudFront CDNBlob storage for originals, CDN for edge delivery. Images are immutable once uploaded.
User Profiles & PreferencesRedis / DynamoDBLow-latency reads for personalization. User preferences are small and frequently accessed.
Reviews & RatingsCassandra / DynamoDBWrite-heavy workload (millions of reviews per day), append-only access pattern, eventual consistency acceptable.
InventoryPostgreSQL (ACID) + Redis cacheRequires strong consistency for stock reservation. Redis provides fast reads for display.
Search Analytics & LogsApache Kafka → S3 → Athena/RedshiftHigh-throughput write of search events, batch analytics for relevance tuning and business intelligence.
Schema Design Principle: Use a flexible attribute store (Dictionary<string, object>) for product attributes rather than fixed columns. Each category defines its own attribute schema via CategoryAttributeDefinition. This allows the system to support millions of attribute types without schema migrations while still enabling typed queries and facets for each category.

Partitioning Strategy

Product data is partitioned by ProductId using consistent hashing. This ensures even distribution across storage nodes and allows any node to locate any product with a single hash lookup. For Elasticsearch, the index is sharded by product hash with a replication factor of 2 for high availability. The category hierarchy is replicated to every node since it is small (fewer than 100,000 categories) and read-heavy.

C#
// Product partition key strategy
public class ProductPartitionResolver
{
    private readonly ConsistentHashRing<string> _ring;

    public string GetPartitionKey(string productId)
    {
        return _ring.GetNode(productId);
    }

    public string GetSearchShard(string queryToken)
    {
        var tokenHash = ComputeHash(queryToken);
        return $"search_shard_{tokenHash % 128}";
    }

    private uint ComputeHash(string key)
    {
        using var sha256 = System.Security.Cryptography.SHA256.Create();
        var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(key));
        return BitConverter.ToUInt32(hash, 0);
    }
}

5. High-Level Architecture Overview

The architecture of an e-commerce product catalog and search system follows a microservices pattern with clearly separated concerns. The key architectural principle is the separation of the write path (catalog updates, price changes, inventory modifications) from the read path (search queries, browse requests, product page views). The write path is optimized for durability and correctness, while the read path is optimized for low latency and high throughput.

graph TB subgraph ClientLayer[Client Layer] Web[Web App] Mobile[Mobile App] API_GW[API Gateway] end subgraph EdgeLayer[Edge Layer] CDN[CDN] ImageEdge[Image Optimization] end subgraph AppServices[Application Services] SearchSvc[Search Service] CatalogSvc[Catalog Service] BrowseSvc[Browse Service] PriceSvc[Price Service] InventorySvc[Inventory Service] CartSvc[Cart Service] OrderSvc[Order Service] ReviewSvc[Review Service] RecSvc[Recommendation Service] end subgraph SearchInfra[Search Infrastructure] ES_Cluster[Elasticsearch Cluster] ML_Rank[ML Ranking Pipeline] FeatureStore[Feature Store] end subgraph DataStores[Data Stores] DynamoDB[DynamoDB] PostgreSQL[PostgreSQL] Redis[Redis Cache] Kafka[Apache Kafka] S3[S3 Storage] end Web --> CDN Mobile --> CDN Web --> API_GW Mobile --> API_GW API_GW --> SearchSvc & CatalogSvc & BrowseSvc & PriceSvc & InventorySvc & CartSvc & OrderSvc & ReviewSvc & RecSvc SearchSvc --> ES_Cluster & ML_Rank & FeatureStore CatalogSvc --> DynamoDB & Kafka InventorySvc --> PostgreSQL & Redis Kafka --> ES_Cluster

Request Flow: Product Search

When a user types a search query, the request flows through the following path: the API Gateway authenticates the request and routes it to the Search Service. The Search Service performs query preprocessing (spell correction, tokenization, synonym expansion), then sends the processed query to Elasticsearch for candidate retrieval. The top 1,000 candidates are passed to the ML Ranking Service, which applies a trained model to re-rank results based on personalization features, business rules, and relevance signals. The final top 50 results are enriched with real-time pricing from the Price Service and inventory status from the Inventory Service before being returned to the client.

sequenceDiagram participant U as User participant GW as API Gateway participant SS as Search Service participant ES as Elasticsearch participant ML as ML Ranker participant PS as Price Service participant IS as Inventory Service participant FS as Feature Store U->>GW: Search wireless headphones GW->>SS: Forward search request SS->>SS: Preprocess query SS->>ES: Query index ES-->>SS: Top 1000 candidates SS->>FS: Fetch user features FS-->>SS: User embedding SS->>ML: Re-rank candidates ML-->>SS: Top 50 ranked results SS->>PS: Batch fetch prices PS-->>SS: Price data SS->>IS: Batch fetch stock IS-->>SS: Availability data SS-->>GW: Search results GW-->>U: Rendered results

Request Flow: Catalog Update

When a seller updates a product listing, the change flows through the Catalog Service, which validates the update, persists it to DynamoDB, and publishes an event to Kafka. The Index Worker consumes the event, transforms the product data into the Elasticsearch document format, and updates the search index. The Price Sync Worker propagates price changes to the Redis cache. The entire pipeline from seller update to search index update typically completes within 5-30 seconds, depending on the type of change.

C#
// Search request handler - orchestrates the full search pipeline
public class SearchRequestHandler
{
    private readonly IQueryPreprocessor _preprocessor;
    private readonly ISearchIndex _searchIndex;
    private readonly IMlRanker _mlRanker;
    private readonly IFeatureStore _featureStore;
    private readonly IPriceService _priceService;
    private readonly IInventoryService _inventoryService;

    public async Task<SearchResult> HandleAsync(SearchRequest request, CancellationToken ct)
    {
        var processedQuery = await _preprocessor.PreprocessAsync(request.Query);

        var candidates = await _searchIndex.SearchAsync(new SearchParams
        {
            Query = processedQuery.Query,
            Filters = request.Filters,
            SortBy = request.SortBy,
            PageSize = 1000,
            CategoryId = request.CategoryId
        }, ct);

        var userFeatures = await _featureStore.GetUserFeaturesAsync(request.UserId);
        var rankedResults = await _mlRanker.RankAsync(candidates.Items, userFeatures);

        var topResults = rankedResults.Take(50).ToList();
        var productIds = topResults.Select(r => r.ProductId).ToList();

        var prices = await _priceService.BatchGetPricesAsync(productIds);
        var inventory = await _inventoryService.BatchGetAvailabilityAsync(productIds);

        var results = topResults.Select(r => new SearchResultItem
        {
            Product = r,
            CurrentPrice = prices[r.ProductId].Price,
            PromotionBadge = prices[r.ProductId].PromotionBadge,
            InStock = inventory[r.ProductId].IsAvailable,
            DeliveryEstimate = inventory[r.ProductId].EstimatedDelivery
        }).ToList();

        return new SearchResult
        {
            Items = results,
            TotalCount = candidates.TotalHits,
            Query = request.Query,
            AppliedFilters = request.Filters
        };
    }
}
Architecture Win: By separating the write path (Catalog Service → Kafka → Index Workers) from the read path (Search Service → Elasticsearch), we achieve independent scaling. Search traffic can spike 10x on Black Friday without impacting catalog updates, and catalog updates can burst during seller onboarding without degrading search latency. The Kafka event bus provides natural backpressure and replay capability.

6. Product Catalog Service

The Product Catalog Service is the authoritative source of truth for all product data. It manages the full lifecycle of product listings — from creation and editing to eventual deactivation. The service must support multiple access patterns: single-product reads (for product detail pages), batch reads (for search result enrichment), and complex writes (for product creation and updates). It must also manage the relationship between parent products and their variants (SKUs).

Catalog CRUD Operations

C#
public class ProductCatalogService
{
    private readonly IProductRepository _repository;
    private readonly IEventPublisher _eventPublisher;
    private readonly ICacheManager _cache;
    private readonly IValidationPipeline _validator;

    public async Task<Product> CreateProductAsync(CreateProductRequest request)
    {
        var validationResult = await _validator.ValidateAsync(request);
        if (!validationResult.IsValid)
            throw new ValidationException(validationResult.Errors);

        var product = new Product
        {
            ProductId = Guid.NewGuid().ToString(),
            Title = request.Title,
            Description = request.Description,
            Brand = request.Brand,
            CategoryId = request.CategoryId,
            CategoryPath = await GetCategoryPathAsync(request.CategoryId),
            Attributes = request.Attributes,
            BasePrice = request.BasePrice,
            SellerId = request.SellerId,
            Status = ProductStatus.Active,
            CreatedAt = DateTime.UtcNow,
            UpdatedAt = DateTime.UtcNow
        };

        await _repository.CreateAsync(product);
        await _cache.SetAsync($"product:{product.ProductId}", product, TimeSpan.FromMinutes(5));

        await _eventPublisher.PublishAsync(new ProductCreatedEvent
        {
            ProductId = product.ProductId,
            CategoryId = product.CategoryId,
            SellerId = product.SellerId,
            Timestamp = DateTime.UtcNow
        });

        return product;
    }

    public async Task UpdatePriceAsync(string productId, decimal newPrice, string reason)
    {
        var product = await _repository.GetByIdAsync(productId);
        var oldPrice = product.BasePrice;
        product.BasePrice = newPrice;
        product.UpdatedAt = DateTime.UtcNow;
        await _repository.UpdateAsync(product);

        await _eventPublisher.PublishAsync(new PriceChangedEvent
        {
            ProductId = productId,
            OldPrice = oldPrice,
            NewPrice = newPrice,
            ChangeReason = reason,
            Timestamp = DateTime.UtcNow
        }, topic: "price-updates");
    }
}

Product Variant Management

Product variants (also called SKUs or child ASINs on Amazon) represent the purchasable units within a product listing. A single "Nike Air Max 90" product may have dozens of variants representing different size and color combinations. The variant model must support independent pricing, stock levels, and attributes while sharing common product data like title, description, and images.

C#
public class VariantManagementService
{
    public async Task<ProductVariant> CreateVariantAsync(string productId, CreateVariantRequest request)
    {
        var product = await _repository.GetByIdAsync(productId);
        var category = await _categoryService.GetCategoryAsync(product.CategoryId);
        ValidateVariantOptions(category, request.Options);

        var existingVariants = await _repository.GetVariantsAsync(productId);
        if (existingVariants.Any(v => OptionsMatch(v.Options, request.Options)))
            throw new ConflictException("A variant with these options already exists");

        var variant = new ProductVariant
        {
            VariantId = GenerateSku(request),
            ProductId = productId,
            Options = request.Options,
            Price = request.Price,
            StockQuantity = request.InitialStock,
            Barcode = request.Barcode,
            Weight = request.Weight,
            IsActive = true
        };

        await _repository.CreateVariantAsync(variant);
        await _eventPublisher.PublishAsync(new VariantCreatedEvent
        {
            ProductId = productId,
            VariantId = variant.VariantId,
            Price = variant.Price
        });

        return variant;
    }

    private string GenerateSku(CreateVariantRequest request)
    {
        var optionString = string.Join("|",
            request.Options.OrderBy(o => o.Key).Select(o => $"{o.Key}={o.Value}"));
        var hash = ComputeHash($"{request.ProductId}:{optionString}");
        return $"SKU-{hash.Substring(0, 12).ToUpper()}";
    }
}
Consistency Consideration: When updating a product and its variants simultaneously, use a transactional outbox pattern. Write the change to the database and the Kafka event in a single transaction, then publish the event asynchronously. This prevents the search index from becoming inconsistent with the primary data store. At Amazon, this pattern is implemented as a "change data capture" (CDC) pipeline using DynamoDB Streams.

7. Search Engine (Elasticsearch)

Elasticsearch is the backbone of the product search experience. It provides full-text search with relevance scoring, faceted aggregations for filtering, and near-real-time index updates. However, at Amazon scale, a single Elasticsearch cluster is insufficient. The search infrastructure requires a multi-cluster architecture with custom indexing pipelines, query routing, and result aggregation. This section covers the index design, query optimization, and relevance tuning that make sub-200ms search possible at scale.

Index Design

The Elasticsearch index is designed as a denormalized view of the product catalog optimized for search and filtering. Each document in the index represents a product variant (SKU) and contains all the fields needed for search results, filtering, and initial display. The index is sharded by product hash with 128 primary shards and 1 replica per shard, distributed across a cluster of 50+ data nodes.

C#
// Elasticsearch index mapping for product search
public class ProductIndexMapping
{
    public static object GetMapping() => new
    {
        settings = new
        {
            number_of_shards = 128,
            number_of_replicas = 1,
            refresh_interval = "1s",
            analysis = new
            {
                analyzer = new
                {
                    product_analyzer = new
                    {
                        type = "custom",
                        tokenizer = "standard",
                        filter = new[] { "lowercase", "asciifolding", "product_synonyms", "english_stemmer" }
                    },
                    product_search_analyzer = new
                    {
                        type = "custom",
                        tokenizer = "standard",
                        filter = new[] { "lowercase", "asciifolding", "product_synonyms" }
                    }
                },
                filter = new
                {
                    product_synonyms = new
                    {
                        type = "synonym",
                        synonyms = new[]
                        {
                            "tv, television, flat screen",
                            "laptop, notebook, portable computer",
                            "headphones, earphones, earbuds",
                            "cell phone, mobile phone, smartphone"
                        }
                    }
                }
            }
        },
        mappings = new
        {
            properties = new
            {
                product_id = new { type = "keyword" },
                sku_id = new { type = "keyword" },
                title = new
                {
                    type = "text",
                    analyzer = "product_analyzer",
                    search_analyzer = "product_search_analyzer",
                    fields = new
                    {
                        keyword = new { type = "keyword", ignore_above = 256 },
                        autocomplete = new { type = "text", analyzer = "standard", search_analyzer = "standard" }
                    }
                },
                description = new { type = "text", analyzer = "product_analyzer" },
                brand = new { type = "keyword" },
                category_id = new { type = "keyword" },
                category_path = new { type = "keyword" },
                price = new { type = "scaled_float", scaling_factor = 100 },
                original_price = new { type = "scaled_float", scaling_factor = 100 },
                discount_percent = new { type = "integer" },
                rating_avg = new { type = "float" },
                rating_count = new { type = "integer" },
                sales_rank = new { type = "integer" },
                stock_status = new { type = "keyword" },
                seller_id = new { type = "keyword" },
                seller_name = new { type = "keyword" },
                seller_rating = new { type = "float" },
                is_prime = new { type = "boolean" },
                delivery_days = new { type = "integer" },
                attributes = new { type = "object", enabled = true },
                popularity_score = new { type = "float" },
                conversion_rate = new { type = "float" },
                last_updated = new { type = "date" }
            }
        }
    };
}

Query Execution Pipeline

The search query execution pipeline transforms a user's natural language query into a structured Elasticsearch query, retrieves candidates, and applies re-ranking. The pipeline has several stages: query understanding (intent classification, spell correction, entity extraction), candidate retrieval (full-text search with boosting), and result scoring (ML-based ranking with hundreds of features).

C#
public class SearchQueryBuilder
{
    public object BuildQuery(ProcessedQuery query, SearchFilters filters)
    {
        var mustClauses = new List<object>();
        var shouldClauses = new List<object>();
        var filterClauses = new List<object>();

        mustClauses.Add(new
        {
            multi_match = new
            {
                query = query.Terms,
                fields = new[] { "title^5", "title.keyword^8", "brand^3", "description^1", "attributes^2" },
                type = "best_fields",
                fuzziness = "AUTO",
                prefix_length = 2
            }
        });

        if (!string.IsNullOrEmpty(query.ExtractedBrand))
        {
            shouldClauses.Add(new
            {
                term = new { brand = new { value = query.ExtractedBrand, boost = 10.0 } }
            });
        }

        filterClauses.Add(new { term = new { stock_status = "in_stock" } });

        if (filters != null)
        {
            if (filters.PriceMin.HasValue)
                filterClauses.Add(new { range = new { price = new { gte = filters.PriceMin } } });
            if (filters.PriceMax.HasValue)
                filterClauses.Add(new { range = new { price = new { lte = filters.PriceMax } } });
            if (filters.Brands?.Any() == true)
                filterClauses.Add(new { terms = new { brand = filters.Brands } });
            if (filters.MinRating.HasValue)
                filterClauses.Add(new { range = new { rating_avg = new { gte = filters.MinRating } } });
            if (filters.CategoryId != null)
                filterClauses.Add(new { term = new { category_id = filters.CategoryId } });
        }

        return new
        {
            @bool = new
            {
                must = mustClauses,
                should = shouldClauses,
                filter = filterClauses,
                must_not = new[] { new { term = new { stock_status = "out_of_stock" } } }
            }
        };
    }
}

Indexing Pipeline

The indexing pipeline transforms raw product data from DynamoDB into Elasticsearch documents. This involves denormalization (flattening category paths, seller names), text analysis (tokenizing titles and descriptions), feature computation (popularity scores, conversion rates), and image URL generation. The pipeline is built on Kafka consumers that process product change events in parallel across multiple consumer groups.

graph LR subgraph WritePath[Write Path] ProductDB[DynamoDB] CDC[CDC Stream] Kafka_P[Kafka: product-events] end subgraph Workers[Index Workers] W1[Worker 1] W2[Worker 2] W3[Worker N] end subgraph Search[Search Cluster] ES_N1[ES Node 1] ES_N2[ES Node 2] end ProductDB --> CDC --> Kafka_P Kafka_P --> W1 & W2 & W3 W1 & W2 & W3 --> ES_N1 & ES_N2
Failure Mode Alert: Elasticsearch index refreshes consume significant I/O. During bulk indexing (for example, after a full reindex), search latency can spike by 2-5x. Mitigate this by using a separate indexing cluster, implementing bulk indexing rate limits, and scheduling full reindexes during low-traffic periods. Amazon uses a "blue-green" index deployment pattern where a new index is built in the background and traffic is switched atomically.

9. Product Detail Page

The Product Detail Page (PDP) is the most revenue-critical page in the entire e-commerce platform. It is where purchase decisions are made — where users read descriptions, examine images, compare specifications, read reviews, and ultimately decide whether to add an item to their cart. Amazon has publicly stated that a 100ms improvement in PDP load time leads to a 1% increase in sales. Given Amazon's revenue, that 100ms is worth hundreds of millions of dollars per year.

PDP Data Assembly

The PDP requires data from multiple services: the Product Catalog Service (title, description, attributes), the Price Service (current price, promotions), the Inventory Service (stock status, delivery estimates), the Review Service (ratings, reviews), the Recommendation Service (related products), and the Seller Service (seller info, ratings). Assembling this data within the latency budget requires aggressive parallelism and caching.

C#
public class ProductDetailPageService
{
    private readonly ICatalogService _catalog;
    private readonly IPriceService _price;
    private readonly IInventoryService _inventory;
    private readonly IReviewService _reviews;
    private readonly IRecommendationService _recommendations;
    private readonly ISellerService _sellers;
    private readonly ICacheManager _cache;

    public async Task<ProductDetailPage> GetProductDetailAsync(string productId, string userId)
    {
        var catalogTask = _catalog.GetProductAsync(productId);
        var priceTask = _price.GetPriceAsync(productId);
        var inventoryTask = _inventory.GetAvailabilityAsync(productId);
        var reviewTask = _reviews.GetReviewSummaryAsync(productId);
        var recTask = _recommendations.GetRelatedProductsAsync(productId, userId, limit: 12);

        await Task.WhenAll(catalogTask, priceTask, inventoryTask, reviewTask, recTask);

        var product = await catalogTask;
        var price = await priceTask;
        var inventory = await inventoryTask;
        var reviewSummary = await reviewTask;
        var relatedProducts = await recTask;
        var seller = await _sellers.GetSellerAsync(product.SellerId);

        return new ProductDetailPage
        {
            Product = product,
            CurrentPrice = price.CurrentPrice,
            CompareAtPrice = price.CompareAtPrice,
            Savings = price.CompareAtPrice.HasValue
                ? price.CompareAtPrice.Value - price.CurrentPrice
                : null,
            Promotions = price.ActivePromotions,
            Availability = new AvailabilityInfo
            {
                InStock = inventory.IsAvailable,
                StockLevel = inventory.Quantity > 0 && inventory.Quantity < 10
                    ? $"Only {inventory.Quantity} left in stock"
                    : null,
                EstimatedDelivery = inventory.EstimatedDelivery,
                ShippingOptions = inventory.ShippingOptions,
                SoldBy = seller.Name,
                SellerRating = seller.Rating
            },
            Reviews = reviewSummary,
            RelatedProducts = relatedProducts
        };
    }
}

PDP Caching Strategy

Given that the PDP is the highest-traffic page and has the tightest latency requirements, caching is critical. The system uses a multi-layer caching strategy: an in-memory L1 cache on each application server (for the hottest products), a distributed L2 cache in Redis (with 60-second TTL for most data, 5-second TTL for price and stock data), and the underlying data stores as L3.

Caching Pitfall: Product pages are highly personalized — different users see different prices (Prime vs. non-Prime), different delivery estimates (based on location), and different recommendations (based on history). This means you cannot cache the full PDP response per product ID. Instead, cache individual data components (catalog data, price, inventory) separately and compose them at request time. This maximizes cache hit rates while preserving personalization.

SEO and Metadata

Product detail pages must be optimized for search engines. The system generates server-side rendered (SSR) HTML with structured data markup (Schema.org Product schema) for Google and other search engines. This includes product title, description, price, availability, rating, and image URLs in JSON-LD format. The SSR content is pre-rendered and cached in the CDN, while the personalized components (recommendations, delivery estimates) are loaded asynchronously via client-side JavaScript.

10. Inventory Management

Inventory management in an e-commerce system is deceptively complex. At its core, it tracks how many units of each product variant exist across the platform's fulfillment network. But the real challenge lies in the concurrency: thousands of customers may simultaneously attempt to purchase the last unit of a popular product, and the system must ensure that no more units are sold than are available. Overselling leads to order cancellations, customer dissatisfaction, and operational costs — while underselling (holding back inventory unnecessarily) leaves money on the table.

Inventory Reservation at Checkout

C#
public class InventoryService
{
    private readonly IInventoryRepository _repository;
    private readonly IDistributedLockManager _lockManager;
    private readonly IEventPublisher _events;

    public async Task<ReservationResult> ReserveInventoryAsync(
        string variantId, int quantity, string orderId, TimeSpan lockDuration)
    {
        var lockKey = $"inventory:{variantId}";
        using var lockHandle = await _lockManager.AcquireAsync(lockKey, lockDuration);

        if (lockHandle == null)
            return ReservationResult.Failed("System busy, please retry");

        var inventory = await _repository.GetInventoryAsync(variantId);
        var available = inventory.Quantity - inventory.Reserved;

        if (available < quantity)
            return ReservationResult.Failed($"Insufficient stock. Available: {available}");

        inventory.Reserved += quantity;
        inventory.ReservationExpiry = DateTime.UtcNow.Add(lockDuration);
        await _repository.UpdateInventoryAsync(inventory);

        await _events.PublishAsync(new InventoryReservedEvent
        {
            VariantId = variantId,
            OrderId = orderId,
            Quantity = quantity,
            ExpiresAt = inventory.ReservationExpiry
        });

        return ReservationResult.Success(inventory);
    }

    public async Task ConfirmReservationAsync(string variantId, string orderId, int quantity)
    {
        var inventory = await _repository.GetInventoryAsync(variantId);
        inventory.Reserved -= quantity;
        inventory.Quantity -= quantity;
        await _repository.UpdateInventoryAsync(inventory);

        await _events.PublishAsync(new StockLevelChangedEvent
        {
            VariantId = variantId,
            NewQuantity = inventory.Quantity,
            Timestamp = DateTime.UtcNow
        }, topic: "inventory-updates");
    }

    public async Task ReleaseReservationAsync(string variantId, string orderId, int quantity)
    {
        var inventory = await _repository.GetInventoryAsync(variantId);
        inventory.Reserved -= quantity;
        await _repository.UpdateInventoryAsync(inventory);
    }
}

Inventory Reservation State Machine

Each inventory unit goes through a state machine: Available, Reserved, Confirmed (deducted), Shipped. If a reservation expires or the order is cancelled, the unit returns to Available.

stateDiagram-v2 [*] --> Available: Product Added Available --> Reserved: Customer Adds to Cart Reserved --> Confirmed: Order Placed Reserved --> Available: Reservation Expired Reserved --> Available: Customer Removed Item Confirmed --> Shipped: Warehouse Ships Confirmed --> Available: Order Cancelled Shipped --> [*]
Critical Failure Mode: If the reservation lock fails (for example, Redis crash during lock acquisition), two customers could simultaneously reserve the same last unit, leading to overselling. Mitigate this with a database-level SELECT FOR UPDATE as a safety net, and implement an automated oversell detection system that flags and resolves conflicts within minutes.

11. Price & Promotion Engine

The Price and Promotion Engine is responsible for computing the final price a customer sees for a product. This involves base pricing, promotional discounts, coupons, tiered pricing (bulk discounts), member pricing (Prime vs. non-Prime), regional pricing, and dynamic pricing based on demand and competition. The engine must return accurate prices in under 50 milliseconds because pricing data is needed for every search result and product detail page view.

Pricing Computation Pipeline

C#
public class PriceEngine
{
    private readonly IBasePriceService _basePrice;
    private readonly IPromotionEngine _promotions;
    private readonly ICouponService _coupons;
    private readonly IMemberPricingService _memberPricing;
    private readonly IDynamicPricingService _dynamicPricing;
    private readonly IPriceCache _cache;

    public async Task<PriceResult> ComputePriceAsync(PriceRequest request)
    {
        var cacheKey = BuildCacheKey(request);
        var cached = await _cache.GetAsync<PriceResult>(cacheKey);
        if (cached != null && !cached.IsStale)
            return cached;

        var basePrice = await _basePrice.GetPriceAsync(request.ProductId, request.VariantId);
        var dynamicAdjustment = await _dynamicPricing.GetAdjustmentAsync(
            request.ProductId, basePrice, request.Quantity);
        var applicablePromotions = await _promotions.GetApplicablePromotionsAsync(
            request.ProductId, request.UserId, request.Quantity);

        CouponDiscount couponDiscount = null;
        if (!string.IsNullOrEmpty(request.CouponCode))
        {
            couponDiscount = await _coupons.ValidateAndApplyAsync(
                request.CouponCode, request.ProductId, request.UserId);
        }

        var memberDiscount = await _memberPricing.GetMemberDiscountAsync(
            request.UserId, basePrice);

        var finalPrice = ComputeFinalPrice(
            basePrice, dynamicAdjustment, applicablePromotions, couponDiscount, memberDiscount);

        var result = new PriceResult
        {
            ProductId = request.ProductId,
            BasePrice = basePrice,
            FinalPrice = finalPrice,
            Currency = "USD",
            CompareAtPrice = GetCompareAtPrice(basePrice, applicablePromotions),
            Savings = basePrice - finalPrice,
            ActivePromotions = applicablePromotions,
            PriceValidUntil = DateTime.UtcNow.AddMinutes(15)
        };

        await _cache.SetAsync(cacheKey, result, TimeSpan.FromSeconds(30));
        return result;
    }

    private decimal ComputeFinalPrice(
        decimal basePrice, decimal dynamicAdjustment,
        List<Promotion> promotions, CouponDiscount coupon, MemberDiscount member)
    {
        var price = basePrice + dynamicAdjustment;

        if (promotions.Any())
            price -= promotions.MaxBy(p => p.DiscountAmount).DiscountAmount;

        if (coupon?.StacksWithPromotions == true)
            price -= coupon.Amount;
        else if (coupon != null)
            price -= coupon.Amount;

        if (member != null)
            price -= member.Amount;

        return Math.Max(price, 0);
    }
}

Promotion Types and Stacking Rules

Promotion TypeExampleStackable?Priority
Percentage Off20% off all electronicsNo (best one wins)Medium
Fixed Amount Off$15 off orders over $100No (best one wins)Medium
Buy One Get One (BOGO)Buy 2 shirts, get 1 freeNoHigh
Bundle DiscountBuy laptop + mouse, save $50NoHigh
Coupon CodeWELCOME10 for 10% offYes (configurable)Low
Member PricingPrime exclusive priceAlways appliedLowest (applied last)
Flash SaleLightning deal: 40% off for 4 hoursNo (exclusive)Highest
Volume DiscountBuy 3+ for 15% off eachNoMedium
Price Consistency Guarantee: The price shown in search results, the product detail page, and the cart must be identical. To achieve this, prices are computed by a single service (PriceEngine) and cached with a unified key. When a customer adds an item to cart, the displayed price is stored in the cart line item. If the price changes before checkout, the system must alert the customer and ask for re-confirmation — never silently change the price.

12. Recommendation Engine

The Recommendation Engine is responsible for driving product discovery across the platform. It powers "Customers who viewed this also viewed," "Frequently bought together," "Recommended for you," and "Inspired by your browsing history" widgets that appear on virtually every page. Amazon has publicly stated that 35% of its revenue comes from its recommendation engine, making it one of the most impactful systems in e-commerce.

Recommendation Architecture

graph TB subgraph Data[Data Collection] Clicks[Click Stream] Views[Page Views] Purchases[Purchase Events] end subgraph Processing[Data Processing] Kafka_R[Kafka Stream] SparkProcessing[Spark/Flink] FeatureEng[Feature Engineering] end subgraph Models[ML Models] CF[Collaborative Filtering] CBF[Content-Based] CTR[CTR Prediction] BTO[Buy Together Model] end subgraph Serving[Serving Layer] FeatureStore_R[Feature Store] ModelServing[Model Serving] RecCache[Rec Cache] API_R[Recommendation API] end Clicks & Views & Purchases --> Kafka_R Kafka_R --> SparkProcessing --> FeatureEng FeatureEng --> CF & CBF & CTR & BTO CF & CBF & CTR & BTO --> ModelServing --> RecCache --> API_R FeatureEng --> FeatureStore_R --> API_R

Recommendation Models

C#
public class RecommendationService
{
    private readonly IFeatureStore _featureStore;
    private readonly IModelServingClient _modelClient;
    private readonly IRecommendationCache _cache;

    public async Task<List<ProductRecommendation>> GetPersonalizedRecommendationsAsync(
        string userId, string context, int limit = 20)
    {
        var cacheKey = $"rec:{userId}:{context}:{limit}";
        var cached = await _cache.GetAsync<List<ProductRecommendation>>(cacheKey);
        if (cached != null) return cached;

        var userFeatures = await _featureStore.GetUserFeaturesAsync(userId);

        var candidateTasks = new[]
        {
            GetCollaborativeFilteringCandidatesAsync(userFeatures, limit * 3),
            GetContentBasedCandidatesAsync(userFeatures, limit * 3),
            GetTrendingCandidatesAsync(userFeatures.PreferredCategories, limit * 2),
            GetRecentlyViewedSimilarAsync(userFeatures.RecentViews, limit * 2)
        };

        await Task.WhenAll(candidateTasks);
        var allCandidates = candidateTasks
            .SelectMany(t => t.Result)
            .GroupBy(c => c.ProductId)
            .Select(g => g.First())
            .ToList();

        var scoredResults = await _modelClient.ScoreBatchAsync(new ScoringRequest
        {
            UserId = userId,
            Candidates = allCandidates.Select(c => new CandidateItem
            {
                ProductId = c.ProductId,
                Features = c.Features
            }).ToList(),
            ModelName = "recommendation_ranking_v3"
        });

        var finalResults = ApplyBusinessRules(scoredResults, userFeatures, limit);
        await _cache.SetAsync(cacheKey, finalResults, TimeSpan.FromMinutes(10));
        return finalResults;
    }

    public async Task<List<BoughtTogetherResult>> GetFrequentlyBoughtTogetherAsync(
        string productId, int limit = 8)
    {
        var cacheKey = $"bought-together:{productId}";
        var cached = await _cache.GetAsync<List<BoughtTogetherResult>>(cacheKey);
        if (cached != null) return cached;

        var associations = await _modelClient.GetAssociationRulesAsync(productId);
        var productIds = associations.Select(a => a.ProductId).ToList();
        var availability = await _catalog.BatchCheckAvailabilityAsync(productIds);

        var results = associations
            .Where(a => availability[a.ProductId])
            .Take(limit)
            .Select(a => new BoughtTogetherResult
            {
                ProductId = a.ProductId,
                Confidence = a.Confidence,
                Support = a.Support,
                DisplayReason = "Customers who bought this item also bought"
            }).ToList();

        await _cache.SetAsync(cacheKey, results, TimeSpan.FromMinutes(30));
        return results;
    }
}
Relevance Metrics: Measure recommendation quality using multiple metrics: Click-Through Rate (CTR) measures initial engagement, Conversion Rate measures actual purchases, Mean Reciprocal Rank (MRR) measures ranking quality, and Coverage measures the diversity of products recommended. A well-tuned recommendation system should achieve 5-15% CTR and 2-5% conversion rate on recommendation widgets.

13. Review & Rating System

The Review and Rating System is a critical trust signal in e-commerce. Research consistently shows that products with reviews have 270% higher conversion rates than those without. The system must handle millions of reviews per day, aggregate ratings in near-real-time, detect and mitigate fake reviews, and serve review summaries with sub-100ms latency. The review data also feeds into search ranking (products with higher ratings rank higher) and recommendation systems (collaborative filtering uses review sentiment).

Review Data Model and Storage

C#
public class Review
{
    public string ReviewId { get; set; }
    public string ProductId { get; set; }
    public string UserId { get; set; }
    public string OrderId { get; set; }
    public int StarRating { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
    public List<ReviewImage> Images { get; set; }
    public bool IsVerifiedPurchase { get; set; }
    public int HelpfulVotes { get; set; }
    public int UnhelpfulVotes { get; set; }
    public ReviewStatus Status { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class ReviewAggregate
{
    public string ProductId { get; set; }
    public double AverageRating { get; set; }
    public int TotalReviews { get; set; }
    public Dictionary<int, int> RatingDistribution { get; set; }
    public int VerifiedPurchaseCount { get; set; }
    public double SentimentScore { get; set; }
    public DateTime LastUpdated { get; set; }
}

public class ReviewService
{
    private readonly IReviewRepository _repository;
    private readonly IAggregateCalculator _aggregator;
    private readonly IFakeReviewDetector _fakeDetector;
    private readonly IEventPublisher _events;

    public async Task<Review> SubmitReviewAsync(ReviewSubmission submission)
    {
        var existingReview = await _repository.GetUserReviewAsync(
            submission.ProductId, submission.UserId);
        if (existingReview != null)
            throw new ConflictException("You have already reviewed this product");

        var isVerified = await VerifyPurchaseAsync(
            submission.OrderId, submission.UserId, submission.ProductId);

        var review = new Review
        {
            ReviewId = Guid.NewGuid().ToString(),
            ProductId = submission.ProductId,
            UserId = submission.UserId,
            OrderId = submission.OrderId,
            StarRating = submission.StarRating,
            Title = submission.Title,
            Body = submission.Body,
            Images = submission.Images,
            IsVerifiedPurchase = isVerified,
            Status = ReviewStatus.Approved,
            CreatedAt = DateTime.UtcNow
        };

        _ = Task.Run(async () =>
        {
            var authenticity = await _fakeDetector.AnalyzeReviewAsync(review);
            if (authenticity.IsLikelyFake)
            {
                review.Status = ReviewStatus.Flagged;
                await _events.PublishAsync(new ReviewFlaggedEvent
                {
                    ReviewId = review.ReviewId,
                    Confidence = authenticity.Confidence,
                    Reasons = authenticity.Reasons
                });
            }
        });

        await _repository.SaveReviewAsync(review);
        await _aggregator.RecomputeAggregateAsync(submission.ProductId);

        await _events.PublishAsync(new ReviewSubmittedEvent
        {
            ProductId = submission.ProductId,
            ReviewId = review.ReviewId,
            StarRating = review.StarRating,
            Timestamp = DateTime.UtcNow
        });

        return review;
    }
}

Fake Review Detection

Fake reviews are a significant problem for e-commerce trust. The detection system uses multiple signals: review timing patterns (many reviews within hours of each other suggest coordinated campaigns), reviewer history (users who only leave 5-star reviews), text similarity (duplicate or near-duplicate review text), purchase verification (reviews without a linked purchase), and sentiment analysis (unusually positive language patterns).

Review Integrity: Never display review aggregates that include flagged reviews. The aggregate computation pipeline must filter out reviews with Status == Flagged or Status == Pending. Additionally, weight verified purchase reviews higher than unverified ones in the aggregate calculation, as verified reviews are significantly more trustworthy.

14. Cart & Wishlist

The shopping cart is the bridge between browsing and purchasing. It must provide a seamless experience across devices (desktop, mobile, tablet) while maintaining strict consistency with inventory and pricing. Cart abandonment — where users add items but leave without purchasing — costs the industry hundreds of billions of dollars annually.

Cart Service Implementation

C#
public class CartService
{
    private readonly ICartRepository _repository;
    private readonly IPriceService _priceService;
    private readonly IInventoryService _inventoryService;
    private readonly IPromotionService _promotionService;
    private readonly IEventPublisher _events;

    public async Task<Cart> AddToCartAsync(string userId, AddToCartRequest request)
    {
        var cart = await GetOrCreateCartAsync(userId);

        var availability = await _inventoryService.GetAvailabilityAsync(request.VariantId);
        if (!availability.IsAvailable)
            throw new BusinessException("This item is currently out of stock");

        var existingItem = cart.Items.FirstOrDefault(i => i.VariantId == request.VariantId);
        if (existingItem != null)
        {
            var newQuantity = existingItem.Quantity + request.Quantity;
            if (newQuantity > availability.Quantity)
                throw new BusinessException($"Only {availability.Quantity} units available");
            if (newQuantity > MaxQuantityPerItem)
                throw new BusinessException($"Maximum {MaxQuantityPerItem} units per item");

            existingItem.Quantity = newQuantity;
        }
        else
        {
            if (cart.Items.Count >= MaxItemsPerCart)
                throw new BusinessException($"Cart is full (maximum {MaxItemsPerCart} items)");

            cart.Items.Add(new CartItem
            {
                CartItemId = Guid.NewGuid().ToString(),
                VariantId = request.VariantId,
                ProductId = request.ProductId,
                Quantity = request.Quantity,
                AddedAt = DateTime.UtcNow,
                PriceAtAddition = (await _priceService.GetPriceAsync(
                    request.ProductId, request.VariantId)).FinalPrice
            });
        }

        await RecomputeCartAsync(cart);
        await _repository.SaveCartAsync(cart);

        await _events.PublishAsync(new CartModifiedEvent
        {
            UserId = userId,
            CartId = cart.CartId,
            Action = "item_added",
            VariantId = request.VariantId,
            Timestamp = DateTime.UtcNow
        });

        return cart;
    }

    private async Task RecomputeCartAsync(Cart cart)
    {
        foreach (var item in cart.Items)
        {
            var currentPrice = await _priceService.GetPriceAsync(item.ProductId, item.VariantId);
            item.CurrentPrice = currentPrice.FinalPrice;
            item.PriceChanged = Math.Abs(item.CurrentPrice - item.PriceAtAddition) > 0.01m;
        }

        cart.Subtotal = cart.Items.Sum(i => i.CurrentPrice * i.Quantity);
        cart.Discount = await _promotionService.ComputeCartDiscountAsync(cart);
        cart.Tax = await ComputeTaxAsync(cart);
        cart.Total = cart.Subtotal - cart.Discount + cart.Tax;
        cart.UpdatedAt = DateTime.UtcNow;
    }
}

Cart vs. Wishlist

FeatureCartWishlist
PurposeItems intended for immediate purchaseItems saved for future consideration
Price ValidationReal-time price check at view timePrice shown may be stale
Inventory CheckStrict availability validationBest-effort availability
Quantity LimitPer-item limit (for example, 99 max)No quantity concept (binary)
ExpirationItems removed after 7 days inactivityNever expires
Abandonment TrackingYes — email reminders at 1h, 24h, 72hNo — sale notifications only
SharingPrivate to userCan be shared via link
Cart Abandonment Recovery: Implement a cart abandonment pipeline that tracks when users add items but do not complete checkout within 30 minutes. Send a series of reminder emails (1 hour, 24 hours, 72 hours) with the exact items in the cart. Amazon's abandonment recovery email campaign has a 10-15% conversion rate, recovering billions in lost revenue annually.

15. Order Management

The Order Management System (OMS) coordinates the entire post-purchase lifecycle: order creation, payment processing, inventory reservation confirmation, warehouse assignment, shipping label generation, delivery tracking, and returns processing. This is one of the most complex components because it must orchestrate multiple external systems while maintaining strong consistency and providing real-time status updates.

Order State Machine

stateDiagram-v2 [*] --> Pending: Order Placed Pending --> PaymentProcessing: Payment Initiated PaymentProcessing --> Confirmed: Payment Success PaymentProcessing --> Cancelled: Payment Failed Confirmed --> Processing: Warehouse Assigned Processing --> Shipped: Carrier Pickup Shipped --> Delivered: Customer Confirmed Shipped --> Returned: Return Requested Delivered --> Refunded: Refund Approved Returned --> Refunded: Return Received Cancelled --> [*] Refunded --> [*]

Order Creation Pipeline

C#
public class OrderService
{
    private readonly ICartService _cartService;
    private readonly IInventoryService _inventory;
    private readonly IPaymentService _payment;
    private readonly IPriceService _price;
    private readonly IOrderRepository _repository;
    private readonly IEventPublisher _events;
    private readonly IFulfillmentService _fulfillment;

    public async Task<Order> PlaceOrderAsync(PlaceOrderRequest request)
    {
        var cart = await _cartService.GetCartAsync(request.UserId);
        if (!cart.Items.Any())
            throw new BusinessException("Cart is empty");

        var priceValidation = await _price.ValidateCartPricesAsync(cart);
        if (priceValidation.HasChanges)
            throw new PriceChangedException(priceValidation.ChangedItems);

        var reservationIds = new List<string>();
        try
        {
            foreach (var item in cart.Items)
            {
                var reservation = await _inventory.ReserveInventoryAsync(
                    item.VariantId, item.Quantity, TimeSpan.FromMinutes(30));
                if (!reservation.Success)
                    throw new InsufficientStockException(
                        item.VariantId, reservation.AvailableQuantity);
                reservationIds.Add(reservation.ReservationId);
            }

            var order = new Order
            {
                OrderId = Guid.NewGuid().ToString(),
                UserId = request.UserId,
                Status = OrderStatus.Pending,
                Items = cart.Items.Select(i => new OrderItem
                {
                    ProductId = i.ProductId,
                    VariantId = i.VariantId,
                    Quantity = i.Quantity,
                    UnitPrice = i.CurrentPrice,
                    LineTotal = i.CurrentPrice * i.Quantity
                }).ToList(),
                Subtotal = cart.Subtotal,
                Tax = cart.Tax,
                ShippingCost = await _fulfillment.CalculateShippingAsync(
                    cart, request.ShippingAddress),
                Total = cart.Total + await _fulfillment.CalculateShippingAsync(
                    cart, request.ShippingAddress),
                ShippingAddress = request.ShippingAddress,
                PaymentMethodId = request.PaymentMethodId,
                CreatedAt = DateTime.UtcNow
            };

            var paymentResult = await _payment.ChargeAsync(new PaymentRequest
            {
                Amount = order.Total,
                Currency = "USD",
                PaymentMethodId = request.PaymentMethodId,
                OrderId = order.OrderId,
                IdempotencyKey = order.OrderId
            });

            if (!paymentResult.Success)
            {
                foreach (var reservationId in reservationIds)
                    await _inventory.ReleaseReservationAsync(reservationId);
                throw new PaymentFailedException(paymentResult.ErrorMessage);
            }

            order.Status = OrderStatus.Confirmed;
            order.PaymentTransactionId = paymentResult.TransactionId;
            await _repository.SaveOrderAsync(order);

            foreach (var reservationId in reservationIds)
                await _inventory.ConfirmReservationAsync(reservationId);

            await _cartService.ClearCartAsync(request.UserId);

            await _events.PublishAsync(new OrderPlacedEvent
            {
                OrderId = order.OrderId,
                UserId = order.UserId,
                Items = order.Items,
                Total = order.Total,
                Timestamp = DateTime.UtcNow
            });

            return order;
        }
        catch (Exception)
        {
            foreach (var reservationId in reservationIds)
                await _inventory.ReleaseReservationAsync(reservationId);
            throw;
        }
    }
}
Distributed Transaction Pattern: Order creation involves multiple services (inventory, payment, order store) and must be atomic. Use the Saga pattern with compensating transactions: each step can be undone by a compensating action. If payment fails, release all inventory reservations. If order save fails, refund the payment. The Saga orchestrator tracks the state of each step and applies compensating actions in reverse order on failure.

16. Content Delivery for Product Images

Product images are the largest data category in an e-commerce platform. Amazon serves billions of product images per day, and image loading accounts for 60-80% of page weight on search results and product detail pages. The system must deliver these images with sub-50ms latency to edge locations worldwide while supporting multiple resolutions, formats (JPEG, WebP, AVIF for modern browsers), and on-the-fly transformations.

Image Processing Pipeline

graph LR Upload[Seller Uploads] --> S3_Orig[S3: Originals] S3_Orig --> Lambda[Image Processor Lambda] Lambda --> Variants[Generate Variants] Variants --> S3_Proc[S3: Processed] S3_Proc --> CloudFront[CloudFront CDN] CloudFront --> Edge[200+ Edge PoPs] Edge --> User[User Browser]
C#
public class ImageVariantGenerator
{
    private static readonly Dictionary<string, ImageVariant> Variants = new()
    {
        ["thumbnail"] = new(150, 150, 70, ImageFormat.WebP),
        ["small"] = new(300, 300, 75, ImageFormat.WebP),
        ["medium"] = new(600, 600, 80, ImageFormat.WebP),
        ["large"] = new(1200, 1200, 85, ImageFormat.JPEG),
        ["zoom"] = new(2400, 2400, 90, ImageFormat.JPEG),
        ["social"] = new(1200, 630, 80, ImageFormat.JPEG)
    };

    public async Task<Dictionary<string, string>> GenerateVariantsAsync(string originalS3Key)
    {
        var results = new Dictionary<string, string>();
        var originalImage = await DownloadFromS3Async(originalS3Key);

        foreach (var (name, variant) in Variants)
        {
            var resized = ResizeImage(originalImage, variant.MaxWidth, variant.MaxHeight);
            var optimized = ApplyQuality(resized, variant.Quality);
            var converted = ConvertFormat(optimized, variant.Format);

            var s3Key = $"products/variants/{name}/{ComputeHash(converted)}." +
                $"{variant.Format.ToString().ToLower()}";
            await UploadToS3Async(s3Key, converted);
            results[name] = GetCdnUrl(s3Key);
        }

        results["srcset"] = GenerateSrcSet(originalImage);
        return results;
    }

    private string GenerateSrcSet(byte[] image)
    {
        var widths = new[] { 150, 300, 450, 600, 768, 992, 1200 };
        var srcSetEntries = widths.Select(w =>
        {
            var resized = ResizeImage(image, w, w);
            var key = $"products/responsive/{ComputeHash(resized)}_{w}w.webp";
            return $"{GetCdnUrl(key)} {w}w";
        });
        return string.Join(", ", srcSetEntries);
    }
}

CDN Caching Strategy

Product images are essentially immutable (a product's image does not change frequently), making them ideal for aggressive CDN caching. The strategy uses long TTLs (30 days for CloudFront) with cache invalidation only when images are explicitly updated.

Content TypeCache TTLInvalidation StrategyCompression
Product Images (processed)30 daysOnly on explicit update (rare)WebP/AVIF with JPEG fallback
Search Result Thumbnails1 hourAuto invalidation on product updateWebP, quality 70
Category Icons7 daysManual invalidation (very rare)SVG or WebP
Search Result HTML60 secondsShort TTL, stale-while-revalidateBrotli
Product Detail HTML (SSR)5 minutesShort TTL, edge-side includesBrotli
Performance Tip: Implement lazy loading for product images on search results pages. Use the Intersection Observer API to load images only when they enter the viewport. This reduces initial page load by 60-70% and improves Core Web Vitals (particularly Largest Contentful Paint). Amazon uses progressive JPEG rendering to show a low-quality placeholder immediately while the full image loads in the background.

17. A/B Testing Framework

A/B testing is the backbone of continuous improvement at Amazon. Every significant change to the search algorithm, product page layout, recommendation model, pricing display, and checkout flow is tested through controlled experiments before being rolled out to all users. Amazon runs thousands of concurrent experiments, and the infrastructure for experiment assignment, metric collection, and statistical analysis is one of the most sophisticated systems in the company.

Experiment Assignment System

C#
public class ExperimentAssignmentService
{
    private readonly IExperimentRepository _repository;
    private readonly IFeatureStore _featureStore;

    public async Task<ExperimentAssignment> AssignUserAsync(string userId, string experimentKey)
    {
        var experiment = await _repository.GetExperimentAsync(experimentKey);

        if (!experiment.IsActive)
            return new ExperimentAssignment { Treatment = experiment.ControlTreatment };

        var existing = await _featureStore.GetUserExperimentAsync(userId, experimentKey);
        if (existing != null)
            return existing;

        var hash = ComputeAssignmentHash(userId, experimentKey);
        var bucketIndex = hash % 100;

        var treatment = AssignTreatment(experiment, bucketIndex);

        var assignment = new ExperimentAssignment
        {
            ExperimentKey = experimentKey,
            Treatment = treatment,
            AssignedAt = DateTime.UtcNow
        };

        await _featureStore.SetUserExperimentAsync(userId, experimentKey, assignment);
        await LogAssignmentAsync(userId, experimentKey, treatment);

        return assignment;
    }

    private string AssignTreatment(Experiment experiment, int bucketIndex)
    {
        var cumulativeWeight = 0;
        foreach (var treatment in experiment.Treatments.OrderBy(t => t.Weight))
        {
            cumulativeWeight += treatment.Weight;
            if (bucketIndex < cumulativeWeight)
                return treatment.Name;
        }
        return experiment.Treatments.Last().Name;
    }
}

public class ABTestMetricsCollector
{
    public async Task TrackMetricAsync(string userId, string experimentKey, MetricEvent metric)
    {
        var assignment = await _experimentService.GetAssignmentAsync(userId, experimentKey);

        var metricRecord = new ExperimentMetric
        {
            UserId = userId,
            ExperimentKey = experimentKey,
            Treatment = assignment.Treatment,
            MetricName = metric.Name,
            MetricValue = metric.Value,
            Timestamp = DateTime.UtcNow,
            DeviceType = metric.DeviceType,
            GeographicRegion = metric.Region,
            UserSegment = metric.UserSegment
        };

        await _kafkaProducer.SendAsync("experiment-metrics", metricRecord);
        await _analyticsStore.WriteAsync(metricRecord);
    }
}

Key Metrics Tracked

Metric CategorySpecific MetricsImpact of Change
Search QualityCTR, dwell time, zero-result rate, search-to-purchase rate1% CTR improvement can mean millions in revenue
RecommendationsCTR, conversion rate, revenue per session, diversity score5% conversion improvement drives significant revenue
Pricing DisplayClick rate on discounted items, conversion rate, AOVBadge design affects perceived value and click-through
PDP LayoutTime on page, scroll depth, add-to-cart rate, bounce rateImage gallery design directly impacts conversion
Checkout FlowCart abandonment rate, checkout completion rate1% improvement means millions in recovered revenue
Statistical Rigor: Never declare a winner before reaching statistical significance (p-value < 0.05, 95% confidence). Use sequential testing (rather than fixed-horizon tests) to allow early stopping when results are clear. Amazon's experiment platform uses Bayesian statistical methods that provide probability distributions over treatment effects rather than binary yes/no decisions.

18. Reliability, Failure Modes & Disaster Recovery

E-commerce systems must maintain extremely high availability because downtime directly translates to lost revenue. Amazon estimates that every minute of outage costs approximately $220,000 in lost sales. The system must be designed to degrade gracefully — if the recommendation engine is slow, it should not block the search results page. If the review service is down, it should not prevent the product detail page from loading.

Circuit Breaker and Fallback Patterns

C#
public class ResilientSearchService
{
    private readonly ISearchIndex _primaryIndex;
    private readonly ISearchIndex _fallbackIndex;
    private readonly ICircuitBreaker _circuitBreaker;
    private readonly IMetricsCollector _metrics;

    public async Task<SearchResult> SearchAsync(SearchRequest request)
    {
        try
        {
            return await _circuitBreaker.ExecuteAsync(async () =>
            {
                return await _primaryIndex.SearchAsync(request);
            });
        }
        catch (CircuitBreakerOpenException)
        {
            _metrics.Increment("search.failover.primary_to_secondary");
            return await _fallbackIndex.SearchAsync(request);
        }
        catch (Exception ex) when (IsTransient(ex))
        {
            _metrics.Increment("search.degraded_mode");
            return await SearchDegradedAsync(request);
        }
    }

    private async Task<SearchResult> SearchDegradedAsync(SearchRequest request)
    {
        var candidates = await _primaryIndex.SearchAsync(new SearchParams
        {
            Query = request.Query,
            Filters = request.Filters,
            PageSize = 100,
            UseMlRanking = false
        });

        return new SearchResult
        {
            Items = candidates.Items,
            TotalCount = candidates.TotalHits,
            IsDegraded = true,
            DegradationReason = "Personalized ranking temporarily unavailable"
        };
    }
}

Failure Modes and Mitigations

Failure ModeImpactDetectionMitigation
Elasticsearch cluster failureSearch unavailableCluster health API, latency spikeMulti-AZ deployment, automatic failover to replica shards
Price service timeoutCannot compute pricesCircuit breaker, timeout monitoringUse last-known-good price from cache
Inventory service downCannot verify stockHealth check failuresDisplay "check availability", block checkout
Kafka broker failureEvent pipeline stallsConsumer lag monitoringMulti-broker cluster, consumer retries, dead-letter queue
Redis cluster failureCache miss stormConnection pool exhaustionRead-through caching with local L1 cache
Recommendation engine downNo personalized suggestionsModel serving health checksStatic fallback recommendations (bestsellers)
CDN failureImages fail to loadCDN health monitoringDirect S3 URLs as fallback, multiple CDN providers
Database replication lagStale readsReplication lag monitoringRead from primary when lag exceeds threshold
Cascading Failure Prevention: The most dangerous failure mode is a cascade: the search index becomes slow, more retries pile up, the index becomes slower, more retries, total collapse. Prevent this with aggressive timeouts (100ms max per query), circuit breakers (open after 5 failures in 30 seconds), load shedding (reject low-priority queries during overload), and retry budgets (max 2 retries per request across the entire call chain). Amazon's "Cell-Based Architecture" isolates failure domains so that one cell's problems do not propagate to others.

Disaster Recovery

The system is deployed across three AWS availability zones (AZs) with active-active configuration. In the event of a full AZ failure, traffic is automatically rerouted to the remaining two AZs within 30 seconds. For regional disaster recovery (entire region failure), a warm standby in a secondary region can be promoted to active within 15 minutes. The search index is continuously replicated to the secondary region, and product data uses DynamoDB Global Tables for cross-region replication with a lag of under 1 second.

19. Cost Estimation & Infrastructure Sizing

Running an Amazon-scale e-commerce platform is one of the most expensive infrastructure operations in technology. The total infrastructure cost is in the billions of dollars per year, spanning compute, storage, networking, CDN, and third-party services. Understanding the cost breakdown is critical for architectural decision-making, as many design choices have significant cost implications at scale.

Monthly Cost Breakdown (Estimated for Amazon Scale)

ComponentServiceMonthly Cost (Est.)Key Cost Driver
Product Catalog DBDynamoDB (on-demand)$2,000,000Read/write capacity for 350M products
Search IndexElasticsearch (50+ nodes)$1,500,000m5.8xlarge instances, EBS storage
Product ImagesS3 + CloudFront$5,000,000900TB storage + 10PB/month CDN transfer
Redis CacheElastiCache (100 nodes)$800,000Hot cache for pricing, sessions
Application ServersEC2 (2000 instances)$3,000,000All application services
Message QueueKafka (50 brokers)$500,000Event streaming
ML Model ServingSageMaker / GPU instances$1,000,000Recommendation and ranking inference
PostgreSQL (Inventory)RDS Multi-AZ (20 nodes)$200,000ACID-compliant inventory management
Analytics & Data LakeS3 + Athena + Redshift$500,000Search logs, model training data
Networking & TransferVPC, Direct Connect$300,000Inter-service communication
Total Monthly$14,800,000
Total Annual$177,600,000

Cost Optimization Strategies

C#
public class CostOptimizationStrategies
{
    // WebP images are 25-35% smaller than JPEG at equivalent quality
    public async Task<long> CalculateImageSavingsAsync(int totalImages, long avgSizeBytes)
    {
        var jpegSize = totalImages * avgSizeBytes;
        var webpSize = jpegSize * 0.70;
        var annualTransferPb = 10;
        var costPerGbTransfer = 0.08m;
        var monthlySavings = (annualTransferPb * 1024 * 1024 * 0.30m) * costPerGbTransfer;
        return (long)monthlySavings;
    }

    // Provisioned DynamoDB is ~50% cheaper for predictable workloads
    public decimal CompareDynamoDbPricing(long dailyReads, long dailyWrites)
    {
        var onDemandCost = (dailyReads / 1_000_000m * 0.25m) +
                          (dailyWrites / 1_000_000m * 1.25m);
        var provisionedCost = onDemandCost * 0.50m;
        return onDemandCost - provisionedCost;
    }

    // Reserved instances for steady-state workloads
    public decimal CalculateReservedInstanceSavings(
        int instanceCount, decimal hourlyRate, int months)
    {
        var onDemandCost = instanceCount * hourlyRate * 730 * months;
        var reservedDiscount = months switch
        {
            12 => 0.30m,
            36 => 0.55m,
            _ => 0m
        };
        return onDemandCost * reservedDiscount;
    }
}
Cost at Scale: At Amazon's scale, even small optimizations compound into massive savings. A 1% improvement in image compression saves $50,000/month. A 10% improvement in DynamoDB query efficiency saves $200,000/month. Moving from provisioned to on-demand for bursty workloads can save 30-50% on database costs. Every architectural decision should be evaluated not just for performance but for cost-effectiveness at scale.

20. Interview Q&A Deep Dive

This section covers the most common system design interview questions related to e-commerce product catalog and search systems. These questions are drawn from real interview loops at Amazon, Google, Meta, Microsoft, and other major technology companies. Each answer demonstrates the depth of thinking expected at the Senior+ level.

Q1: How would you handle search for products with millions of attributes across diverse categories?

Answer: Use a category-driven schema design where each category defines its own attribute schema via CategoryAttributeDefinition. Store attributes in Elasticsearch as a nested object with dynamic mapping, but enforce type consistency through the schema registry at indexing time. For search queries, dynamically select the appropriate facets and filters based on the current category context. This avoids the "one schema fits all" problem while maintaining type safety within each category. The attribute definitions themselves are cached in Redis and loaded at query time, adding less than 1ms of overhead.

Q2: How do you handle products that appear in multiple categories?

Answer: Support multi-category assignment using a many-to-many relationship between products and categories. In Elasticsearch, store an array of category_ids and category_paths. When a user browses "Electronics - Accessories," the query filters on the specific category path. When browsing "Bags - Laptop Bags," a different category path is used. The product appears in both categories. The challenge is facet computation — brand facets must reflect only products in the current category context, not all products with that category assignment. Use post-facet filtering to ensure accurate facet counts.

Q3: How would you design the system to handle Black Friday traffic spikes (50x normal load)?

Answer: A multi-layered approach: (1) Pre-warm infrastructure 48 hours before the event by adding 3x capacity. (2) Use a CDN aggressively — cache search results for popular queries for 5-10 seconds. (3) Implement "thundering herd" mitigation by rate-limiting at the edge and queuing excess requests. (4) Degrade gracefully — disable ML re-ranking for non-personalized queries, serve stale recommendations from cache, reduce facet computation to popular facets only. (5) Use cell-based architecture to isolate traffic. (6) Implement request hedging — if the primary search path is slow, serve cached results after 100ms.

Q4: How do you maintain search index consistency when product data changes rapidly?

Answer: Use an event-driven architecture with Kafka as the backbone. Product changes are published as events immediately upon write. Index workers consume events and update Elasticsearch within seconds. For critical data (price, stock), use a dedicated high-priority Kafka topic with dedicated consumers. For less critical data (description updates), use the standard topic with batch consumers. Implement idempotent index updates to handle duplicate events. Use DynamoDB Streams as a secondary change capture mechanism to detect and repair any inconsistencies.

Q5: How would you design the buy box algorithm?

Answer: The buy box is a real-time auction between competing sellers. The algorithm considers: (1) Price (40% weight), (2) Seller rating and review count (20%), (3) Fulfillment method — FBA/Prime beats merchant-fulfilled (20%), (4) Delivery speed (15%), (5) Stock availability (5%). The algorithm runs at product page load time and caches the result for 60 seconds. A seller who loses the buy box can still sell through their own product page, but the default "Add to Cart" goes to the winner. The algorithm must be configurable per category and market.

Q6: How do you handle real-time price changes during flash sales?

Answer: Flash sale prices are pre-computed and loaded into Redis before the sale starts, with an expiration timestamp. When a user views a product during the sale, the price is read from Redis (sub-1ms latency) rather than computing through the full pricing pipeline. The search index is pre-updated with sale prices before the event starts. During the sale, price changes propagate via a dedicated Kafka topic with 30-second latency. At checkout, the system re-validates the price against the pricing engine. The Price Lock feature freezes the displayed price for 15 minutes once a user adds the item to cart.

Q7: How would you design the review aggregation system to handle millions of reviews?

Answer: Use a pre-computed aggregate pattern. When a new review is submitted, publish an event. A dedicated consumer recomputes the aggregate (average rating, distribution, sentiment score) using the latest review data. The aggregate is stored in a fast-access store (DynamoDB or Redis) and cached at the CDN edge. For products with very high review volumes (over 100K reviews), use incremental aggregation — maintain a running sum and count, and recompute the full aggregate periodically rather than on every review submission.

Q8: How do you handle product deduplication when the same product is listed by multiple sellers?

Answer: Product deduplication uses a multi-signal approach: (1) UPC/EAN barcode matching — if two listings share the same barcode, they are the same product. (2) Fuzzy title matching using edit distance and token overlap. (3) Image similarity using perceptual hashing (pHash). (4) Attribute matching — same brand, model number, and specifications. Deduplication runs as an offline batch job (daily) and an online near-real-time check for new listings. Confirmed duplicates are linked to a canonical product listing, and their offers appear as competing sellers on the same product detail page.

Q9: How would you design personalization for search results without violating user privacy?

Answer: Privacy-preserving personalization uses several techniques: (1) On-device personalization — compute user preferences locally and send only the preference vector to the server. (2) Differential privacy — add calibrated noise to aggregate behavior data so individual users cannot be identified. (3) Federated learning — train personalization models on-device and only share model gradients with the server. (4) Opt-in personalization — clearly communicate what data is used and allow users to opt out. (5) Data retention limits — automatically delete browsing history after 90 days. The recommendation system works with anonymized cohort data rather than individual tracking.

Q10: How would you implement a zero-downtime index rebuild?

Answer: Use the blue-green index deployment pattern. (1) Create a new index (green) alongside the existing blue index. (2) Index all products into the green index from DynamoDB (full rebuild takes 4-8 hours for 350M products). (3) During the rebuild, all search queries go to the blue index. (4) Once the green index is complete, verify data integrity by comparing a sample of 10,000 products. (5) Switch the search alias from blue to green atomically (under 1 second). (6) Monitor green index performance for 30 minutes. (7) If issues detected, roll back by switching alias back to blue. This achieves zero-downtime index rebuild with sub-second cutover.

Q11: How do you handle search autocomplete with high accuracy and low latency?

Answer: Search autocomplete uses a trie-based data structure backed by Elasticsearch's completion suggester. The trie is pre-built from popular search queries (top 10M queries), product titles, brand names, and category names. The trie is loaded into Redis for sub-5ms lookups. When a user types "wirel," the system returns completions like "wireless headphones," "wireless mouse," etc. The completion scorer combines prefix match, popularity (query frequency), and personalization (user's past searches). Results are returned within 30ms, well below the 100ms threshold for perceived instant response.

Q12: How would you handle a situation where the search index and the product database become inconsistent?

Answer: Implement a reconciliation system that runs periodically (every 15 minutes) to compare the search index against the source of truth in DynamoDB. The reconciliation job: (1) Samples a random set of 10,000 products from each. (2) Compares key fields (price, stock status, title, category). (3) Identifies discrepancies and generates a report. (4) Automatically repairs inconsistencies by re-indexing the affected products. (5) Alerts the engineering team if inconsistency rate exceeds 0.1%. Additionally, implement a "shadow read" system where search results are compared against DynamoDB reads in real time for a sample of queries, providing immediate detection of inconsistencies.

E-Commerce Product Catalog — Senior+ Guide