What happens when a class needs to use another class for a brief moment to get a job done, without needing to hold onto it forever?
That's dependency. It represents the weakest form of relationship between classes.
Unlike association, aggregation, or composition, a depdency isn't a structural “we belong together” relationship. There is no shared lifecycle and no long-term connection.
Instead, it reflects a one-time interaction, often through method parameters, local variables, or return types.
Historical Context
Before object-oriented programming became popular, software was primarily procedural. Programs looked something like this:
main()
↓
Read File
↓
Process Data
↓
Print ResultEverything happened sequentially.
Functions simply called other functions.
As software became larger, engineers realized something interesting.
Even though functions weren't storing other functions they still depended on them.
Example:
calculateSalary()
{
readEmployee();
calculateTax();
saveSalary();
}Here,
calculateSalary()
depends on
readEmployee()
calculateTax()
saveSalary()If any of those functions changed significantly, the caller could break.
This idea eventually evolved into dependency.
When object-oriented programming emerged, functions became methods and data became objects.
Why Does Dependency Exist?
Imagine a restaurant.
There are:
- Chef
- Waiter
- Customer
- Cashier
- Delivery Person
Does the chef own the waiter?
No.
Does the chef contain the waiter?
No.
Does the waiter belong to the chef?
No.
The chef simply needs the waiter to deliver food.
Once the food leaves the kitchen, their interaction is finished.
This is dependency.
The chef uses the waiter. Nothing more.
Software works the same way.
An object often needs another object only while performing a task. It does not permanently own it.
First Principles
Let's forget programming. Imagine building a house.
You need:
- Hammer
- Drill
- Saw
- Measuring tape
Do you become permanently connected to the hammer?
No.
You simply use it. After finishing, the hammer goes away.
You relationship with the hammer is temporary.
This is exactly what dependency represents.
Dependency answers only one question:
“Who needs whom to accomplish a task?”
Not
- Who owns whom?
- Who contains whom?
- Who manages lifetime?
Only
Who temporarily uses whom?
What Is Dependency?
A Dependency exists when one class relies on another to fulfill a responsibility, but does so without retaining a permanent reference to it.
This is typically happens when:
- A class accepts another class as a method parameter.
- A class instantiates or uses another class inside a method.
- A class returns an object of another class from a method.
Formal Definition
A dependency exists when
One software element requires another software element in order to perform some operation, but does not own or permemently maintain it.
The key word is requires.
Not:
- contains
- owns
- manages
- stores
- inherits
- just
- requires
Key Characteristics of Dependency
- Short-lived: The relationship exists only during method execution.
- No ownership: The depdendent class does not sstore the otehr as a field.
- “Uses-a” relationship: The class uses another to accomplish a task, but does not retain it.
The Mental Model
Imagine people in an office.
Manager
asks
Accountant
↓
Generate Report
↓
Accountant returns report
↓
Interaction endsThe manager depends on the accountant.
But the manager does not own the accountant.
This is dependency.
The Simplest Example
class Printer
{
public:
void print()
{
std::cout << "Printing...\n";
}
};Now,
class Student
{
public:
void submitAssignment(Printer& printer)
{
printer.print();
}
};Question: Does Student own Printer?
No.
Student merely needs Printer temporarily.
As soon as the function finishes, the dependency disappears.
UML Representation
In UML class diagrams, dependency is represented by a dashed arrow ( - - - - - - - >) pointing form the dependent class to the class it depends on.
This is the lightest notation in UML, reflecting the fact that dependency is the lightest relationship.

