Skip to main content

Amazon (Marketplace)

This is not the cart-and-checkout problem - that's its own page. This one is one level up: a catalog where the same product can be sold by several sellers at several prices, and an order has to pick a seller and take stock out of that seller's inventory, not some single global count.

Requirements

Functional

  • A Catalog lists Products; each product can have multiple Listings, one per Seller, each with its own price and stock count.
  • A customer places an Order for a product; the system picks a listing to fulfill it from (cheapest available, in this design) and decrements that seller's stock.
  • A Seller can update the price or stock of their own listings without touching anyone else's.
  • An order can span multiple products from different sellers and still be one order the customer sees as a whole.

Non-functional

  • Picking a listing must never oversell a seller's stock, even when several orders for the same product arrive close together.
  • Adding a new way to rank listings (fastest shipping, best seller rating) should not require changing Order or Catalog.

Design

Catalog maps a product to its listings; it never touches money or stock directly. Order asks a ListingSelectionStrategy to pick a listing per line item, then calls that listing's reserveStock - the only place stock actually decreases. A Seller owns its listings the same way ParkingFloor owns its spots: nothing outside touches a Listing's stock field except through that one method.

CustomerOrderCatalogSelectionStrategyListingplace(productId, qty)1listingsFor(productId)2select(listings)3reserveStock(qty)4return reserved5
  1. 1The customer orders a product, not a specific seller.
  2. 2The order asks the catalog for every seller currently offering this product.
  3. 3Which listing wins is delegated entirely - order placement never hardcodes "cheapest".
  4. 4Only the chosen listing's stock ever changes; every other seller's count is untouched.
  5. 5The listing confirms the reservation before the order is finalized.

Splitting Product (the catalog entry: title, description, category) from Listing (one seller's price and stock for that product) is what makes "three sellers, three prices" a non-event instead of a special case bolted onto Product.

Class diagram

«interface»ListingSelectionStrategy+ select(listings, qty): ListingCatalog- listingsByProduct: Map<string, List<Listing>>+ listListings(productId): List<Listing>+ addListing(l)Product- id: string- title: string- category: stringSeller- id: string- name: string- rating: doubleListing- product: Product- seller: Seller- price: double- stock: int+ reserveStock(qty): bool+ restock(qty)Order- lines: List<OrderLine>- customerId: string+ place(catalog, productId, qty)+ total(): doubleOrderLine- listing: Listing- quantity: intCheapestFirstStrategy+ select(listings, qty): Listing
implementsuses
Catalog holds Products; each Product fans out to per-seller Listings; Order reserves stock on the Listing it selects.

Code

import java.util.*;
 
class Product {
final String id;
final String title;
final String category;
 
Product(String id, String title, String category) {
this.id = id;
this.title = title;
this.category = category;
}
}
 
class Seller {
final String id;
final String name;
 
Seller(String id, String name) {
this.id = id;
this.name = name;
}
}
 
class Listing {
final Product product;
final Seller seller;
double price;
private int stock;
 
Listing(Product product, Seller seller, double price, int stock) {
this.product = product;
this.seller = seller;
this.price = price;
this.stock = stock;
}
 
boolean reserveStock(int qty) {
if (stock < qty) return false;
stock -= qty;
return true;
}
 
void restock(int qty) {
stock += qty;
}
 
int stock() {
return stock;
}
}
 
interface ListingSelectionStrategy {
Optional<Listing> select(List<Listing> listings, int qty);
}
 
class CheapestFirstStrategy implements ListingSelectionStrategy {
public Optional<Listing> select(List<Listing> listings, int qty) {
return listings.stream()
.filter(l -> l.stock() >= qty)
.min(Comparator.comparingDouble(l -> l.price));
}
}
 
class Catalog {
private final Map<String, List<Listing>> listingsByProduct = new HashMap<>();
 
void addListing(Listing listing) {
listingsByProduct
.computeIfAbsent(listing.product.id, k -> new ArrayList<>())
.add(listing);
}
 
List<Listing> listListings(String productId) {
return listingsByProduct.getOrDefault(productId, List.of());
}
}
 
class OrderLine {
final Listing listing;
final int quantity;
 
OrderLine(Listing listing, int quantity) {
this.listing = listing;
this.quantity = quantity;
}
}
 
class Order {
private final List<OrderLine> lines = new ArrayList<>();
private final ListingSelectionStrategy strategy;
private final Catalog catalog;
 
Order(Catalog catalog, ListingSelectionStrategy strategy) {
this.catalog = catalog;
this.strategy = strategy;
}
 
void place(String productId, int qty) {
Listing listing = strategy.select(catalog.listListings(productId), qty)
.orElseThrow(() -> new IllegalStateException("No listing can fulfill " + productId));
if (!listing.reserveStock(qty)) {
throw new IllegalStateException("Lost race for stock on " + productId);
}
lines.add(new OrderLine(listing, qty));
}
 
double total() {
return lines.stream().mapToDouble(l -> l.listing.price * l.quantity).sum();
}
}

Design decisions

  • Listing is a separate class from Product, not a Map<Seller, Price> field on Product. A listing has its own lifecycle - a seller can deactivate theirs while others stay live - and its own stock count that changes independently. Modeling it as a first-class object gives reserveStock somewhere to live; a bare map would push that logic back up into Order or Catalog, whichever touched it first.
  • reserveStock lives on Listing, and it's the only mutator of stock in the whole system. Every path that reduces stock - a placed order, a returned order putting it back - goes through this one method, so the invariant "stock never goes negative" has exactly one place to be enforced instead of N.
  • Listing selection is a ListingSelectionStrategy, mirroring FeeStrategy from parking-lot.mdx. Cheapest-first is one policy; a real marketplace also weighs shipping speed, seller rating, and Prime eligibility. None of that should change Order's placement logic, only which strategy gets wired in.
  • What's missing for a real system: reserveStock here isn't atomic across concurrent orders - a real implementation needs a compare-and-swap or DB-level row lock on the listing's stock count so two orders racing for the last unit can't both succeed, and a reservation needs a timeout/release path for abandoned carts rather than committing stock the instant a listing is picked.
0%0 of 122 pages studied