AlgoMaster Logo

Design Principles - Crash Course

6 min readUpdated July 24, 2025
Listen to this chapter
Unlock Audio

Design principles are the guiding philosophies behind writing clean, maintainable, and extensible code. While Object-Oriented Programming gives you the tools, design principles tell you how to use them wisely.

In this crash course, we’ll cover the 10 most important design principles you must know for Low-Level Design interviews.

1. DRY (Don't Repeat Yourself)

Every piece of knowledge or logic should have a single, unambiguous, authoritative representation in your codebase.

This principle aims to eliminate redundancy and duplication in:

  • Code logic
  • Data structures
  • Configuration
  • Documentation

How to Apply the DRY Principle

Here are the common ways developers implement DRY, from simple to complex.

  1. Functions and Methods: The most basic form of DRY. If you find yourself copying and pasting a block of code, extract it into a function.
  2. Classes and Objects (Encapsulation): Group related data and the behavior that acts on that data. Instead of passing the same 5 variables to 10 different functions, create a class that holds those variables as properties and has methods to operate on them.
  3. Constants and Configuration Files: Never hardcode values like strings or numbers ("magic numbers"). Store them in a single place, like a Constants class or a .properties/.json configuration file. This centralizes all the "magic" values that might need to change.
  4. Helper/Utility Classes: For common, stateless operations that don't belong to a specific business object (e.g., StringUtils, DateUtils, ValidationUtils).
  5. Inheritance and Composition: These OOP techniques are powerful tools for code reuse. A base Vehicle class can define a move() method that subclasses like Car and Motorcycle can reuse.
  6. Service Layers: In larger applications, extract core business processes into dedicated service classes (e.g., NotificationService, PricingService). This ensures that any part of the application that needs to send a notification or calculate a price does so in the exact same way.

Example

❌ Without DRY (Duplicated Logic)

Problem: The entire logic for calculating the total is duplicated. If the free shipping threshold changes to $150, you have to remember to change it in both classes.

✅ With DRY

We extract this logic into a single, authoritative place. A PricingService is a great candidate.

Now, if any pricing rule changes, we only need to modify the PricingService class. The system is more maintainable, reliable, and easier to understand.

2. KISS (Keep It Simple, Stupid)

Most systems work best if they are kept simple rather than made complicated. Therefore, simplicity should be a key goal in design, and unnecessary complexity should be avoided.

What does "simple" mean in this context?

  • Easy to Understand: A junior developer can look at the code and quickly grasp what it's doing without needing a 2-hour lecture. The cognitive load is low.
  • Easy to Reason About: You can predict the behavior of the code under different conditions without having to trace through dozens of files and layers of abstraction.
  • Direct and Explicit: The code does what it looks like it's doing. There are no hidden side effects or overly "clever" tricks.

How to Apply the KISS Principle

Applying KISS is a conscious effort to resist complexity. Here's how you do it:

  • Start with the "Dumbest" Thing That Could Possibly Work: When faced with a problem, don't immediately jump to complex design patterns. First, think of the most straightforward, brute-force solution. Then, only add complexity if it's genuinely required by the problem's constraints (e.g., for performance, scalability, or extensibility).
  • Prefer Clarity Over "Cleverness": Avoid obscure language features, complex one-liners, or "magic" code that saves a few keystrokes at the cost of readability.
  • Question Every Layer of Abstraction: Abstractions are essential tools, but they are not free. Each layer adds mental overhead. Ask yourself: "Does this new class/interface/module truly simplify the system, or does it just hide the complexity one layer deeper?"
  • Refactor Towards Simplicity: Your first draft won't always be the simplest. As you understand the problem better, continuously refactor your code to make it simpler. Remove dead code, merge functions that do almost the same thing, and clarify variable names.

3. YAGNI (You Ain’t Gonna Need It)

Always implement things when you actually need them, never when you just foresee that you need them.

Its goal is to prevent overengineering and wasted effort.

4. Law of Demeter (Principle of Least Knowledge)

A class should only talk to its immediate friends, not strangers.

An object should only interact with:

  1. Itself
  2. Its own fields
  3. Objects passed as arguments
  4. Objects it creates

❌ What Violates the Law of Demeter?

👎 Method Chaining That Reaches Too Far

Here, the calling code knows too much about the internal structure of Order → Customer → Address → City. If anything changes in the chain, multiple classes may break.

✅ Applying Law of Demeter

✔ Better Design: Expose Only What’s Needed

Rule of Thumb for LoD

A method M of object O should only call:

  • Methods of O (itself)
  • Methods of objects passed into M
  • Methods of objects that O instantiates
  • Methods of its own fields (but not fields of those fields!)

Favor Composition Over Inheritance

Prefer building functionality using composition (has-a relationship) instead of inheritance (is-a relationship), unless inheritance provides a clear and simple advantage.

Coupling and Cohesion

Encapsulate What Varies

Program to an Interface, Not an Implementation