Cracking The Coding Interview Together – Problem 9.6: Sales Rank
Imagine walking into a massive digital warehouse with hundreds of millions of products. Every single night, as the clock strikes midnight, a silent transformation occurs. Millions of items shift in position, climbing or falling based on the day's commercial velocity. This is the world of the daily sales rank, a critical metric that influences discovery, conversion, and ultimately, millions of dollars in revenue. When we step into a system design interview or face a massive architectural challenge at scale, calculating these ranks is not just about sorting numbers—it is about orchestrating petabytes of transactions under strict time constraints. Today, we are going to dive deep into Problem 9.6 from Cracking the Coding Interview and scale it up to a real-world distributed system.
Problem Statement
We need to design a system to compute the sales rank for every item on a large eCommerce platform. The sales rank is computed daily based on metrics such as units sold, revenue generated, and recent velocity trends. Our operational goals are clear: process hundreds of millions of transactions, aggregate sales metrics reliably, compute a deterministic sort order (rank), and publish these ranks efficiently so that downstream read services can serve them with sub-millisecond latencies.
Analyze the Problem Together
Let's break down the constraints and bottlenecks we face. First, scale is our primary enemy. If we have 100 million products and billions of daily orders, a simple in-memory sort on a single machine is an absolute non-starter. Second, ranking is relative: an item's rank depends entirely on its performance relative to every other item in its category or store-wide.
Let's look at our architectural options:
| Approach | Pros | Cons |
|---|---|---|
| Naive Single-Database Sort | Simple to implement, ACID compliant. | Fails at scale (O(N log N) bottlenecks, disk I/O limits). |
| Distributed MapReduce / Spark | Handles massive datasets, highly parallelizable, fault-tolerant. | High operational complexity, batch processing latency. |
| Streaming Real-Time Windowing | Immediate rank updates throughout the day. | Extremely high resource consumption, race conditions, expensive state management. |
The Aha! conflict here is that while users love real-time updates, true daily sales rank is traditionally a batch-oriented aggregate metric. Attempting to maintain a real-time global sort across 100 million items for every single purchase introduces severe lock contention. Therefore, our architectural choice is a hybrid approach: a robust, distributed batch pipeline running nightly for the canonical rank, backed by localized caching for intraday adjustments if required.
Questions for the Interviewer
Before writing a single line of code or drawing system boxes, a senior engineer asks clarifying questions to bound the problem space:
- Scale: Are we computing ranks globally across the entire catalog, or independently within specific product categories (e.g., Electronics, Books)?
- Latency: Does 'computed daily' mean the batch must complete within a specific maintenance window (e.g., 2 hours before peak morning traffic)?
- Definition of Rank: Is the rank strictly a linear descending sort of total units sold, or does it incorporate a decay factor giving more weight to recent purchases?
- Consistency: Can downstream services tolerate eventual consistency during the rank publishing phase, or do we need an atomic swap?
Think About the Solution
Our high-level architecture consists of three distinct phases: Ingestion & Aggregation, Sorting & Ranking, and Storage & Serving. We will use a distributed data processing framework like Apache Spark to read daily order logs from a data lake (such as Amazon S3), aggregate metrics per item, apply our ranking algorithm, and write the resulting rank-to-item mapping into a distributed key-value store like Cassandra or DynamoDB for lightning-fast reads. We must also handle edge cases such as tie-breaking (items with identical sales figures must have a deterministic secondary sort, like item ID creation timestamp) and zero-sale items.
Write Code and Explain
Let's look at a production-ready Java implementation using a Spark-like conceptual pipeline and sorting logic to demonstrate how we process and assign ranks cleanly.
import java.util.*;
import java.util.stream.Collectors;
public class SalesRankEngine {
public static class SaleRecord {
String itemId;
int unitsSold;
double totalRevenue;
long creationTimestamp;
public SaleRecord(String itemId, int unitsSold, double totalRevenue, long creationTimestamp) {
this.itemId = itemId;
this.unitsSold = unitsSold;
this.totalRevenue = totalRevenue;
this.creationTimestamp = creationTimestamp;
}
}
public static class RankedItem {
String itemId;
int rank;
int unitsSold;
public RankedItem(String itemId, int rank, int unitsSold) {
this.itemId = itemId;
this.rank = rank;
this.unitsSold = unitsSold;
}
}
/**
* Computes deterministic sales ranks given a collection of aggregated item sales.
* Ties are broken by total revenue, and finally by creation timestamp (older items win ties).
*/
public List<RankedItem> computeRanks(List<SaleRecord> records) {
// Step 1: Sort items using a custom multi-level comparator
records.sort((a, b) -> {
if (b.unitsSold != a.unitsSold) {
return Integer.compare(b.unitsSold, a.unitsSold); // Descending units sold
}
if (Double.compare(b.totalRevenue, a.totalRevenue) != 0) {
return Double.compare(b.totalRevenue, a.totalRevenue); // Descending revenue
}
return Long.compare(a.creationTimestamp, b.creationTimestamp); // Ascending timestamp (older first)
});
// Step 2: Assign sequential ranks, handling dense/standard ranking logic
List<RankedItem> rankedItems = new ArrayList<>(records.size());
for (int i = 0; i < records.size(); i++) {
SaleRecord record = records.get(i);
// Rank is 1-indexed; handle duplicate scores gracefully if business logic requires standard competition ranking
int rank = i + 1;
rankedItems.add(new RankedItem(record.itemId, rank, record.unitsSold));
}
return rankedItems;
}
}
Walkthrough of the code: We define a SaleRecord holding our core aggregation metrics. Our sorting logic implements a robust multi-tier comparator: primary sort by units sold, secondary by revenue, and tertiary by item creation timestamp to guarantee deterministic results across job re-runs. Finally, we iterate through the sorted list to assign 1-indexed ranks, producing a clean output list ready for database persistence.
Time & Space Complexity
Let's analyze the algorithmic performance of our ranking engine:
- Time Complexity: O(N log N) where N is the total number of unique items with sales. This is driven by the sorting algorithm. In distributed frameworks like Spark, this sorting is distributed across cluster nodes via shuffle operations, effectively parallelizing the heavy lifting.
- Space Complexity: O(N) to store the item records and computed ranks in memory during the processing window. In distributed environments, this memory overhead is partitioned horizontally across executor nodes.
Why This Problem Matters (The Senior Perspective)
Designing a sales rank engine teaches us crucial lessons in distributed systems architecture. In production, you cannot simply update millions of rows in a relational database overnight without locking tables and causing cascading outages. Senior engineers design systems with decoupled storage layers—using immutable batch outputs that perform atomic switches (blue-green deployments or pointer swaps in NoSQL/caches) to update the serving layer instantly. Furthermore, understanding deterministic sorting prevents phantom rank flapping caused by unstable sort algorithms.
Final Thoughts
Tackling Problem 9.6 bridges the gap between basic data structures and petabyte-scale distributed data pipelines. By thinking critically about sorting constraints, tie-breaking strategies, and batch processing windows, you build systems that scale gracefully. Keep experimenting with your pipelines, and happy coding!
Member discussion