Bird
Raised Fist0
LLDsystem_design~25 mins

Dependency Inversion Principle in LLD - System Design Exercise

Choose your learning style10 modes available
Design: Dependency Inversion Principle Application
Focus on the design of the payment processing system applying the Dependency Inversion Principle. Out of scope are actual payment gateway integrations and UI design.
Functional Requirements
FR1: Design a simple payment processing system that supports multiple payment methods (e.g., credit card, PayPal, bank transfer).
FR2: The system should allow easy addition of new payment methods without changing existing code.
FR3: High-level modules (payment processing logic) should not depend on low-level modules (specific payment method implementations).
FR4: Both should depend on abstractions (interfaces).
FR5: Abstractions should not depend on details; details should depend on abstractions.
Non-Functional Requirements
NFR1: The system should be modular and maintainable.
NFR2: It should be easy to extend with new payment methods.
NFR3: Latency for processing a payment should be under 500ms.
NFR4: The system should be testable with mock payment methods.
Think Before You Design
Questions to Ask
❓ Question 1
❓ Question 2
❓ Question 3
❓ Question 4
❓ Question 5
Key Components
PaymentProcessor interface (abstraction)
Concrete payment method classes (e.g., CreditCardPayment, PayPalPayment)
High-level PaymentService that uses PaymentProcessor interface
Dependency injection mechanism to provide payment method implementations
Mock payment processors for testing
Design Patterns
Dependency Injection
Strategy Pattern
Interface Segregation
Inversion of Control
Reference Architecture
  +-------------------+       implements       +-----------------------+
  |  PaymentProcessor  |<-----------------------|  CreditCardPayment     |
  +-------------------+                        +-----------------------+
           ^                                         ^
           |                                         |
           |                                         |
  +-------------------+       uses              +-----------------------+
  |  PaymentService    |----------------------->|  PayPalPayment        |
  +-------------------+                        +-----------------------+
           |
           | uses
           v
  +-------------------+
  |  Client Code       |
  +-------------------+
Components
PaymentProcessor
Interface/Abstract Class
Defines abstraction for payment methods that high-level modules depend on.
CreditCardPayment, PayPalPayment, BankTransferPayment
Concrete Classes
Implement specific payment methods depending on PaymentProcessor abstraction.
PaymentService
Class
High-level module that processes payments using PaymentProcessor interface.
Dependency Injection Container
DI Framework or Manual Injection
Provides concrete payment method implementations to PaymentService without tight coupling.
MockPaymentProcessor
Mock Class
Used for testing PaymentService independently of real payment methods.
Request Flow
1. Client calls PaymentService to process a payment.
2. PaymentService depends on PaymentProcessor interface, not concrete classes.
3. Dependency Injection provides a concrete payment method implementation to PaymentService.
4. PaymentService calls processPayment() on the injected PaymentProcessor.
5. Concrete payment method executes payment logic.
6. Result is returned to PaymentService and then to Client.
Database Schema
Not applicable as this is a low-level design focusing on code structure and dependencies.
Scaling Discussion
Bottlenecks
Adding new payment methods may cause code duplication if not designed properly.
Tight coupling if abstractions are not well defined.
Testing difficulty if dependencies are hard-coded.
Performance overhead if dependency injection is misused.
Solutions
Use clear, minimal interfaces to reduce coupling and duplication.
Apply Dependency Injection to decouple modules and improve testability.
Use design patterns like Strategy to encapsulate payment algorithms.
Profile and optimize dependency injection usage to minimize overhead.
Interview Tips
Time: Spend 10 minutes explaining the principle and requirements, 20 minutes designing the system with diagrams and code structure, 10 minutes discussing scaling and testing.
Explain the Dependency Inversion Principle in simple terms.
Show how high-level modules depend on abstractions, not details.
Demonstrate how new payment methods can be added without changing existing code.
Discuss dependency injection and its role in decoupling.
Highlight testability improvements using mocks.
Mention potential bottlenecks and how to address them.

Practice

(1/5)
1. What is the main goal of the Dependency Inversion Principle in system design?
easy
A. To make high-level modules independent of low-level modules by using abstractions
B. To tightly couple all modules for better performance
C. To avoid using interfaces or abstractions in the system
D. To make low-level modules dependent on high-level modules

Solution

  1. Step 1: Understand the principle's purpose

    The Dependency Inversion Principle aims to reduce direct dependencies between high-level and low-level modules.
  2. Step 2: Identify the correct approach

    It achieves this by introducing abstractions or interfaces so that high-level modules do not depend on low-level module details.
  3. Final Answer:

    To make high-level modules independent of low-level modules by using abstractions -> Option A
  4. Quick Check:

    Dependency Inversion = High-level independent [OK]
Hint: Remember: High-level modules depend on abstractions, not details [OK]
Common Mistakes:
  • Thinking low-level modules depend on high-level modules
  • Ignoring the use of interfaces or abstractions
  • Assuming tight coupling improves design
2. Which of the following code snippets correctly demonstrates the Dependency Inversion Principle?
easy
A. class PaymentProcessor { void process(Payment payment) { payment.pay(); } } interface Payment { void pay(); } class CreditCardPayment implements Payment { void pay() { /* pay with card */ } }
B. class PaymentProcessor { void process(CreditCardPayment payment) { payment.pay(); } } class CreditCardPayment { void pay() { /* pay with card */ } }
C. class PaymentProcessor { void process() { CreditCardPayment payment = new CreditCardPayment(); payment.pay(); } } class CreditCardPayment { void pay() { /* pay with card */ } }
D. interface PaymentProcessor { void process(); } class CreditCardPayment implements PaymentProcessor { void process() { /* pay with card */ } }

