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.
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.
- 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.
- 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.
- 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.
- Helper/Utility Classes: For common, stateless operations that don't belong to a specific business object (e.g., StringUtils, DateUtils, ValidationUtils).
- 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.
- 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.
Overdoing DRY
For a very small amount of duplication, creating a complex abstraction can make the code harder to read and follow.
- Premature abstraction leads to tight coupling and hard-to-maintain code.
- Sometimes, similar-looking code is conceptually different and should remain separate.
A common guideline is the Rule of Three: don't abstract something until you see it repeated three times.
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.
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.
A class should only talk to its immediate friends, not strangers.
An object should only interact with:
- Itself
- Its own fields
- Objects passed as arguments
- 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!)
Prefer building functionality using composition (has-a relationship) instead of inheritance (is-a relationship), unless inheritance provides a clear and simple advantage.