The diagram read: “Student” depends on Printer. The Student uses a Printer during its submitAssignment, but doesn't store it as a field. The dashed arrow is the visual shorthand for this temporary relationship.
Code Example
#include <iostream>
#include <string>
using namespace std;
class Printer {
public:
void print(string& content)
{
cout << "Printing.....\n";
cout << content << "\n";
}
};
class Student {
private:
string content;
public:
Student(const string& content)
: content(content)
{}
void submitAssignment(Printer& printer) {
printer.print(content);
}
};
int main()
{
Printer printer;
Student s1("Science Proj");
s1.submitAssignment(printer);
// After this function, the student as no reference of the printer.
return 0;
}Recognizing Dependencies in Code
Dependencies can appear several common forms within a class:
As Method Parameters
This is the most common and most recognizable form of dependency. The dependent class receives another class as a parameter, uses it during the methods, and lets it go.
class ReportGenerator {
public:
string generate(DataSource& source) {
auto data = source.fetchAll();
// Format data into report...
return formattedReport;
}
};ReportGenerator depends on DataSource, but doesn't store it.
The DataSource comes in, gets uses, and is gone once generate() returns.
As Local Variables
Sometimes a class creates another class inside a method, uses it and discards it. This created object never escapes the method scope.
class OrderProcessor {
public:
void process(Order& order) {
JsonFormatter formatter;
string json = formatter.format(order);
// Send json to external API...
}
};OrderProcessor depends on JsonFormatter, but the formatter is a local variable. It's created inside the method and disappears when the method ends. No field, no structural link.
As Return Types
A method can return an object of another class, creating a depedency on that type even if the class doesn't store it.
class UserFactory {
public:
User createUser(const string& name, const string& email) {
return User(name, email);
}
};UserFactory depends on User because it creates and returns User objects, but it doesn't store any User as a field. The factory's job is to produce users, not to hold onto them.
As Static Method Calls
A class can depends on another class by calling its static methods. There's no object reference at all, just a class-level depdency.
class PasswordService {
public:
bool verify(const string& input, const string& hash) {
return HashUtils::sha256(input) == hash;
}
};PasswordService depends on HashUtils, but there's no instance of HashUtils stored anywhere. The depdency is purely at the class level through a static call.
Dependency Injection
In real-world applications, classes often depend on other classes to get their work done.
A UserService might rely on a DatabaseClient to fetch users, or a NotificationService might rely on an EmailSender to send messages.
But how should these dependencies be provided?
You could let the class create its own dependencies internally but that leads to tight coupling, making your code rigid and hard to test.
A better approach is to inject those dependencies from the outside.
This is called Dependency Injection (DI), one of the most powerful principles in modern software design.
Dependency Injection is a design technique where a class receives the objects it depends on, instead of creating them itself.
This leads to:
- Better testability: You can inject mock dependencies during unit tests.
- Greater modularity: Swap implementations (e.g., EmailSender → SMSSender) without changing core logic.
- Loose coupling: Classes only depend on abstract contracts (interfaces), not concrete implementations.
Example
Consider a NotificationService that sends email notifications. A straightforward implementation might create its own EmailSender internally:

class NotificationService {
private:
EmailSender sender; // Creates its own dependency
public:
NotificationService() : sender() {}
void notifyUser(const string& message) {
sender.send(message);
}
};This looks reasonable, but it has several problems:
- Can't switch implementations. Want to send SMS instead of email? We have to modify the
NotificationServiceclass itself. - Can't test in isolation. Unit tests will actually send real email (or fail trying) because there's no way t substitute a mock.
- Violates the Open/Closed Principle. Adding a new notification channel requires changing existing code rather than extending it.
The NotificationService is tightly coupled to EmailSender. It controls what sender it uses, and nobody from the outside can change that.
The Solution : Inject from Outside
Instead of letting the class create its own dependencies, you provide them from outside. This is Dependency Injection: a design technique where a class receives the objects it depends on rather than creating them itself.

class Sender {
public:
virtual void send(const string& message) = 0;
virtual ~Sender() = default;
};
class EmailSender : public Sender {
public:
void send(const string& message) override {
cout << "Email: " << message << endl;
}
};
class SmsSender : public Sender {
public:
void send(const string& message) override {
cout << "SMS: " << message << endl;
}
};
class NotificationService {
private:
Sender* sender;
public:
NotificationService(Sender* sender) : sender(sender) {} // Injected
void notifyUser(const string& message) {
sender->send(message);
}
};Now the NotificationService depends on a Sender interface, not a concrete EmailSender. The specific implementation is provided from outside through the constructor.
This gives you three concrete benefits:
- Swappable implementations. Pass
EmailSenderin production,SmsSenderfor mobile users, orPushSenderfor in-app notifications. No code changes needed insideNotificationService. - Easy testing. Pass a
MockSenderin unit tests that records messages instead of actually sending them. You can verify what was sent without side effects. - Loose coupling.
NotificationServicedepends only on theSenderinterface. It doesn't know or care how messages are actually delivered.
In real-world applications, frameworks like Spring (Java), ASP.NET (C#), and NestJS (TypeScript) handle DI for you. They automatically resolve and inject dependencies based on configuration or annotations, so you don't have to wire everything manually. But the underlying concept is the same: classes declare what they need, and something external provides it.
Leave a comment
Your email address will not be published. Required fields are marked *
