Skip to content

KISS

TL;DR: KISS (Keep It Simple, Stupid) is the most violated principle in LLD interviews. Candidates over-engineer to show off pattern knowledge -- factories, strategies, and decorators where a basic class and a loop would do. The interviewer is not impressed by complexity. They are impressed by clean, working solutions that solve the actual problem.

KISS: Keep It Simple

Why This Is the Most Important Lesson

KISS is the #1 most violated principle in LLD interviews. Here's what happens:

  1. Candidate reads "Design a Parking Lot"
  2. Candidate thinks "I need to show I know design patterns"
  3. Candidate builds a ParkingStrategyFactory that returns an IParkingStrategy that wraps a SpotAllocator that delegates to a FloorManager
  4. Interviewer thinks "This person would be a nightmare to work with"

The goal of an LLD interview is to produce a design that is correct, clear, and extensible enough. Not maximally extensible. Not pattern-showcase-ready. Just enough.

The Over-Engineering Trap

Example 1: Connect Four Win Checking

Connect Four has exactly four directions to check for a win: horizontal, vertical, and two diagonals. These directions will never change -- it's a physical board game.

// BAD: Strategy pattern for fixed directions
interface WinChecker {
    boolean checkWin(Board board, int row, int col);
}

class HorizontalWinChecker implements WinChecker {
    @Override
    public boolean checkWin(Board board, int row, int col) {
        // Check horizontal...
        return false;
    }
}

class VerticalWinChecker implements WinChecker {
    @Override
    public boolean checkWin(Board board, int row, int col) {
        // Check vertical...
        return false;
    }
}

class DiagonalUpWinChecker implements WinChecker { /* ... */ }
class DiagonalDownWinChecker implements WinChecker { /* ... */ }

class WinCheckerManager {
    private final List<WinChecker> checkers = List.of(
        new HorizontalWinChecker(),
        new VerticalWinChecker(),
        new DiagonalUpWinChecker(),
        new DiagonalDownWinChecker()
    );

    public boolean hasWon(Board board, int row, int col) {
        return checkers.stream().anyMatch(c -> c.checkWin(board, row, col));
    }
}

That's five classes and an interface to do what a single method can do. The directions are fixed. There will never be a fifth direction. This is not a case where the Strategy pattern adds value.

// GOOD: Simple loop with direction vectors
class Board {
    private final char[][] grid;
    private static final int[][] DIRECTIONS = {
        {0, 1},   // horizontal
        {1, 0},   // vertical
        {1, 1},   // diagonal down-right
        {1, -1}   // diagonal down-left
    };

    public boolean hasWon(int row, int col, char piece) {
        for (int[] dir : DIRECTIONS) {
            int count = 1;
            count += countInDirection(row, col, dir[0], dir[1], piece);
            count += countInDirection(row, col, -dir[0], -dir[1], piece);
            if (count >= 4) return true;
        }
        return false;
    }

    private int countInDirection(int row, int col, int dr, int dc, char piece) {
        int count = 0;
        int r = row + dr, c = col + dc;
        while (r >= 0 && r < grid.length && c >= 0 && c < grid[0].length
               && grid[r][c] == piece) {
            count++;
            r += dr;
            c += dc;
        }
        return count;
    }
}
// GOOD: C++ equivalent
class Board {
private:
    std::vector<std::vector<char>> grid;
    static constexpr int DIRECTIONS[4][2] = {
        {0, 1}, {1, 0}, {1, 1}, {1, -1}
    };

    int countInDirection(int row, int col, int dr, int dc, char piece) const {
        int count = 0;
        int r = row + dr, c = col + dc;
        while (r >= 0 && r < (int)grid.size() && c >= 0 && c < (int)grid[0].size()
               && grid[r][c] == piece) {
            count++;
            r += dr;
            c += dc;
        }
        return count;
    }

public:
    bool hasWon(int row, int col, char piece) const {
        for (const auto& dir : DIRECTIONS) {
            int count = 1;
            count += countInDirection(row, col, dir[0], dir[1], piece);
            count += countInDirection(row, col, -dir[0], -dir[1], piece);
            if (count >= 4) return true;
        }
        return false;
    }
};
# GOOD: Python equivalent
class Board:
    DIRECTIONS = [(0, 1), (1, 0), (1, 1), (1, -1)]

    def __init__(self, rows: int, cols: int):
        self.grid = [[' '] * cols for _ in range(rows)]

    def has_won(self, row: int, col: int, piece: str) -> bool:
        for dr, dc in self.DIRECTIONS:
            count = 1
            count += self._count_in_direction(row, col, dr, dc, piece)
            count += self._count_in_direction(row, col, -dr, -dc, piece)
            if count >= 4:
                return True
        return False

    def _count_in_direction(self, row: int, col: int,
                            dr: int, dc: int, piece: str) -> int:
        count = 0
        r, c = row + dr, col + dc
        while (0 <= r < len(self.grid) and 0 <= c < len(self.grid[0])
               and self.grid[r][c] == piece):
            count += 1
            r += dr
            c += dc
        return count

