Skip to content

DRY and YAGNI

TL;DR: DRY means extracting shared logic when the same code appears in 3+ places -- but not before. YAGNI means building only what the requirements ask for right now. Together they keep your LLD interview answer focused: no duplicated logic, no speculative features, no premature abstractions.

DRY: Don't Repeat Yourself

DRY says that every piece of knowledge should have a single, authoritative representation in the system. When you copy-paste logic, a future change needs to happen in every copy -- and you will forget one of them.

When to Extract

The rule of thumb: if the same logic appears in 3+ places, extract it. Two occurrences might be a coincidence. Three is a pattern.

// BAD: Validation duplicated across methods
class UserService {
    public void createUser(String email, String name) {
        if (email == null || !email.contains("@") || email.length() > 254) {
            throw new IllegalArgumentException("Invalid email");
        }
        // ... create user
    }

    public void updateEmail(String userId, String newEmail) {
        if (newEmail == null || !newEmail.contains("@") || newEmail.length() > 254) {
            throw new IllegalArgumentException("Invalid email");
        }
        // ... update email
    }

    public void sendInvite(String email) {
        if (email == null || !email.contains("@") || email.length() > 254) {
            throw new IllegalArgumentException("Invalid email");
        }
        // ... send invite
    }
}
// GOOD: Validation extracted
class UserService {
    private void validateEmail(String email) {
        if (email == null || !email.contains("@") || email.length() > 254) {
            throw new IllegalArgumentException("Invalid email");
        }
    }

    public void createUser(String email, String name) {
        validateEmail(email);
        // ... create user
    }

    public void updateEmail(String userId, String newEmail) {
        validateEmail(newEmail);
        // ... update email
    }

    public void sendInvite(String email) {
        validateEmail(email);
        // ... send invite
    }
}
// GOOD: C++ equivalent
class UserService {
private:
    void validateEmail(const std::string& email) const {
        if (email.empty() || email.find('@') == std::string::npos
            || email.size() > 254) {
            throw std::invalid_argument("Invalid email");
        }
    }

public:
    void createUser(const std::string& email, const std::string& name) {
        validateEmail(email);
        // ... create user
    }

    void updateEmail(const std::string& userId, const std::string& newEmail) {
        validateEmail(newEmail);
        // ... update email
    }

    void sendInvite(const std::string& email) {
        validateEmail(email);
        // ... send invite
    }
};
# GOOD: Python equivalent
class UserService:
    def _validate_email(self, email: str) -> None:
        if not email or "@" not in email or len(email) > 254:
            raise ValueError("Invalid email")

    def create_user(self, email: str, name: str) -> None:
        self._validate_email(email)
        # ... create user

    def update_email(self, user_id: str, new_email: str) -> None:
        self._validate_email(new_email)
        # ... update email

    def send_invite(self, email: str) -> None:
        self._validate_email(email)
        # ... send invite

When NOT to Extract: The False DRY Trap

Two pieces of code can look identical today but serve different purposes. Forcing them to share an implementation couples things that should evolve independently.

// Two methods that LOOK similar but serve different domains
class OrderService {
    public double calculateShippingDiscount(Order order) {
        if (order.getTotal() > 10000) return 0.10;  // 10% off shipping
        if (order.getTotal() > 5000) return 0.05;
        return 0.0;
    }
}

class TaxService {
    public double calculateTaxReduction(Order order) {
        if (order.getTotal() > 10000) return 0.10;  // 10% tax reduction
        if (order.getTotal() > 5000) return 0.05;
        return 0.0;
    }
}

These look identical, and a DRY purist might extract a shared calculateDiscount method. But shipping discounts and tax reductions are governed by different business rules. Next month, shipping discounts might change to be based on weight, while tax reductions stay based on total. If you merged them, you now have to pull them apart again.

Interview tip: If you notice duplication, ask yourself: "Do these change for the same reason?" If yes, extract. If they just happen to look the same today, leave them separate.

The Tension Between DRY and KISS

DRY can conflict with KISS. Extracting shared code sometimes means adding a new class, a new parameter, or a new level of indirection. When DRY and KISS conflict, KISS usually wins in an interview setting.

// Over-applying DRY: forcing shared code where it hurts readability
class Validator {
    // "Reusable" validator that handles everything
    public void validate(Object value, String fieldName, boolean required,
                         int minLength, int maxLength, String regex) {
        if (required && value == null) throw new IllegalArgumentException(fieldName + " required");
        if (value instanceof String s) {
            if (s.length() < minLength) throw new IllegalArgumentException(fieldName + " too short");
            if (s.length() > maxLength) throw new IllegalArgumentException(fieldName + " too long");
            if (regex != null && !s.matches(regex)) throw new IllegalArgumentException(fieldName + " invalid");
        }
    }
}

