Bird
Raised Fist0
LLDsystem_design~7 mins

Object-oriented design principles in LLD - System Design Guide

Choose your learning style10 modes available
Problem Statement
When software classes are tightly coupled, hard to change, or have unclear responsibilities, the code becomes fragile and difficult to maintain. This leads to bugs, slow development, and poor scalability as the system grows.
Solution
Object-oriented design principles guide how to organize classes and objects so they are easy to understand, extend, and maintain. They promote clear responsibilities, loose coupling, and reusable code by following simple rules during design.
Architecture
Class A
(Single Resp)
→Class B
↓
Interface I
(Interface Seg)

This diagram shows how different object-oriented design principles relate: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion, connected through class and interface relationships.

Trade-offs
✓ Pros
→
Improves code maintainability by clearly defining class responsibilities.
→
Enables easier extension of features without modifying existing code.
→
Reduces bugs by promoting loose coupling and clear interfaces.
→
Facilitates teamwork by making code easier to understand and modify.
✗ Cons
→
Requires upfront design effort and understanding of principles.
→
May introduce more classes and interfaces, increasing initial complexity.
→
Over-applying principles can lead to over-engineering and unnecessary abstraction.
Use when building systems expected to grow in size or complexity, or when multiple developers work on the codebase to ensure maintainability and scalability.
Avoid strict application in small, simple scripts or prototypes where speed of development is more important than long-term maintainability.
Real World Examples
Amazon
Uses Single Responsibility and Dependency Inversion principles to build modular microservices that can evolve independently without breaking others.
Netflix
Applies Open/Closed and Interface Segregation principles to add new streaming features without changing existing stable code.
Uber
Employs Liskov Substitution to ensure different vehicle types can be used interchangeably in ride dispatch logic.
Code Example
The before code mixes data processing and file saving in one class, violating Single Responsibility. The after code separates these concerns into different classes and uses abstract base classes to invert dependencies, making the design more modular and testable.
LLD
### Before applying principles (violating Single Responsibility and Dependency Inversion):

class Report:
    def __init__(self, data):
        self.data = data

    def calculate_statistics(self):
        # complex calculation logic
        pass

    def save_to_file(self, filename):
        with open(filename, 'w') as f:
            f.write(str(self.data))


### After applying principles:

from abc import ABC, abstractmethod

class DataProcessor(ABC):
    @abstractmethod
    def calculate_statistics(self):
        pass

class Report(DataProcessor):
    def __init__(self, data):
        self.data = data

    def calculate_statistics(self):
        # complex calculation logic
        pass

class FileSaver(ABC):
    @abstractmethod
    def save(self, filename):
        pass

class ReportFileSaver(FileSaver):
    def __init__(self, report):
        self.report = report

    def save(self, filename):
        with open(filename, 'w') as f:
            f.write(str(self.report.data))
OutputSuccess
Alternatives
Procedural Programming
Focuses on functions and procedures rather than objects and classes.
Use when: Choose when the problem domain is simple and does not require complex state management or extensibility.
Functional Programming
Emphasizes pure functions and immutable data instead of mutable objects and inheritance.
Use when: Choose when you want to avoid side effects and improve concurrency safety.
Summary
Object-oriented design principles help create code that is easier to maintain, extend, and understand.
They promote clear responsibilities, loose coupling, and reliance on abstractions rather than concrete implementations.
Applying these principles thoughtfully improves software quality and team productivity over time.

Practice

(1/5)
1. Which of the following best describes the Single Responsibility Principle (SRP) in object-oriented design?
easy
A. Classes should be tightly coupled to improve performance.
B. A class should inherit from multiple classes to reuse code.
C. Objects should be able to change their behavior at runtime.
D. A class should have only one reason to change, meaning it should have only one job.

Solution

  1. Step 1: Understand SRP definition

    The Single Responsibility Principle states that a class should have only one reason to change, meaning it should focus on a single job or responsibility.
  2. Step 2: Evaluate options against SRP

    A class should have only one reason to change, meaning it should have only one job. matches this definition exactly. Options A, B, and C describe other concepts or incorrect ideas.
  3. Final Answer:

    A class should have only one reason to change, meaning it should have only one job. -> Option D
  4. Quick Check:

    Single Responsibility Principle = One class, one job [OK]
Hint: SRP means one class, one responsibility [OK]
Common Mistakes:
  • Confusing SRP with inheritance rules
  • Thinking SRP allows multiple unrelated jobs in one class
  • Believing tight coupling is good design
2. Which syntax correctly demonstrates the Open/Closed Principle in class design?
easy
A. class Shape { void draw() {} } class Circle { void draw() { /* draw circle */ } }
B. class Shape { void draw() {} } class Circle extends Shape { void draw() { /* draw circle */ } }
C. class Shape { void draw() {} } class Circle extends Shape { void draw() { super.draw(); } }
D. class Shape { void draw() {} } class Circle extends Shape { void draw() { /* draw circle */ } } class Square extends Circle {}

