Dependency in OOP: Uses-A Relationship Guide

Updated 2026-03-11

TL;DR

Dependency is the weakest form of class relationship where one class temporarily uses another, typically through method parameters, local variables, or return types. Unlike association, the dependent class doesn’t store a reference to the other class as an instance variable. Understanding dependency helps you design loosely coupled systems that are easier to test and maintain.

Prerequisites: Basic understanding of classes and objects, familiarity with method parameters and return types, knowledge of instance variables versus local variables.

After this topic: Identify dependency relationships in code, distinguish dependencies from stronger relationships like association and composition, design classes with minimal dependencies to improve testability, and explain dependency injection as a technique for managing dependencies.

Core Concept

What is Dependency?

Dependency occurs when one class (the dependent) uses another class (the dependency) temporarily within a method, but doesn’t maintain a lasting relationship. The dependent class “knows about” the other class only during method execution.

Think of it like using a calculator app on your phone. You open it, perform a calculation, and close it. Your phone depends on the calculator temporarily, but doesn’t keep it running in the background.

How Dependencies Manifest in Code

Dependencies appear in three primary ways:

  1. Method Parameters: A class receives an object of another class as a parameter
  2. Local Variables: A class creates or uses an object of another class within a method scope
  3. Return Types: A method returns an object of another class

Why Dependencies Matter

Dependencies represent coupling between classes. High coupling makes code harder to:

  • Test: You need the dependency available to test the dependent class
  • Change: Modifying the dependency might break the dependent class
  • Reuse: The dependent class can’t work without access to its dependencies

Dependency vs. Association

The key difference: lifetime and storage.

  • Dependency: Temporary relationship, no instance variable storing the reference
  • Association: Lasting relationship, stored as an instance variable

If a class has self.other_object as an instance variable, that’s association. If it only uses other_object within a method, that’s dependency.

Dependency Direction

Dependencies have direction. If class A depends on class B, we say “A depends on B” or “A uses B”. The arrow in UML diagrams points from the dependent to the dependency, showing the direction of knowledge.

Visual Guide

Dependency Relationship Diagram

classDiagram
    class EmailService {
        +send_email(message: EmailMessage)
    }
    class EmailMessage {
        +to: string
        +subject: string
        +body: string
    }
    EmailService ..> EmailMessage : depends on
    note for EmailService "Uses EmailMessage temporarily\nas a method parameter"

Dependency shown with a dashed arrow. EmailService depends on EmailMessage but doesn’t store it.

Dependency vs Association

classDiagram
    class ReportGenerator {
        +generate(data: DataSource) Report
    }
    class DataSource {
        +fetch_data()
    }
    class Report {
        +content: string
    }
    class Logger {
        -log_file: File
        +write(message)
    }
    class File {
        +write()
    }
    ReportGenerator ..> DataSource : dependency
    ReportGenerator ..> Report : dependency
    Logger --> File : association
    note for ReportGenerator "Temporary usage only"

Dependency (dashed) is temporary; Association (solid) is persistent storage.

Examples

Example 1: Method Parameter Dependency

class EmailMessage:
    def __init__(self, to, subject, body):
        self.to = to
        self.subject = subject
        self.body = body

class EmailService:
    """Depends on EmailMessage through method parameter"""
    
    def send_email(self, message: EmailMessage) -> bool:
        # EmailService uses EmailMessage temporarily
        print(f"Sending to: {message.to}")
        print(f"Subject: {message.subject}")
        print(f"Body: {message.body}")
        return True

# Usage
service = EmailService()
msg = EmailMessage("user@example.com", "Hello", "Test message")
service.send_email(msg)

# Expected Output:
# Sending to: user@example.com
# Subject: Hello
# Body: Test message

Key Point: EmailService doesn’t store EmailMessage as an instance variable. It only uses it within the send_email method.

Java Equivalent:

public class EmailService {
    public boolean sendEmail(EmailMessage message) {
        System.out.println("Sending to: " + message.getTo());
        return true;
    }
}

Example 2: Local Variable Dependency

class DatabaseConnection:
    def __init__(self, connection_string):
        self.connection_string = connection_string
    
    def execute(self, query):
        return f"Executing: {query}"

