Last Updated: February 12, 2026
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 dependency isn’t a structural “we belong together” relationship. There’s no shared lifecycle and no long-term connection.
Instead, it reflects a one-time interaction, often through method parameters, local variables, or return types.
A Dependency exists when one class relies on another to fulfill a responsibility, but does so without retaining a permanent reference to it.
This typically happens when:
Imagine a Chef preparing a meal.
This represents a dependency. The chef depends on the knife only during the cooking process.
In UML class diagrams, dependency is represented by a dashed arrow (..>) pointing from 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.
This diagram reads: "Printer depends on Document." The Printer uses a Document during its print() method, but doesn't store it as a field. The dashed arrow is the visual shorthand for this temporary relationship.
Let's model a simple Printer that depends on a Document to print. The Printer receives the document as a method parameter, uses it, and doesn't store it.
Pay attention to what makes this a dependency and not an association:
Printer has no fields referencing Document. The Printer class has zero instance variables pointing to Document. The document exists only as a parameter inside the print() method.print() returns, the Printer object has no knowledge that a Document was ever involved.Document is created externally and passed in. The Printer doesn't construct, own, or manage the document's lifecycle.If the Printer stored a Document reference as a field (private Document lastPrinted), it would become an association. The structural link would persist beyond the method call.
The Printer/Document example is simple, but dependencies in real code take several different forms. Let's look at the most common ones so you can spot them in code reviews and interviews.
Dependencies can appear in several common forms within a class:
This is the most common and most recognizable form of dependency. The dependent class receives another class as a parameter, uses it during the method, and lets it go.
ReportGenerator depends on DataSource, but doesn't store it. The DataSource comes in, gets used, and is gone once generate() returns.
Sometimes a class creates another class inside a method, uses it, and discards it. The created object never escapes the method scope.
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.
A method can return an object of another class, creating a dependency on that type even if the class doesn't store it.
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.
A class can depend on another class by calling its static methods. There's no object reference at all, just a class-level dependency.
PasswordService depends on HashUtils, but there's no instance of HashUtils stored anywhere. The dependency is purely at the class level through a static call.
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:
EmailSender → SMSSender) without changing core logic.Consider a NotificationService that sends email notifications. A straightforward implementation might create its own EmailSender internally:
This looks reasonable, but it has several problems:
NotificationService class itself.The NotificationService is tightly coupled to EmailSender. It controls what sender it uses, and nobody from the outside can change that.
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.
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:
EmailSender in production, SmsSender for mobile users, or PushSender for in-app notifications. No code changes needed inside NotificationService.MockSender in unit tests that records messages instead of actually sending them. You can verify what was sent without side effects.NotificationService depends only on the Sender interface. 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.
Let's put dependency into practice with a realistic scenario. A TicketBookingService handles the complete flow of booking an event ticket. During the bookTicket() method, it needs to validate that seats are available, process a payment, generate a QR code for the ticket, and send a confirmation email. Each of these responsibilities belongs to a separate class, and the booking service depends on all four of them, but only during the booking method.
All four dashed arrows point outward from TicketBookingService. None of these classes are stored as fields. They're all received as method parameters, used during booking, and released.
TicketBookingService has zero fields. Every collaborator comes in through bookTicket() and disappears when the method returns. This is pure dependency with no structural coupling.SeatValidator validates seats. PaymentProcessor handles payments. QRCodeGenerator generates codes. EmailService sends emails. The booking service just coordinates the flow.PaymentProcessor that returns false.SmsService instead of EmailService. The booking service doesn't care, it just calls the method on whatever it receives.