Solution

  1. Step 1: Understand Open/Closed Principle (OCP)

    OCP means software entities like classes should be open for extension but closed for modification. This means you can add new behavior by extending classes without changing existing code.
  2. Step 2: Analyze options for OCP compliance

    class Shape { void draw() {} } class Circle extends Shape { void draw() { /* draw circle */ } } shows a base class Shape and a subclass Circle that extends Shape and overrides draw(), adding new behavior without modifying Shape. This follows OCP. class Shape { void draw() {} } class Circle { void draw() { /* draw circle */ } } lacks inheritance, so no extension. class Shape { void draw() {} } class Circle extends Shape { void draw() { super.draw(); } } calls super.draw() but does not add new behavior. class Shape { void draw() {} } class Circle extends Shape { void draw() { /* draw circle */ } } class Square extends Circle {} extends Circle with Square, which is allowed but less clear and may break OCP if Square changes Circle behavior.
  3. Final Answer:

    class Shape { void draw() {} } class Circle extends Shape { void draw() { /* draw circle */ } } -> Option B
  4. Quick Check:

    Open/Closed = Extend by subclassing, no base change [OK]
Hint: OCP means extend classes, don't modify them [OK]
Common Mistakes:
  • Ignoring inheritance for extension
  • Modifying base class instead of extending
  • Confusing method overriding with no added behavior
3. Consider the following Python code snippet applying the Liskov Substitution Principle (LSP):
class Bird:
    def fly(self):
        return "Flying"

class Penguin(Bird):
    def fly(self):
        return "Cannot fly"

bird = Bird()
penguin = Penguin()
print(bird.fly())
print(penguin.fly())

What is the output of this code?
medium
A. "Flying" followed by "Cannot fly"
B. Both print "Flying"
C. Runtime error because Penguin cannot fly
D. Both print "Cannot fly"

Solution

  1. Step 1: Understand code behavior

    Bird class has fly() returning "Flying". Penguin inherits Bird but overrides fly() to return "Cannot fly". bird.fly() prints "Flying". penguin.fly() prints "Cannot fly".
  2. Step 2: Predict output lines

    First print outputs "Flying". Second print outputs "Cannot fly" as per Penguin's override.
  3. Final Answer:

    "Flying" followed by "Cannot fly" -> Option A
  4. Quick Check:

    Subclass override changes output correctly [OK]
Hint: Subclass method overrides parent method output [OK]
Common Mistakes:
  • Assuming subclass must behave exactly like parent
  • Expecting runtime error for overridden method
  • Confusing method override with inheritance
4. Identify the problem in the following Java code violating the Dependency Inversion Principle (DIP):
class LightBulb {
    void turnOn() { System.out.println("Light on"); }
    void turnOff() { System.out.println("Light off"); }
}

class Switch {
    private LightBulb bulb = new LightBulb();
    void operate(boolean on) {
        if(on) bulb.turnOn();
        else bulb.turnOff();
    }
}
medium
A. Switch class should create multiple LightBulb instances.
B. LightBulb class should not have turnOn and turnOff methods.
C. Switch class directly depends on concrete LightBulb class instead of an abstraction.
D. Switch class should inherit from LightBulb.

Solution

  1. Step 1: Understand Dependency Inversion Principle (DIP)

    DIP states that high-level modules should not depend on low-level modules directly; both should depend on abstractions (interfaces or abstract classes).
  2. Step 2: Analyze code for DIP violation

    Switch class creates and uses a concrete LightBulb instance directly, tightly coupling Switch to LightBulb. This violates DIP because Switch depends on a concrete class, not an abstraction.
  3. Final Answer:

    Switch class directly depends on concrete LightBulb class instead of an abstraction. -> Option C
  4. Quick Check:

    DIP = Depend on abstractions, not concrete classes [OK]
Hint: High-level classes should depend on interfaces, not concrete classes [OK]
Common Mistakes:
  • Thinking methods in LightBulb are wrong
  • Believing multiple instances are needed
  • Confusing inheritance with dependency
5. You are designing a payment system that must support multiple payment methods (credit card, PayPal, crypto). Which object-oriented design principle helps you add new payment methods without changing existing code, and how should you apply it?
hard
A. Use the Open/Closed Principle by defining a Payment interface and implementing new payment classes for each method.
B. Use the Single Responsibility Principle by putting all payment logic in one PaymentProcessor class.
C. Use the Liskov Substitution Principle by making all payment classes inherit from a base Payment class but override methods differently.
D. Use the Dependency Inversion Principle by making payment classes depend on concrete implementations.

Solution

  1. Step 1: Identify the need for extensibility

    The system must allow adding new payment methods without modifying existing code, which is a classic use case for the Open/Closed Principle (OCP).
  2. Step 2: Apply OCP with interface and implementations

    Define a Payment interface with common methods. Implement each payment method (credit card, PayPal, crypto) as separate classes implementing this interface. This allows adding new methods by creating new classes without changing existing ones.
  3. Final Answer:

    Use the Open/Closed Principle by defining a Payment interface and implementing new payment classes for each method. -> Option A
  4. Quick Check:

    OCP = Add new classes, no existing code change [OK]
Hint: OCP means add new classes, don't modify old ones [OK]
Common Mistakes:
  • Putting all logic in one class violating SRP
  • Confusing inheritance with extensibility
  • Depending on concrete classes instead of abstractions
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard