Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
⏱️ 8 min read

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


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:

  1. First occurrence: Write it directly. No abstraction.
  2. Second occurrence: Note the duplication, maybe extract a method. Still no pattern.
  3. 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

  1. Start concrete, go abstract – Write the specific code first. Extract when repetition appears.
  2. Make it work, make it right, make it fast – In that order. Working code first.
  3. Design for the current requirements – Not for hypothetical future requirements.
  4. Leave extension points – Use interfaces where you SEE variation. Don’t preemptively interface everything.
  5. 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:


Interview Questions

  1. β€œ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.

  2. β€œ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.

  3. β€œ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.”

  4. β€œ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.

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access