Solution

  1. Step 1: Identify use of abstraction

    class PaymentProcessor { void process(Payment payment) { payment.pay(); } } interface Payment { void pay(); } class CreditCardPayment implements Payment { void pay() { /* pay with card */ } } uses an interface Payment that both PaymentProcessor and CreditCardPayment depend on, following Dependency Inversion.
  2. Step 2: Check other options for tight coupling

    Snippets that tightly couple PaymentProcessor to CreditCardPayment by direct reference or instantiation violate DIP. interface PaymentProcessor { void process(); } class CreditCardPayment implements PaymentProcessor { void process() { /* pay with card */ } } reverses roles incorrectly.
  3. Final Answer:

    class PaymentProcessor { void process(Payment payment) { payment.pay(); } } interface Payment { void pay(); } class CreditCardPayment implements Payment { void pay() { /* pay with card */ } } -> Option A
  4. Quick Check:

    Use interfaces for dependency inversion [OK]
Hint: Look for interfaces or abstractions connecting modules [OK]
Common Mistakes:
  • Directly instantiating concrete classes in high-level modules
  • Not using interfaces or abstractions
  • Confusing interface roles
3. Given the following code, what will be the output?
interface Logger {
  void log(String message);
}

class ConsoleLogger implements Logger {
  public void log(String message) {
    System.out.println("Console: " + message);
  }
}

class Application {
  private Logger logger;
  public Application(Logger logger) {
    this.logger = logger;
  }
  public void run() {
    logger.log("App started");
  }
}

public class Main {
  public static void main(String[] args) {
    Logger logger = new ConsoleLogger();
    Application app = new Application(logger);
    app.run();
  }
}
medium
A. App started
B. Console: App started
C. Compilation error due to missing method
D. Runtime error due to null pointer

Solution

  1. Step 1: Trace object creation and method calls

    Main creates a ConsoleLogger and passes it to Application. Calling app.run() calls logger.log("App started").
  2. Step 2: Understand the log method behavior

    ConsoleLogger.log prints "Console: " plus the message, so output is "Console: App started".
  3. Final Answer:

    Console: App started -> Option B
  4. Quick Check:

    Logger prints with prefix "Console:" [OK]
Hint: Follow object creation and method calls step-by-step [OK]
Common Mistakes:
  • Ignoring the prefix in print statement
  • Assuming direct print of message without prefix
  • Confusing interface with implementation
4. Identify the problem in the following design violating the Dependency Inversion Principle:
class FileStorage {
  public void save(String data) {
    // save data to file
  }
}

class DataManager {
  private FileStorage storage = new FileStorage();
  public void saveData(String data) {
    storage.save(data);
  }
}
medium
A. DataManager should create FileStorage inside saveData method
B. FileStorage should not have a save method
C. There is no problem; design follows Dependency Inversion
D. DataManager depends directly on concrete FileStorage instead of an abstraction

Solution

  1. Step 1: Analyze dependencies between classes

    DataManager creates and depends directly on concrete FileStorage, causing tight coupling.
  2. Step 2: Check for abstraction usage

    There is no interface or abstraction between DataManager and FileStorage, violating Dependency Inversion Principle.
  3. Final Answer:

    DataManager depends directly on concrete FileStorage instead of an abstraction -> Option D
  4. Quick Check:

    High-level depends on abstraction, not concrete [OK]
Hint: Look for direct use of concrete classes in high-level modules [OK]
Common Mistakes:
  • Thinking method placement is the issue
  • Ignoring lack of interfaces
  • Assuming creating objects inside methods is correct
5. You are designing a notification system that can send messages via Email, SMS, or Push notifications. To follow the Dependency Inversion Principle, which design is best?
hard
A. Each notifier class depends on NotificationManager to get messages.
B. NotificationManager directly creates and calls methods on EmailNotifier, SMSNotifier, and PushNotifier classes.
C. Create an interface Notifier with method send(message), and implement it in EmailNotifier, SMSNotifier, and PushNotifier. The high-level NotificationManager depends on Notifier interface.
D. NotificationManager contains all sending logic inside itself without separate classes.

Solution

  1. Step 1: Identify high-level and low-level modules

    NotificationManager is high-level; EmailNotifier, SMSNotifier, PushNotifier are low-level modules.
  2. Step 2: Apply Dependency Inversion Principle

    High-level module should depend on abstraction (Notifier interface), not concrete classes. Create an interface Notifier with method send(message), and implement it in EmailNotifier, SMSNotifier, and PushNotifier. The high-level NotificationManager depends on Notifier interface. follows this by using interface and implementations.
  3. Final Answer:

    Use Notifier interface implemented by all notifiers; NotificationManager depends on interface -> Option C
  4. Quick Check:

    High-level depends on interface, not concrete classes [OK]
Hint: Use interfaces for all notification types to decouple [OK]
Common Mistakes:
  • High-level module directly creating concrete classes
  • Putting all logic in one class
  • Low-level modules depending on high-level modules
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