"Another bug?"
"Should we just call the order validation module guy over to debug?"In the meeting room, we went through endless rounds of postmortems.
The code was clean. The design patterns was textbook-perfect. However, the chaos on the ground was overwhelming.At that moment, I realized: it was exactly the elegant design pattern itself that created chaos in operation.
To protect company confidentiality, I’m using a fictional case to illustrate a real problem I encountered: why well-designed patterns can become a nightmare in real business scenarios.
The Fictional Scenario
A large restaurant chain developed an ordering system that supports three order types:
- Dine-in
- Takeout
- Delivery
These three types share some common steps, like placing the order and payment. However, they each also have specific business rules:
- Dine-in orders require a table number
- Takeout orders require a pickup time
- Delivery orders require a valid address
Since the majority of the logic was similar, the IT department decided to adopt the Factory Design Pattern to handle this.
Factory Design Pattern
The Factory Pattern is a creational design pattern that centralizes object instantiation logic. It avoids repeating shared logic across different modules and makes it easier to scale or add new order types such as catering.
The team created:
- An Order interface with shared methods
- Three order classes: EatInOrder, TakeAwayOrder, and DeliveryOrder, each implements Order
- An OrderFactory to instantiate the correct type of order
The Order interface had three methods:
- confirm() to place the order
- payment() to handle payment
- orderRule() to validate order-specific rules
The three order classes only override the orderRule() method, since the confirm and payment logic is shared and assumed to be the same.
interface Order {
default void confirm(){}
default void payment(){}
boolean orderRule();
}
class EatInOrder implements Order{
@Override
public boolean orderRule() {
boolean hasTableNo = true;
return hasTableNo;
}
}
class TakeAwayOrder implements Order{
@Override
public boolean orderRule() {
boolean hasTakeTime = true;
return hasTakeTime;
}
}
class DeliveryOrder implements Order{
@Override
public boolean orderRule() {
boolean isAddressValid = true;
return isAddressValid;
}
}
A separate OrderValidator class handled final order checks, like payment verification, since these were assumed to be shared across all the order types.
class OrderValidator {
public boolean validate(Order order){
boolean commonProcessResult = true;
return commonProcessResult;
}
}
Clean and elegant, right? But once we launched it into production, debugging became a nightmare.
Operational Bottlenecks
In this system, each order type was managed by a different engineer. The validation logic, however, was centralized.
So when:
- a dine-in table number error occurred,
- a takeout time issue was flagged, or
- a delivery address bug popped up—
The engineer handling that specific order type module had to work with the engineer responsible for the OrderValidator module.
In short, the pressure of all order validation ultimately landed on the validator team.
As the website traffic increased, so did bugs — and the bottleneck meant the debug of the entire system slowed down, as order modules couldn’t do so independently.
A Better Solution: Encapsulation
Even though the validation logic was shared, we chose to move the validation method into each order class and encapsulating the logic.
interface Order {
default void confirm(){}
default void payment(){}
default boolean validate(){
boolean commonProcessResult = true;
return commonProcessResult;
}
boolean orderRule();
}
class OrderValidator {
public boolean validate(Order order){
return order.validate();
}
}
This allowed each order module owner to control their own validation logic, even if it was identical at the time. If divergence happens later, changes can be made in the respective module without bottlenecking the validation team.
interface Order {
default void confirm(){}
default void payment(){}
boolean validate();
boolean orderRule();
}
class EatInOrder implements Order{
@Override
public boolean validate() {
boolean processResult = true;
return processResult;
}
@Override
public boolean orderRule() {
boolean hasTableNo = true;
return hasTableNo;
}
}
class TakeAwayOrder implements Order{
@Override
public boolean validate() {
boolean processResult = true;
return processResult;
}
@Override
public boolean orderRule() {
boolean hasTakeTime = true;
return hasTakeTime;
}
}
class DeliveryOrder implements Order{
@Override
public boolean validate() {
boolean processResult = true;
return processResult;
}
@Override
public boolean orderRule() {
boolean isAddressValid = true;
return isAddressValid;
}
}
Final Thoughts
When designing software, we can’t only focus on code reuse or architectural elegance. We also have to think about how operation duty are distributed across teams.
After all, no matter how elegant the code looks — if the frontline team can’t survive, it’s all for nothing.