Over-Engineering in LLD
Over-engineering means adding complexity that doesnβt solve a current problem. In machine coding rounds, this kills you: you spend 30 minutes building an elaborate plugin system when a simple if-else would have worked, and you never finish the core functionality.
Why this matters: The fastest way to fail a machine coding round is to have beautiful abstractions that donβt compile. Interviewers explicitly evaluate whether you can ship working code under time pressure. Patterns are tools, not goals.
Prerequisites
- SOLID Principles β you need to know these to know when theyβre overkill
- Machine Coding Round β the context where this matters most
YAGNI: You Arenβt Gonna Need It
flowchart LR
A["Is this required<br/>by the current spec?"]:::client
B["Build it now"]:::service
C["Will the interviewer<br/>ask for it?"]:::data
D["Make it easy to add later<br/>but dont build it"]:::service
E["Skip it entirely"]:::data
A -->|Yes| B
A -->|No| C
C -->|Likely| D
C -->|No| E
classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
The principle: donβt build functionality until you actually need it. βWe might need this laterβ is not a reason to build it now β especially in a 90-minute interview.
Real Examples of Over-Engineering in Interviews
1. Pattern for One Implementation
// OVER-ENGINEERED: Factory + Strategy + Builder for a single pricing rule
public interface PricingStrategyFactory {
PricingStrategy create(PricingConfig config);
}
public class DefaultPricingStrategyFactory implements PricingStrategyFactory {
@Override
public PricingStrategy create(PricingConfig config) {
return new HourlyPricingStrategy(config.getRate());
}
}
public interface PricingStrategy {
Money calculate(Duration duration, VehicleType type);
}
public class HourlyPricingStrategy implements PricingStrategy {
// ... the only implementation that exists
}
// JUST RIGHT: Direct implementation. Add abstraction when you need a second variant.
public class PricingService {
private final Map<VehicleType, Integer> hourlyRates;
public PricingService(Map<VehicleType, Integer> hourlyRates) {
this.hourlyRates = hourlyRates;
}
public Money calculate(VehicleType type, Duration duration) {
int rate = hourlyRates.getOrDefault(type, 10);
long hours = Math.max(1, duration.toHours()); // minimum 1 hour
return Money.of(rate * hours);
}
}
// If interviewer says "add tiered pricing," THEN extract the interface.
# OVER-ENGINEERED: ABC + Factory + Registry for one implementation
class PricingStrategy(ABC):
@abstractmethod
def calculate(self, duration, vehicle_type): ...
class HourlyPricing(PricingStrategy):
def calculate(self, duration, vehicle_type):
return duration.total_seconds() / 3600 * RATES[vehicle_type]
class PricingFactory:
_registry = {"hourly": HourlyPricing}
@classmethod
def create(cls, strategy_type):
return cls._registry[strategy_type]()
# JUST RIGHT: A function. Seriously.
HOURLY_RATES = {VehicleType.CAR: 20, VehicleType.BIKE: 10, VehicleType.TRUCK: 50}
def calculate_price(vehicle_type: VehicleType, duration: timedelta) -> Decimal:
rate = HOURLY_RATES[vehicle_type]
hours = max(1, int(duration.total_seconds() / 3600))
return Decimal(rate * hours)
// OVER-ENGINEERED: virtual dispatch for one behavior
class IPricingEngine {
public:
virtual ~IPricingEngine() = default;
virtual double calculate(VehicleType type, int hours) = 0;
};
class HourlyPricingEngine : public IPricingEngine {
public:
double calculate(VehicleType type, int hours) override {
return rates_[type] * hours;
}
};
// JUST RIGHT: a method on the service
class PricingService {
unordered_map<VehicleType, int> rates_;
public:
double calculate(VehicleType type, int hours) {
return rates_[type] * max(1, hours);
}
};
2. Generic Framework When You Need Specific Logic
// OVER-ENGINEERED: Generic event system for a simple notification
public class EventBus<T extends Event> {
private final Map<Class<? extends Event>, List<EventHandler<? extends Event>>> handlers;
// ... 50 lines of generic event infrastructure
}
// When all you needed was:
public class ParkingLot {
private final NotificationService notifications;
public Receipt unpark(Ticket ticket) {
// ... unpark logic ...
notifications.sendReceipt(ticket.getVehicle().getOwnerPhone(), receipt);
return receipt;
}
}
3. Premature Thread Safety
// OVER-ENGINEERED: ConcurrentHashMap, locks, AtomicInteger everywhere
// when the problem statement says "single-user CLI application"
public class VendingMachine {
private final AtomicInteger balance = new AtomicInteger(0);
private final ConcurrentHashMap<String, Product> inventory = new ConcurrentHashMap<>();
private final ReentrantReadWriteLock stateLock = new ReentrantReadWriteLock();
// ... way more complex than needed for a single-threaded demo
}
// JUST RIGHT for single-threaded:
public class VendingMachine {
private int balance = 0;
private final Map<String, Product> inventory = new HashMap<>();
// Simple, readable, works for the demo
// Mention thread safety as an improvement if asked
}
The Rule of Three
Donβt abstract until you have three instances of the same pattern:
- First occurrence: Write it directly. No abstraction.
- Second occurrence: Note the duplication, maybe extract a method. Still no pattern.
- Third occurrence: NOW introduce the pattern. You have enough examples to design a good abstraction.
In interviews, you usually wonβt hit three occurrences. So keep it simple.
Signs Youβre Over-Engineering
| Signal | What It Means |
|---|---|
| Youβre writing code that no test exercises | YAGNI β delete it |
| Class has one implementation and no plans for a second | Skip the interface |
| You spent 20 min on architecture, 0 lines run | Ship first, refactor second |
| The pattern adds more code than it saves | The pattern isnβt paying rent |
| Youβre building βjust in caseβ | Build when the case arrives |
| Your abstractions have abstractions | Step back |
How to Stay Right-Sized
- Start concrete, go abstract β Write the specific code first. Extract when repetition appears.
- Make it work, make it right, make it fast β In that order. Working code first.
- Design for the current requirements β Not for hypothetical future requirements.
- Leave extension points β Use interfaces where you SEE variation. Donβt preemptively interface everything.
- The 10-line rule β If an βalgorithmβ is under 10 lines, it doesnβt need its own Strategy class yet.
When to Use vs When to Avoid Patterns
| Pattern | Use When | Over-Engineering When |
|---|---|---|
| Strategy | 3+ variants exist TODAY | Only 1 variant, βmight add moreβ |
| Factory | Creation logic is complex or type-dependent | new Thing() is all you need |
| Observer | Multiple independent reactors exist | One thing needs to know about another |
| Builder | 5+ optional fields | 2-3 required fields in constructor |
| Singleton | Genuinely one instance needed globally | βI only have one right nowβ |
| Decorator | Composing 3+ cross-cutting behaviors | One wrapper around one thing |
The Interview Balance
The sweet spot in a machine coding interview:
- Under-engineered: God class, no interfaces, magic strings everywhere. Fails on βdesign qualityβ and βextensibility.β
- Over-engineered: 15 classes, 8 interfaces, 0 working features. Fails on βworking code.β
- Just right: 6-10 classes, 2-3 interfaces where variation exists, working demo, clean code. Scores well on all criteria.
Interview Questions
-
βIs this pattern necessary here?β β Be ready to defend OR simplify. βI used Strategy because the problem mentions three allocation methodsβ is good. βI used Strategy because you might want different algorithms somedayβ is over-engineering.
-
βWhat would you add with more time?β β This is your chance to mention the patterns you deliberately skipped. βIβd extract a Strategy for pricing if we needed tiered rates.β Shows awareness without wasted effort.
-
βWhy didnβt you use X pattern?β β βBecause I only have one variant, and adding the abstraction would cost 15 minutes I need for the core flow. If you ask me to add a second variant, Iβll extract the interface then.β
-
βHow do you decide when to abstract?β β Rule of Three. Or when the interviewer explicitly asks for multiple variants. Until then, concrete code that works beats abstract code that doesnβt.
See It in Action
Every LLD problem requires this judgment. Watch for the balance in Parking Lot and Splitwise.