How to Design an E-Commerce Product Catalog & Search System
Building an Amazon-Scale System — Catalog, Search, Filtering, Pricing, Inventory, and Recommendations
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.
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.
2. Functional & Non-Functional Requirements
Functional Requirements
- 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.
- 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.
- 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.
- Product Detail Pages: Users can view comprehensive product information including title, description, images, price, availability, seller information, reviews and ratings, specifications, and related products.
- 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.
- 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.
- Recommendations: The system must provide "Customers who viewed this also viewed," "Frequently bought together," "Similar items," and personalized "Recommended for you" product suggestions.
- 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.
- Cart & Wishlist: Users can add products to a shopping cart or wishlist. The cart must validate inventory availability and pricing at checkout time.
- 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
| Requirement | Target | Rationale |
|---|---|---|
| Search Latency (p99) | < 200ms | Users abandon search results pages that take longer than 500ms. Amazon targets sub-200ms for search queries. |
| Product Detail Page Latency (p99) | < 300ms | PDP is the most revenue-critical page. Latency directly impacts conversion rates. |
| Throughput | 100K+ QPS peak | Must handle traffic spikes during sales events (Black Friday, Prime Day) with 10-50x normal load. |
| Availability | 99.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 changes | Users must not see stale pricing or stock information. Price changes and stock-outs must propagate quickly. |
| Data Durability | 99.999999999% (11 nines) | Product catalog data is the business's core asset. Loss is unacceptable. |
| Consistency Model | Eventual consistency for search; strong consistency for cart/checkout | Search can tolerate seconds of staleness; cart and payment cannot tolerate inconsistencies. |
| Personalization | Results personalized per user within 50ms overhead | Personalization improves conversion by 15-30% but must not significantly impact latency. |
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.
| Metric | Daily | Per Second (avg) | Per Second (peak, 10x) |
|---|---|---|---|
| Search Queries | 3 billion | 35,000 | 350,000 |
| Product Detail Page Views | 5 billion | 58,000 | 580,000 |
| Category Browse Requests | 2 billion | 23,000 | 230,000 |
| Cart Operations | 500 million | 5,800 | 58,000 |
| Order Placements | 100 million | 1,160 | 11,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; }
}
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 Type | Storage Technology | Reasoning |
|---|---|---|
| Product Catalog (primary) | Amazon DynamoDB / CosmosDB | Key-value access pattern, massive scale, single-digit ms latency. Products are primarily accessed by ID. |
| Product Catalog (secondary index) | Elasticsearch | Full-text search, faceted queries, and relevance ranking. The primary search interface. |
| Category Hierarchy | PostgreSQL / MongoDB | Hierarchical queries, moderate size (thousands of categories), strong consistency needed for tree operations. |
| Product Images | S3 + CloudFront CDN | Blob storage for originals, CDN for edge delivery. Images are immutable once uploaded. |
| User Profiles & Preferences | Redis / DynamoDB | Low-latency reads for personalization. User preferences are small and frequently accessed. |
| Reviews & Ratings | Cassandra / DynamoDB | Write-heavy workload (millions of reviews per day), append-only access pattern, eventual consistency acceptable. |
| Inventory | PostgreSQL (ACID) + Redis cache | Requires strong consistency for stock reservation. Redis provides fast reads for display. |
| Search Analytics & Logs | Apache Kafka → S3 → Athena/Redshift | High-throughput write of search events, batch analytics for relevance tuning and business intelligence. |
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.
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.
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
};
}
}
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()}";
}
}
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.
8. Faceted Search & Filtering
Faceted search is one of the most distinctive features of e-commerce search compared to general web search. When a user searches for "laptop," the system must not only return matching products but also compute available filter options (brands, price ranges, screen sizes, RAM amounts) and their counts. These facets enable users to progressively narrow their search without leaving the results page. The challenge is that facet counts must be accurate, dynamically computed based on the current query context, and returned within the existing latency budget.
Facet Computation in Elasticsearch
C#
public class FacetedSearchService
{
private readonly IElasticClient _elasticClient;
public async Task<FacetedSearchResult> SearchWithFacetsAsync(FacetSearchRequest request)
{
var searchResponse = await _elasticClient.SearchAsync<ProductDocument>(s => s
.Index("products")
.Size(request.PageSize)
.From(request.Page * request.PageSize)
.Query(q => BuildQuery(request))
.Aggregations(aggs => aggs
.Terms("brands", t => t
.Field(f => f.Brand)
.Size(50)
.Order(o => o.CountDescending()))
.Histogram("price_ranges", h => h
.Field(f => f.Price)
.Interval(10)
.MinimumDocumentCount(1))
.Range("ratings", r => r
.Field(f => f.RatingAvg)
.Ranges(
ra => ra.From(4).To(5).Key("4 Stars & Up"),
ra => ra.From(3).To(5).Key("3 Stars & Up"),
ra => ra.From(2).To(5).Key("2 Stars & Up")))
.Terms("delivery", t => t.Field(f => f.DeliveryDays))
.Nested("attributes", n => n
.Path(p => p.Attributes)
.Aggregations(a => a
.Terms("screen_size", t => t.Field("attributes.screen_size").Size(20))
.Terms("ram", t => t.Field("attributes.ram").Size(20))
.Terms("processor", t => t.Field("attributes.processor").Size(30))))
)
);
return MapToFacetedResult(searchResponse);
}
}
Cross-Facet Consistency
A subtle but critical challenge in faceted search is cross-facet consistency. When a user selects the "Sony" brand filter, the price range facet should update to show only price ranges available for Sony products. This requires "post-facet" computation where each facet is computed in the context of all other active filters. Elasticsearch handles this through "scoped aggregations" where each facet is computed with the current filter context applied.
C#
// Scoped facet computation - each facet sees all OTHER filters applied
public class ScopedFacetComputer
{
public async Task<Dictionary<string, FacetResult>> ComputeScopedFacetsAsync(
string query, List<ActiveFilter> activeFilters)
{
var results = new Dictionary<string, FacetResult>();
foreach (var facet in FacetDefinitions)
{
var scopedFilters = activeFilters
.Where(f => f.Field != facet.FieldName)
.ToList();
var facetResult = await ComputeFacetAsync(query, scopedFilters, facet);
results[facet.FieldName] = facetResult;
}
return results;
}
}
Dynamic Facets by Category
Different product categories require different facets. A laptop search shows RAM, CPU, and screen size filters. A clothing search shows size, color, and fabric filters. The system must dynamically determine which facets to display based on the current category context.
| Category | Key Facets | Special Considerations |
|---|---|---|
| Electronics - Laptops | Brand, Price, RAM, CPU, Screen Size, Storage, Weight, OS | Numeric ranges for RAM and storage. CPU requires semantic grouping. |
| Clothing - Dresses | Brand, Price, Size, Color, Length, Pattern, Fabric, Sleeve Type | Color facet should display color swatches. Size varies by region. |
| Books - Fiction | Author, Price, Format, Language, Genre, Publication Date | Author facet is extremely popular. Format affects price display. |
| Home and Kitchen | Brand, Price, Material, Size, Dishwasher Safe, Oven Safe, Color | Material groups similar items. Temperature ratings need numeric ranges. |
| Sports - Shoes | Brand, Price, Size, Width, Color, Terrain Type, Cushioning | Size must support half sizes. Width is a critical filter. |
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.
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.
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 Type | Example | Stackable? | Priority |
|---|---|---|---|
| Percentage Off | 20% off all electronics | No (best one wins) | Medium |
| Fixed Amount Off | $15 off orders over $100 | No (best one wins) | Medium |
| Buy One Get One (BOGO) | Buy 2 shirts, get 1 free | No | High |
| Bundle Discount | Buy laptop + mouse, save $50 | No | High |
| Coupon Code | WELCOME10 for 10% off | Yes (configurable) | Low |
| Member Pricing | Prime exclusive price | Always applied | Lowest (applied last) |
| Flash Sale | Lightning deal: 40% off for 4 hours | No (exclusive) | Highest |
| Volume Discount | Buy 3+ for 15% off each | No | Medium |
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
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;
}
}
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).
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
| Feature | Cart | Wishlist |
|---|---|---|
| Purpose | Items intended for immediate purchase | Items saved for future consideration |
| Price Validation | Real-time price check at view time | Price shown may be stale |
| Inventory Check | Strict availability validation | Best-effort availability |
| Quantity Limit | Per-item limit (for example, 99 max) | No quantity concept (binary) |
| Expiration | Items removed after 7 days inactivity | Never expires |
| Abandonment Tracking | Yes — email reminders at 1h, 24h, 72h | No — sale notifications only |
| Sharing | Private to user | Can be shared via link |
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
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;
}
}
}
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
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 Type | Cache TTL | Invalidation Strategy | Compression |
|---|---|---|---|
| Product Images (processed) | 30 days | Only on explicit update (rare) | WebP/AVIF with JPEG fallback |
| Search Result Thumbnails | 1 hour | Auto invalidation on product update | WebP, quality 70 |
| Category Icons | 7 days | Manual invalidation (very rare) | SVG or WebP |
| Search Result HTML | 60 seconds | Short TTL, stale-while-revalidate | Brotli |
| Product Detail HTML (SSR) | 5 minutes | Short TTL, edge-side includes | Brotli |
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 Category | Specific Metrics | Impact of Change |
|---|---|---|
| Search Quality | CTR, dwell time, zero-result rate, search-to-purchase rate | 1% CTR improvement can mean millions in revenue |
| Recommendations | CTR, conversion rate, revenue per session, diversity score | 5% conversion improvement drives significant revenue |
| Pricing Display | Click rate on discounted items, conversion rate, AOV | Badge design affects perceived value and click-through |
| PDP Layout | Time on page, scroll depth, add-to-cart rate, bounce rate | Image gallery design directly impacts conversion |
| Checkout Flow | Cart abandonment rate, checkout completion rate | 1% improvement means millions in recovered revenue |
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 Mode | Impact | Detection | Mitigation |
|---|---|---|---|
| Elasticsearch cluster failure | Search unavailable | Cluster health API, latency spike | Multi-AZ deployment, automatic failover to replica shards |
| Price service timeout | Cannot compute prices | Circuit breaker, timeout monitoring | Use last-known-good price from cache |
| Inventory service down | Cannot verify stock | Health check failures | Display "check availability", block checkout |
| Kafka broker failure | Event pipeline stalls | Consumer lag monitoring | Multi-broker cluster, consumer retries, dead-letter queue |
| Redis cluster failure | Cache miss storm | Connection pool exhaustion | Read-through caching with local L1 cache |
| Recommendation engine down | No personalized suggestions | Model serving health checks | Static fallback recommendations (bestsellers) |
| CDN failure | Images fail to load | CDN health monitoring | Direct S3 URLs as fallback, multiple CDN providers |
| Database replication lag | Stale reads | Replication lag monitoring | Read from primary when lag exceeds threshold |
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)
| Component | Service | Monthly Cost (Est.) | Key Cost Driver |
|---|---|---|---|
| Product Catalog DB | DynamoDB (on-demand) | $2,000,000 | Read/write capacity for 350M products |
| Search Index | Elasticsearch (50+ nodes) | $1,500,000 | m5.8xlarge instances, EBS storage |
| Product Images | S3 + CloudFront | $5,000,000 | 900TB storage + 10PB/month CDN transfer |
| Redis Cache | ElastiCache (100 nodes) | $800,000 | Hot cache for pricing, sessions |
| Application Servers | EC2 (2000 instances) | $3,000,000 | All application services |
| Message Queue | Kafka (50 brokers) | $500,000 | Event streaming |
| ML Model Serving | SageMaker / GPU instances | $1,000,000 | Recommendation and ranking inference |
| PostgreSQL (Inventory) | RDS Multi-AZ (20 nodes) | $200,000 | ACID-compliant inventory management |
| Analytics & Data Lake | S3 + Athena + Redshift | $500,000 | Search logs, model training data |
| Networking & Transfer | VPC, Direct Connect | $300,000 | Inter-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;
}
}
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.