Technology · ssh / indianDevelopers
LLD 06 — Design a Coffee Machine: Recipes, Inventory and Java Code - Part - 2
This is in continuation of this LLD-06 part - 1 you can see that article here Link Adding a New Drink Should Be Boring Suppose we want to add a new beverage tomorrow. The ideal change is not: modify CoffeeMachine modify Inventory modify PreparationUnit add five switch cases It…
This is in continuation of this LLD-06 part - 1 you can see that article here Link
Adding a New Drink Should Be Boring
Suppose we want to add a new beverage tomorrow.
The ideal change is not:
modify CoffeeMachine
modify Inventory
modify PreparationUnit
add five switch cases
It should look closer to:
new BeverageRecipe(
BeverageType.NEW_DRINK,
List.of(
...
)
)
That is a useful test of the design.
If a new recipe uses only preparation actions the machine already supports, recipe data should be enough.
If the new drink genuinely introduces new behaviour—say the machine must dispense chocolate powder using a new actuator—then code changes are justified.
That gives us a useful rule:
New data should usually require new data. New behaviour may require new code.
Where Would Factory Pattern Actually Help?
This problem is often taught with:
CoffeeFactory
├── createEspresso()
├── createLatte()
└── createCappuccino()
I would not begin there.
What problem is that factory solving?
If it contains:
switch (type) {
case ESPRESSO -> new Espresso();
case LATTE -> new Latte();
}
we have just moved the same branching into another class.
A Factory becomes more useful if constructing different beverages requires genuinely different object structures or dependencies.
Our baseline does not have that requirement.
RecipeCatalog is enough.
Where Could Strategy Pattern Help?
Now imagine the interviewer says:
Some drinks require a completely different preparation algorithm.
Perhaps one recipe is handled by the normal espresso hardware while another requires a specialized brewing module.
Now we might introduce:
public interface PreparationStrategy {
void prepare(
BeverageRecipe recipe
);
}
with different implementations.
That would be justified because behaviour is changing.
The pattern should arrive after the requirement, not before it.
What About Extra Sugar, Double Shots and Oat Milk?
This is another point where inheritance can explode.
Please do not create:
Latte
LatteWithSugar
DoubleShotLatte
OatMilkLatte
DoubleShotOatMilkLatte
Those are order customizations.
A better future model might be:
BeverageOrder
├── base beverage
└── modifiers
with modifiers adjusting the effective recipe.
For example:
Latte
+
ExtraShot
+
OatMilk substitution
The important thing is that this is a new requirement.
We don't need to implement it in version one just to prove that the architecture is “extensible.”
What If Preparation Fails After Ingredients Were Deducted?
This is one of the more interesting follow-up questions.
Our flow currently does:
reserve ingredients
↓
start physical preparation
Suppose beans are ground successfully and then the heater fails.
Should we simply put all inventory back?
Probably not.
Those beans may physically be inside the brew chamber already.
Software rollback and physical rollback are not always the same thing.
A more realistic system would distinguish things such as:
reserved ingredients
dispensed ingredients
consumed ingredients
machine fault
and may mark the drink as failed while reporting that some inventory was wasted.
That is a much better answer than blindly putting:
catch (Exception e) {
inventory.refundEverything();
}
around the method.
LLD gets interesting when the domain refuses to behave like a database transaction.
Is availableBeverages() Always Accurate?
It is accurate when it takes its inventory snapshot.
But imagine the UI shows:
CAPPUCCINO → available
and another request consumes the last milk immediately afterwards.
The user could still select cappuccino and receive:
InsufficientIngredientsException
That is okay.
Availability displayed to a user is informational.
The authoritative check happens inside the atomic tryConsume() operation.
This same idea appears in much larger backend systems:
A read can tell you what was available a moment ago. The write path must still protect the invariant.
Why Is make() Synchronized If Inventory Is Already Thread-Safe?
Because inventory is not the only shared resource.
Our machine represents one physical brewing unit.
We probably do not want:
Thread A → grind espresso
Thread B → steam latte
Thread A → brew espresso
Thread B → brew latte
interleaving through the same hardware.
Synchronizing make() gives our baseline machine one active preparation at a time.
If the machine later has two independent brewing heads, locking the entire CoffeeMachine would become unnecessarily restrictive.
Then we would model those brewing units explicitly and coordinate them separately.
Again, concurrency boundaries should reflect physical/domain boundaries.
Common Mistakes
One Subclass Per Menu Item
If drinks differ mainly by quantities and steps, modelling every menu item as another subclass creates unnecessary inheritance.
Ingredient Logic Inside Every Beverage
Avoid repeating:
check water
check beans
deduct water
deduct beans
in every drink implementation.
Inventory should own inventory invariants.
Checking and Deducting Separately
This allows two preparations to oversubscribe the same stock.
The important operation is atomic:
check + consume
Putting Hardware Logic Inside CoffeeMachine
The orchestrator should not know how voltage reaches a heater or how a grinder motor is driven.
Adding Factory, Builder, Strategy and Observer on Day One
Patterns are tools.
If we cannot explain what variability or coupling a pattern removes, we probably do not need it yet.
How I Would Explain This in an Interview
I would start with one question:
“Are the beverages different behaviours, or mostly different recipes?”
Then I would model:
BeverageRecipe
RecipeStep
Inventory
CoffeeMachine
PreparationUnit
before writing any subclasses.
The main workflow I would implement is:
make(beverage)
→ find recipe
→ calculate ingredient requirements
→ atomically consume stock
→ prepare recipe
After that, I would discuss:
adding another recipe;
concurrent orders;
ingredient refill;
customizations;
preparation failure;
multiple brewing units.
I would not spend ten minutes drawing a Button, Display, CupHolder and MilkPipe unless the interviewer actually wants hardware-component modelling.
Those details can exist in the real machine without being the important software abstractions for this interview.
The Core Lesson From LLD 06
Parking Lot taught us to separate entities from replaceable policies.
Vending Machine made state transitions explicit.
Tic-Tac-Toe separated orchestration from rules.
Library Management showed why similar nouns can represent different lifecycles.
Elevator separated building-wide scheduling from car-level scheduling.
Coffee Machine adds another design instinct:
Do not turn every variation in data into a variation in type.
Espresso and latte are different menu choices.
That does not automatically mean they deserve separate class hierarchies.
If the machine can understand both through:
recipe
+
ingredients
+
ordered preparation steps
then composition keeps the design smaller and makes the menu easier to extend.
And when a future requirement introduces genuinely different behaviour, we can add the abstraction then.
That is usually better LLD than predicting fifty requirements and building all fifty abstractions in advance.

Conversation
Comments
Sign in to join the conversation.