// Callers become unreadable
validator.validate(email, "email", true, 3, 254, ".*@.*");
validator.validate(name, "name", true, 1, 100, null);
validator.validate(bio, "bio", false, 0, 500, null);

A simple validateEmail() method is clearer than a generic validator with six parameters. Start with inline validation. If you see the same validation appearing three times, extract that specific validation into a named method -- not a generic framework.

YAGNI: You Aren't Gonna Need It

YAGNI says: don't build it until you need it. In an interview, the "requirements" are what the interviewer tells you. If they didn't mention valet parking, don't build valet parking.

The YAGNI Violation in Interviews

// BAD: The interviewer said "Design a parking lot with cars and motorcycles"
// Candidate builds:
class ParkingLot {
    private ValetService valetService;
    private EVChargingManager chargingManager;
    private LoyaltyPointsTracker loyaltyTracker;
    private ParkingReservationSystem reservationSystem;
    // ...
}

None of those features were in the requirements. The candidate spent 15 minutes on features the interviewer didn't ask for, and now has 10 minutes left to implement the core logic.

// GOOD: Build what was asked
class ParkingLot {
    private final List<ParkingFloor> floors;

    public ParkingSpot parkVehicle(Vehicle vehicle) {
        for (ParkingFloor floor : floors) {
            ParkingSpot spot = floor.findAvailableSpot(vehicle.getType());
            if (spot != null) {
                floor.occupySpot(spot, vehicle);
                return spot;
            }
        }
        throw new ParkingFullException("No available spot for " + vehicle.getType());
    }

    public void removeVehicle(ParkingSpot spot) {
        spot.getFloor().freeSpot(spot);
    }
}
// GOOD: C++ equivalent
class ParkingLot {
    std::vector<ParkingFloor> floors;

public:
    ParkingSpot* parkVehicle(const Vehicle& vehicle) {
        for (auto& floor : floors) {
            ParkingSpot* spot = floor.findAvailableSpot(vehicle.getType());
            if (spot) {
                floor.occupySpot(spot, vehicle);
                return spot;
            }
        }
        throw ParkingFullException("No available spot");
    }

    void removeVehicle(ParkingSpot* spot) {
        spot->getFloor()->freeSpot(spot);
    }
};
# GOOD: Python equivalent
class ParkingLot:
    def __init__(self, floors: list[ParkingFloor]):
        self.floors = floors

    def park_vehicle(self, vehicle: Vehicle) -> ParkingSpot:
        for floor in self.floors:
            spot = floor.find_available_spot(vehicle.vehicle_type)
            if spot:
                floor.occupy_spot(spot, vehicle)
                return spot
        raise ParkingFullException(
            f"No available spot for {vehicle.vehicle_type}")

    def remove_vehicle(self, spot: ParkingSpot) -> None:
        spot.floor.free_spot(spot)

Design for Extension, Don't Build for Extension

YAGNI does not mean writing rigid code. It means implementing only what's needed while keeping the design open to change.

When the interviewer asks "how would you handle EV charging stations?" -- the correct move is:

  1. Talk through it. "I'd add an EVChargingSpot that extends ParkingSpot and a ChargingManager that tracks which spots have active charging sessions."
  2. Show that your current design doesn't block it. "Since we already have ParkingSpot as a separate class, adding a subtype is straightforward."
  3. Don't build it unless they ask you to.

This shows more design maturity than building it upfront. You're demonstrating that you can think about extensibility without prematurely committing to it.

Interview tip: When the interviewer says "what if we need to support X?" -- treat this as a discussion question, not a build request. Talk about how your design would accommodate X. Only implement it if they explicitly say "go ahead and add that."

DRY + YAGNI Together

The two principles reinforce each other:

  • DRY says: "Don't write the same logic twice."
  • YAGNI says: "Don't write logic you don't need yet."
  • Together: "Write each needed piece of logic exactly once."

In practice for an interview:

  1. Start with the simplest implementation that satisfies the requirements.
  2. If you notice you're writing the same validation or lookup logic a third time, extract it into a method.
  3. If you catch yourself building something the interviewer didn't ask for, stop.

Quick Recap

Principle Rule Interview application
DRY Extract shared logic when it appears 3+ times Pull out common validation, lookups, or calculations into named methods
False DRY Don't merge code that looks similar but serves different purposes Ask "do these change for the same reason?" before extracting
DRY vs KISS When they conflict, prefer simplicity A named method beats a generic framework with 6 parameters
YAGNI Build only what the requirements ask for Don't add valet parking, EV charging, or loyalty points unless asked
Extension Design for extension, don't implement for extension Talk through extensions verbally when the interviewer asks "what if?"