One class. One method. Four direction vectors in an array. Done.

Example 2: Parking Lot Pricing

The problem says "cars pay $10/hr, motorcycles pay $5/hr, large vehicles pay $20/hr." Three fixed pricing tiers.

// BAD: Strategy pattern for 3 fixed rates
interface PricingStrategy {
    long calculatePrice(long durationMinutes);
}

class CarPricingStrategy implements PricingStrategy {
    @Override
    public long calculatePrice(long durationMinutes) {
        return (durationMinutes / 60) * 1000; // $10/hr in cents
    }
}

class MotorcyclePricingStrategy implements PricingStrategy { /* ... */ }
class LargeVehiclePricingStrategy implements PricingStrategy { /* ... */ }

class PricingStrategyFactory {
    public static PricingStrategy getStrategy(VehicleType type) {
        switch (type) {
            case CAR: return new CarPricingStrategy();
            case MOTORCYCLE: return new MotorcyclePricingStrategy();
            case LARGE: return new LargeVehiclePricingStrategy();
            default: throw new IllegalArgumentException("Unknown type");
        }
    }
}
// GOOD: A map
class PricingCalculator {
    private static final Map<VehicleType, Long> RATE_CENTS_PER_HOUR = Map.of(
        VehicleType.MOTORCYCLE, 500L,
        VehicleType.CAR, 1000L,
        VehicleType.LARGE, 2000L
    );

    public long calculatePrice(VehicleType type, long durationMinutes) {
        long rateCentsPerHour = RATE_CENTS_PER_HOUR.get(type);
        return (durationMinutes * rateCentsPerHour) / 60;
    }
}
// GOOD: C++ equivalent
class PricingCalculator {
    static const std::unordered_map<VehicleType, long>& getRates() {
        static const std::unordered_map<VehicleType, long> rates = {
            {VehicleType::MOTORCYCLE, 500},
            {VehicleType::CAR, 1000},
            {VehicleType::LARGE, 2000}
        };
        return rates;
    }

public:
    long calculatePrice(VehicleType type, long durationMinutes) const {
        long rateCentsPerHour = getRates().at(type);
        return (durationMinutes * rateCentsPerHour) / 60;
    }
};
# GOOD: Python equivalent
class PricingCalculator:
    RATE_CENTS_PER_HOUR = {
        VehicleType.MOTORCYCLE: 500,
        VehicleType.CAR: 1000,
        VehicleType.LARGE: 2000,
    }

    def calculate_price(self, vehicle_type: VehicleType,
                        duration_minutes: int) -> int:
        rate = self.RATE_CENTS_PER_HOUR[vehicle_type]
        return (duration_minutes * rate) // 60

Three lines of data in a map versus four classes and a factory. The map is easier to read, easier to change, and impossible to get wrong.

When Does Complexity Become Justified?

The time to add complexity is when simplicity stops working. Not before.

If the parking lot requirements say "pricing varies by time of day, with surge pricing on weekends, and loyalty discounts for frequent parkers" -- now the calculation logic is genuinely different per scenario. A Strategy pattern might make sense.

If your single class grows to 500 lines with 20 methods -- now it's time to split it up.

But start simple. Let the problem tell you when you need more structure.

Interview tip: If you catch yourself creating an interface with only one implementation, stop. You probably don't need the interface yet. If the interviewer later adds a requirement that demands polymorphism, you can refactor in 30 seconds. But starting with unnecessary abstractions wastes precious interview time and signals to the interviewer that you can't distinguish between essential and accidental complexity.

How to Apply KISS in an Interview

  1. Read the requirements literally. Don't invent requirements that aren't there.
  2. Start with the simplest thing that works. A single class with a few methods.
  3. Add structure only when it solves a real problem. If two things vary independently, separate them. If they don't, don't.
  4. When the interviewer asks "how would you extend this?" -- talk through the extension verbally. You don't need to build it unless they ask you to.

Quick Recap

Trap What candidates do What to do instead
Pattern showoff Use Strategy, Factory, Decorator everywhere Use the simplest construct that works
Premature abstraction Create interfaces with one implementation Add interfaces when a second implementation appears
Gold-plating Handle edge cases not in requirements Solve the stated problem first, extend if asked
Deep hierarchies 5 classes to do what 1 method can do Ask "does this indirection add value?"