Course Content
Object-Oriented Design Interview
14 sections · 29 lessons
Grocery Store: scoping the prompt and separating product from stock
"Design a system for a grocery store" is the broadest prompt in the course, and the breadth is the test. This lesson covers the first half of the round: choosing a slice and defending it, writing requirements for that slice, and finding the objects — including the modelling error most attempts make, which has the same shape as one you have already met.
This is a scoping test wearing a modelling problem's clothes
A grocery store contains a point of sale, inventory, purchasing, suppliers, staff scheduling, loyalty programmes, promotions, returns, waste tracking, refrigeration monitoring, and accounting. All of it is real. None of it fits in forty-five minutes.
So the interviewer is not testing whether you can model a grocery store. They are testing whether you can look at an unbounded problem, choose a coherent slice, say why you chose it, and build that slice well. A candidate who tries to cover everything produces twenty boxes with no depth and fails; a candidate who models checkout and inventory properly and names the rest as deliberately excluded passes.
The four questions that change the model
1. "Point of sale, inventory, or both?" Both, and the boundary between them is where the design gets interesting. If the interviewer says "only the till", the problem gets much smaller and you should say so and ask for more.
2. "One store, or a chain?" This is the question that produces the modelling insight in finding the objects, below. A chain forces you to separate what a product is from how many of it this store has. Assume a small chain.
3. "Are suppliers and reordering in scope?" Reordering is the natural home for the Observer pattern (low stock triggers a purchase order) and it is a satisfying extension. Assume it is in scope as a notification, with actual purchasing cut.
4. "How does payment work — cash, card, split payment?" Take payment as an interface with a success or failure result. Split payment across two cards is a good extension question and a bad use of the middle of the round.
Assumptions this lesson makes
- A small chain: several stores, one shared product catalogue.
- Point of sale: a cart, scanning items, applying prices, taking one payment, printing a receipt.
- Inventory: per-store stock levels, decremented on sale, with a low-stock threshold.
- Reordering: a notification when stock falls below a threshold. Placing the actual purchase order is out of scope.
- Several tills per store operating concurrently against the same stock.
Requirements
Functional requirements
1. Look up a product by barcode and get its current price at this store2. Add and remove items in a cart, including weighed items3. Total the cart, applying any active promotions4. Take payment and produce an order5. Decrement inventory for every item sold6. Raise a low-stock notification when a product falls below its reorder pointRequirement 2 has a hidden detail worth catching: weighed items. Bananas are priced per kilogram, not per unit, so a cart line is not always "quantity 3". Noticing this in the requirements phase is a small, genuine domain signal — it means OrderLine needs a quantity with a unit, not an integer.
Non-functional requirements
N1. Stock must never go negative. Four tills selling the last two packets of coffee must result in two sales and two refusals, not four sales.
N2. A till must complete a sale even if the inventory service is slow. A supermarket that stops selling because a background system is unhealthy is a business failure. This tension — correctness versus availability — is the honest heart of this problem and Checkout and inventory update, in code handles it.
N3. Adding a promotion type must not modify checkout code.
Explicitly cut, and named
CUT (named, not forgotten)- Loyalty programmes and points- Returns and refunds- Staff scheduling and payroll- Supplier contracts, purchase orders, and goods-in receiving- Shelf planning, waste tracking, refrigeration alarms- Self-checkout hardware, scales, and scanner protocolsName them as you write them. "Returns I'm cutting — it's a whole second flow with its own states, and it would double the design. Loyalty I'm cutting because it mostly affects pricing, which I'm already putting behind an interface, so it would slot in later without changing the structure."
That second half is the good part: it tells the interviewer that the cut item has a place to go in your design. Cutting something and knowing where it would attach is much stronger than cutting it because it is hard.
The numbers that shape it
A single supermarket, order of magnitude:
- Catalogue: tens of thousands of products.
- Tills: eight to twenty, each scanning an item every couple of seconds at peak.
- Transactions: a few thousand per store per day.
So: catalogue lookups are frequent and must be fast (a hash lookup by barcode, or a cache); writes are moderate; contention is on a few popular products rather than on everything. That last observation matters — it means per-product locking is fine and a global inventory lock is not.
Finding the objects
The candidate nouns
Product, item, price, barcode, store, shelf, stock, cart, basket, customer, checkout, order, line, payment, receipt, supplier, threshold, promotion.
The modelling error most attempts make
Almost every first attempt writes this:
1public class Product {2 private String barcode;3 private String name;4 private Money price;5 private int quantityInStock; // <-- the error6}One class, holding both what the product is and how many are on the shelf. It works for a single store and breaks immediately for two, because the same tin of tomatoes has one name and two stock levels.
It is worse than it looks even for one store. The catalogue and the stock level have different lifetimes, different owners, and different change rates:
| The product | The stock level | |
|---|---|---|
| Who owns it | Head office / the catalogue | The individual store |
| How often it changes | Rarely — a name, a description | Constantly — every sale |
| Scope | Global across the chain | Per store |
| Who reads it | Every store, the website | This store's tills |
| Concurrency | Read-mostly | Write-heavy and contended |
Two different things. Split them:
1public class Product { // the catalogue entry: global, read-mostly2 private final String barcode;3 private final String name;4 private final Category category;5 private final UnitOfMeasure unit; // EACH or KILOGRAM — the weighed-item detail6}78public class InventoryItem { // this store's holding of that product9 private final Product product;10 private final Store store;11 private Quantity onHand;12 private Quantity reserved; // in someone's cart at a till right now13 private final Quantity reorderPoint;14 public Quantity available() { return onHand.minus(reserved); }15}Say the reason out loud, because the sentence is the score: "I'm separating Product from InventoryItem because a product is a chain-wide catalogue entry that rarely changes, and a stock level is per-store and changes on every sale. Merging them makes a second store impossible and puts a read-mostly record on the hottest write path in the system."
If you made this error in your attempt, that diff alone was worth the forty-five minutes. It is the same shape as Seat versus ShowSeat in the movie booking design — a permanent definition separated from a per-context state — and noticing the repeat is exactly the pattern transfer this course is building.
Price does not live on Product either
The same argument applies once more. A price varies by store, by date, and by promotion. Put a Money price field on Product and a chain cannot run a regional price, and a promotion has to mutate the catalogue.
public interface PricingService { Money priceFor(Product product, Store store, Instant at);}One interface, and promotions become implementations rather than fields (Extensions).
The rest of the model
| Class | Its one-sentence job |
|---|---|
Product | The chain-wide definition of one sellable thing |
Store | One physical location with its own inventory |
InventoryItem | How much of one product this store holds |
Cart | The items a customer is buying, before payment |
Order | A completed sale: its lines, its total, its payment |
OrderLine | One product, one quantity, and the price charged |
Payment | One attempt to collect the order total |
Supplier | Who to reorder from; thin, only needed for notifications |
PricingService | Turns a product and a context into a price |
Nine, which is at the limit for forty-five minutes — a consequence of the breadth, and another reason the cut list matters.