0
0
LLDsystem_design~7 mins

Program to interface not implementation in LLD - System Design Guide

Choose your learning style9 modes available
Problem Statement
When code depends directly on concrete classes, changing implementations requires modifying all dependent code. This tight coupling makes the system fragile and hard to extend or test.
Solution
This pattern suggests coding against abstract interfaces rather than concrete implementations. Clients interact with interfaces, allowing implementations to change without affecting client code, promoting flexibility and easier maintenance.
Architecture
Client
Interface
ImplementationA

The diagram shows the client depending only on the interface, while multiple implementations can vary independently.

Trade-offs
✓ Pros
Enables swapping implementations without changing client code.
Improves testability by allowing mock implementations.
Reduces coupling, making code easier to maintain and extend.
✗ Cons
Introduces additional abstraction layers, which can increase complexity.
Requires upfront design of interfaces, which may slow initial development.
May lead to over-engineering if used unnecessarily in simple cases.
Use when multiple implementations are expected or when you want to isolate clients from changes in implementation. Suitable for medium to large codebases with evolving requirements.
Avoid when the system is very simple or unlikely to change, as the added abstraction may complicate the design unnecessarily.
Real World Examples
Amazon
Uses interfaces for payment processing to support multiple payment gateways without changing order processing code.
Netflix
Defines interfaces for data storage to switch between different databases or caches transparently.
Uber
Implements interfaces for different map providers, allowing easy replacement or addition of map services.
Code Example
The before code tightly couples OrderService to StripeProcessor, making it hard to change payment methods. The after code defines a PaymentProcessorInterface and programs to it, allowing OrderService to work with any payment processor implementation passed in, improving flexibility and testability.
LLD
### Before: Program to implementation (tight coupling)
class PaymentProcessor:
    def pay(self, amount):
        print(f"Processing payment of {amount} using Stripe")

class OrderService:
    def __init__(self):
        self.processor = PaymentProcessor()

    def checkout(self, amount):
        self.processor.pay(amount)

order = OrderService()
order.checkout(100)


### After: Program to interface (loose coupling)
from abc import ABC, abstractmethod

class PaymentProcessorInterface(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

class StripeProcessor(PaymentProcessorInterface):
    def pay(self, amount):
        print(f"Processing payment of {amount} using Stripe")

class PaypalProcessor(PaymentProcessorInterface):
    def pay(self, amount):
        print(f"Processing payment of {amount} using PayPal")

class OrderService:
    def __init__(self, processor: PaymentProcessorInterface):
        self.processor = processor

    def checkout(self, amount):
        self.processor.pay(amount)

stripe = StripeProcessor()
order = OrderService(stripe)
order.checkout(100)

paypal = PaypalProcessor()
order2 = OrderService(paypal)
order2.checkout(200)
OutputSuccess
Alternatives
Program to implementation
Clients depend directly on concrete classes rather than interfaces.
Use when: Use only in very simple or throwaway code where flexibility is not needed.
Dependency Injection
Focuses on how implementations are provided to clients, often used together with programming to interfaces.
Use when: Use when you want to manage object creation and dependencies externally for better modularity.
Summary
Programming to interface reduces coupling by making clients depend on abstractions, not concrete classes.
This approach improves flexibility, testability, and maintainability of code.
It is best used when multiple implementations or future changes are expected.