class UserRepository:
    """Depends on DatabaseConnection through local variable creation"""
    
    def __init__(self, connection_string):
        self.connection_string = connection_string
    
    def get_user(self, user_id: int) -> dict:
        # Creates dependency locally - not stored as instance variable
        db = DatabaseConnection(self.connection_string)
        result = db.execute(f"SELECT * FROM users WHERE id = {user_id}")
        print(result)
        return {"id": user_id, "name": "John Doe"}

# Usage
repo = UserRepository("localhost:5432")
user = repo.get_user(123)
print(user)

# Expected Output:
# Executing: SELECT * FROM users WHERE id = 123
# {'id': 123, 'name': 'John Doe'}

Key Point: DatabaseConnection is created inside the method and doesn’t persist beyond that method call.

Example 3: Return Type Dependency

class Report:
    def __init__(self, title, content):
        self.title = title
        self.content = content
    
    def display(self):
        return f"{self.title}\n{'-' * len(self.title)}\n{self.content}"

class ReportGenerator:
    """Depends on Report through return type"""
    
    def generate_sales_report(self, sales_data: list) -> Report:
        # Creates and returns Report - dependency through return type
        total = sum(sales_data)
        content = f"Total Sales: ${total}\nTransactions: {len(sales_data)}"
        return Report("Sales Report", content)

# Usage
generator = ReportGenerator()
report = generator.generate_sales_report([100, 250, 175, 300])
print(report.display())

# Expected Output:
# Sales Report
# ------------
# Total Sales: $825
# Transactions: 4

Try it yourself: Modify ReportGenerator to create a MonthlyReport class that extends Report and includes a month field. Update the generator to return this new type.

Example 4: Dependency Injection (Reducing Coupling)

class Logger:
    def log(self, message):
        print(f"[LOG] {message}")

class PaymentProcessor:
    """Better design: inject dependency rather than create it"""
    
    def process_payment(self, amount: float, logger: Logger) -> bool:
        # Dependency injected as parameter - more testable
        logger.log(f"Processing payment of ${amount}")
        
        if amount > 0:
            logger.log("Payment successful")
            return True
        else:
            logger.log("Payment failed: invalid amount")
            return False

# Usage
logger = Logger()
processor = PaymentProcessor()
result = processor.process_payment(99.99, logger)
print(f"Payment result: {result}")

# Expected Output:
# [LOG] Processing payment of $99.99
# [LOG] Payment successful
# Payment result: True

Key Point: By injecting Logger as a parameter, we can easily substitute a mock logger during testing.

Try it yourself: Create a FileLogger class that writes to a file instead of printing. Test that PaymentProcessor works with both Logger and FileLogger without modification.

Common Mistakes

1. Confusing Dependency with Association

Mistake: Thinking any use of another class is a dependency.

# This is ASSOCIATION, not dependency
class ShoppingCart:
    def __init__(self):
        self.items = []  # Stored as instance variable = association
    
    def add_item(self, item):
        self.items.append(item)

# This is DEPENDENCY
class OrderProcessor:
    def process(self, cart: ShoppingCart):
        # Only uses cart temporarily = dependency
        return len(cart.items)

Why it matters: Misidentifying relationships leads to incorrect design decisions. Association implies ownership and lifecycle management; dependency doesn’t.

2. Creating Dependencies Inside Methods (Tight Coupling)

Mistake: Instantiating dependencies directly within methods instead of injecting them.

# BAD: Hard to test, tightly coupled
class OrderService:
    def create_order(self, items):
        # Creates dependency internally - can't substitute for testing
        email_service = EmailService()
        email_service.send_confirmation(items)

# GOOD: Dependency injection
class OrderService:
    def create_order(self, items, email_service: EmailService):
        # Injected dependency - easy to mock in tests
        email_service.send_confirmation(items)

Why it matters: Internal instantiation makes unit testing nearly impossible. You can’t test OrderService without a real EmailService.

3. Ignoring Dependency Direction

Mistake: Creating circular dependencies where A depends on B and B depends on A.

# BAD: Circular dependency
class UserService:
    def get_orders(self, user_id):
        order_service = OrderService()
        return order_service.get_by_user(user_id)

class OrderService:
    def get_user_info(self, order_id):
        user_service = UserService()  # Circular!
        return user_service.get_user(order_id)

Why it matters: Circular dependencies create fragile code that’s hard to understand, test, and modify. They often indicate poor separation of concerns.

4. Over-Depending on Concrete Classes

Mistake: Depending on specific implementations instead of abstractions (interfaces/abstract classes).

# BAD: Depends on concrete MySQLDatabase
class UserRepository:
    def save(self, user, database: MySQLDatabase):
        database.insert(user)

# GOOD: Depends on abstraction
from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def insert(self, data): pass

class UserRepository:
    def save(self, user, database: Database):
        database.insert(user)  # Works with any Database implementation

Why it matters: Depending on abstractions (Dependency Inversion Principle) makes code flexible and testable. You can swap implementations without changing dependent code.

5. Not Documenting Dependencies

Mistake: Failing to make dependencies explicit through type hints or documentation.

# BAD: Unclear what type message should be
class NotificationService:
    def send(self, message):
        print(message.content)

# GOOD: Type hint makes dependency explicit
class NotificationService:
    def send(self, message: EmailMessage):
        print(message.content)

Why it matters: Explicit dependencies improve code readability and help IDEs provide better autocomplete and error checking.

Interview Tips

What Interviewers Look For

1. Can you identify dependency relationships in code?

Interviewers often show code and ask you to identify relationships. Practice saying: “Class A depends on class B because it uses B as a method parameter/local variable/return type, but doesn’t store it as an instance variable.”

Example question: “Looking at this code, what’s the relationship between PaymentProcessor and CreditCard?”

Strong answer: “It’s a dependency relationship. PaymentProcessor uses CreditCard as a method parameter in process_payment(), but doesn’t store it as an instance variable. This is temporary usage, making it a dependency rather than association.”

2. Explain the benefits of dependency injection

Be ready to discuss why injecting dependencies is better than creating them internally.

Key points to mention:

  • Testability: You can inject mock objects during testing
  • Flexibility: Easy to swap implementations without changing code
  • Loose coupling: Classes don’t need to know how to create their dependencies

Example question: “How would you make this class more testable?”

class OrderService:
    def process(self, order):
        db = Database()  # Hard-coded dependency
        db.save(order)

Strong answer: “I’d use dependency injection by passing the database as a parameter: def process(self, order, db: Database). This allows me to inject a mock database during testing without modifying the OrderService class. It follows the Dependency Inversion Principle by depending on an abstraction rather than a concrete implementation.”

3. Discuss the Dependency Inversion Principle (DIP)

Interviewers at senior levels expect you to connect dependencies to SOLID principles.

DIP states: “Depend on abstractions, not concretions.”

Be ready to explain: High-level modules shouldn’t depend on low-level modules. Both should depend on abstractions (interfaces/abstract classes).

4. Recognize when dependencies indicate design problems

If a class has many dependencies (5+), that’s a code smell suggesting the class has too many responsibilities.

Example question: “This class depends on 8 other classes. What might that indicate?”

Strong answer: “That’s likely a violation of the Single Responsibility Principle. The class is doing too much and should be split into smaller, more focused classes. I’d look for cohesive groups of dependencies that could be extracted into separate classes.”

5. Compare dependency with other relationships

Be prepared to create a comparison table:

RelationshipStorageLifetimeCoupling Strength
DependencyNo instance variableTemporary (method scope)Weakest
AssociationInstance variableObject lifetimeModerate
CompositionInstance variableManaged lifecycleStrongest

Practice Problem: Given a class diagram or code snippet, identify all dependencies and suggest how to reduce coupling through dependency injection or interface abstraction.

Red flags to avoid:

  • Confusing dependency with association
  • Not mentioning testability when discussing dependency injection
  • Failing to recognize circular dependencies as problematic
  • Not knowing the Dependency Inversion Principle

Key Takeaways

  • Dependency is temporary usage of one class by another through method parameters, local variables, or return types—not stored as instance variables
  • Dependencies create coupling that affects testability, flexibility, and maintainability; minimize dependencies and depend on abstractions, not concrete classes
  • Dependency injection (passing dependencies as parameters) is superior to internal instantiation because it enables testing with mock objects and allows implementation swapping
  • Distinguish dependency from association by checking for instance variable storage: if stored, it’s association; if only used within method scope, it’s dependency
  • Circular dependencies are a design smell indicating poor separation of concerns; refactor to establish clear dependency direction or introduce an intermediary abstraction