Dependency: the weakest class relationship
Understand dependency in OOP, a temporary uses-a relationship where one class relies on another without holding a persistent reference.
Introduction
Dependency is the relationship to identify when one class uses another without keeping a persistent field reference. It is easy to overlook because the code may contain only a parameter, local variable, return type, or static call, yet the dependency still shapes coupling and change impact.
A common diagramming mistake is drawing association arrows everywhere and missing dependency. A class that accepts a Formatter as a method parameter, calls one method on it, and never stores a reference has a dependency. The usage is temporary, but it still creates coupling.
TL;DR / Mental Model
- Dependency: “I use this for a task.” UML uses a dashed arrow (
..>). - Association: “I keep a reference to this.” UML uses a solid arrow (
-->). - Look for where the type appears—parameter, local construction, return type, static call, or field—and classify the most specific relationship shown by the code.
What Is Dependency
Dependency means one class uses another without holding a persistent reference to it. The usage is temporary: a method parameter, a local variable, a return type, or a static method call. Once the method finishes, the relationship is gone.
Think of a restaurant kitchen. The chef (your class) picks up a knife (the dependency), uses it to chop vegetables, and puts it back. The chef does not own the knife. The chef does not carry it around all day. The knife was needed for a specific task and then released.
Compare this to association, where the chef has a personal knife stored in their locker. That is a persistent reference, a field on the class. Dependency is the version where the knife was handed to the chef for one task only.
Definitions and boundaries
Dependency is a broad “uses” relationship. A field-based association is also a dependency in the general sense, but UML uses association when the persistent reference is the important detail. Likewise, aggregation or composition adds whole-part and lifecycle semantics on top of a reference.
“Temporary” describes where the reference is held, not whether the work is cheap or unimportant. A return type can create a dependency in the public contract even though the method's local reference ends; callers that consume the returned type inherit that contract. Static calls create dependencies too, even when no object is passed.
For an interview or design review: dependency is generally the weakest of these class relationships. If the dependency class changes its interface, the dependent class may break at compile time, but the dependent does not store a persistent reference. That temporary-usage distinction separates dependency from association.
Dependency is everywhere
A method that takes an object as a parameter commonly creates a dependency. It is widespread in codebases and is often omitted from UML diagrams.
UML Notation
Dependency uses a dashed arrow pointing from the dependent class to the class it depends on. The dashed line signals "weaker than association." The arrowhead points toward the thing being used.
Compare the three arrow types you will see on class diagrams:
| Relationship | UML Arrow | Strength | Reference |
|---|---|---|---|
| Dependency | ..> dashed arrow | Weakest | Temporary (parameter, local var) |
| Association | --> solid arrow | Medium | Persistent (field) |
| Inheritance | `-- | >` solid with triangle | Strongest |
The dashed line is the visual cue. If you draw a solid arrow where you meant dependency, you are telling the reader "this class stores a reference," which changes the design conversation. Check the field declarations and method signatures before choosing the arrow.
Notice how OrderService has a solid arrow to OrderRepository (it stores a field reference) but dashed arrows to Cart and PaymentValidator (they are method parameters). This distinction matters because changing OrderRepository's interface affects OrderService at the field level, while changing Cart only affects the single method that receives it.
Types of Dependency
Not all dependencies look the same in code. There are four common forms, and recognizing them helps you identify coupling in code reviews.
Parameter dependency
The most common form. One class receives another as a method parameter.
public class ReportGenerator {
public String generate(Formatter formatter, List<String> data) {
return formatter.format(String.join(",", data));
}
}
ReportGenerator depends on Formatter because it calls format() on it. But it never stores formatter in a field.
Local variable dependency
A class creates or receives an object inside a method body.
public class InvoiceService {
public BigDecimal calculateTax(BigDecimal amount) {
TaxCalculator calc = new TaxCalculator(); // local dependency
return calc.compute(amount);
}
}
This is tighter coupling than a parameter dependency because InvoiceService now depends on TaxCalculator's constructor, not just its interface. A common refactor is to accept a calculator as a parameter or field when substitution and test isolation are important.
Static method call dependency
A class calls a static utility method on another class.
public class UserService {
public String hashPassword(String raw) {
return PasswordUtils.hash(raw); // static dependency
}
}
Static dependencies can be harder to isolate because the class name is hardcoded at the call site and cannot be replaced through an ordinary instance parameter.
Return type dependency
A class depends on another because its method signature returns that type.
public class OrderFactory {
public Order createOrder(String customerId) {
return new Order(customerId, LocalDateTime.now());
}
}
Any class calling createOrder() now depends on Order because of the return type. The dependency propagates outward through the returned value.
Static calls are hidden coupling
Static method dependencies do not show up as constructor or method parameters. They are invisible in the class signature. You only discover them by reading the method body. This makes them hard to mock in tests and easy to miss in code reviews. Prefer instance methods behind an interface when testability matters.
Implementation
// ReportGenerator depends on Formatter through a method parameter.
// It never stores a reference. Once generate() returns, the
// relationship between ReportGenerator and Formatter is gone.
public class ReportGenerator {
// No Formatter field. This is dependency, not association.
public String generate(Formatter formatter, List<String> rows) {
var header = formatter.formatHeader("Monthly Report");
var body = new StringBuilder();
for (String row : rows) {
body.append(formatter.formatRow(row)).append("\n");
}
return header + "\n" + body;
}
}The key observation: ReportGenerator has no fields. It depends on Formatter only through the generate() method signature. OrderService shows both relationships in one class: OrderRepository is association (field), EmailSender is dependency (parameter). That split is useful to communicate on a whiteboard.
Dependency vs Association
This is the distinction that trips people up most. Both involve one class knowing about another. The difference is duration.
| Dimension | Dependency | Association |
|---|---|---|
| UML arrow | ..> dashed | --> solid |
| Reference stored? | No (parameter, local, return) | Yes (field) |
| Duration | Method scope only | Object lifetime |
| Coupling strength | Weak | Moderate |
| Effect of change | Breaks methods that use the type | Breaks the class itself |
| Example | generate(Formatter f) | private Formatter formatter |
The practical rule: if you can remove the import by removing a single method, it is dependency. If the import is needed because of a field declaration, it is association.
This heuristic is useful in code reviews. When a field is only used inside one method, ask whether it should be a parameter instead. Moving it to a parameter can weaken the relationship from association to dependency and may make the class easier to test.
Interview shortcut
When an interviewer asks "what's the difference between dependency and association," answer with the field test. "Dependency is method-scoped use. Association is field-scoped reference. Dependency is the dashed arrow on UML, association is the solid arrow. Dependency is weaker because the relationship ends when the method returns."
Reducing Unnecessary Dependencies
Every dependency is a coupling point. The dependent class breaks if the dependency changes its signature. Fewer and weaker dependencies mean more maintainable code. Here are the two principles that matter most.
Depend on interfaces, not concretions
This is the Dependency Inversion Principle (the "D" in SOLID). Instead of depending on SmtpEmailSender, depend on EmailSender (the interface). Swapping implementations for tests or different environments can then avoid changes in the dependent class.
Look back at the ReportGenerator code. It depends on the Formatter interface. It has no idea whether it is working with CSV, HTML, or something else. That is the ideal: the dependent class knows the shape of the dependency, not the identity.
Law of Demeter (don't talk to strangers)
A method should only call methods on:
- Its own object (
this) - Objects passed as parameters
- Objects it creates locally
- Its direct field references
Violating this creates transitive dependencies. If orderService.getRepository().getConnection().close() appears in your code, OrderService now depends on Repository, Connection, and Connection.close(). Changing any of those three types can break the caller.
The fix: add a method on OrderService that encapsulates the chain. orderService.closeConnection() reduces three dependencies to one.
Design Implications and Trade-offs
Dependency is weaker than a persistent association, but weaker does not mean irrelevant. A parameter dependency limits the lifetime and scope of the reference; the trade-off is that callers must supply the collaborator each time. A field association is appropriate when the object needs the collaborator across operations or to preserve state between calls.
Depending on an interface can make substitution and architectural boundaries explicit, but it also adds a contract and sometimes an extra type. A concrete class is reasonable for stable, pure in-memory logic. For I/O, plugin points, or policies that genuinely vary, an abstraction usually earns its keep. Static calls and long call chains are exceptions to the visible-parameter model: they may be convenient, but their coupling is hidden and often harder to isolate.
The goal is not to eliminate every dependency. Keep dependencies intentional, narrow, and visible where practical; choose a field, parameter, return type, or static utility based on the required lifetime and contract.
30-Second Explanation
A dependency means one class uses another for a task without storing a persistent reference; UML shows it with a dashed arrow. A method parameter, local construction, return type, or static call can create one. If the reference is kept in a field, the more specific relationship is association. Depend on a narrow abstraction when substitution matters, but do not add indirection without a real boundary.
5-Minute Explanation
- Find the type usage: inspect parameters, local
newexpressions, return types, static calls, and fields. - Classify the relationship. A temporary use is dependency; a stored reference is association, possibly aggregation or composition when whole-part ownership also exists.
- Inspect the contract. Depending on
Formatteris less specific than depending onCsvFormatter, while a return type can expose a type dependency to every caller. - Choose the right seam. Pass a parameter for method-scoped work, store a field for a collaborator needed across calls, and wrap static or concrete behavior when tests or runtime variation require substitution.
- Check transitive coupling. A train wreck such as
order.getCustomer().getAddress().getCity()exposes several contracts; move the knowledge behind a method on the direct collaborator when that better protects the boundary.
Common Mistakes
Mistake 1: Storing a parameter as a field "just in case."
A method receives a Formatter parameter. The developer stores it in a field because "we might need it later." This promotes a dependency to an association, tightens coupling, and extends the reference lifecycle unnecessarily. Keep it as a parameter until you have proof you need a field.
Mistake 2: Depending on concrete classes instead of interfaces.
ReportGenerator that takes CsvFormatter as a parameter is limited to that concrete format and makes substitution harder. Prefer the Formatter abstraction when multiple implementations or test doubles are a real requirement.
Mistake 3: Ignoring static dependencies.
PasswordUtils.hash(raw) is a dependency on PasswordUtils, but it does not appear in the constructor or method signature. It is easy to omit from UML diagrams and can be harder to isolate without static mocking or refactoring. Prefer instance methods behind interfaces for behaviour that needs substitution in tests.
Mistake 4: Chaining method calls (train wrecks).
order.getCustomer().getAddress().getCity() can create dependencies on Customer, Address, and City, all from a class that was only supposed to know about Order. Each navigation step can expose another contract the caller must understand. Follow the Law of Demeter and ask Order for what you need directly.
Mistake 5: Confusing dependency with association on UML diagrams. Drawing a solid arrow where you mean a dashed arrow tells the reviewer "this class stores a reference." If the class only takes the object as a parameter, use the dashed arrow. Getting this wrong misrepresents the coupling in your design.
Train wrecks propagate breakage
Each navigation step in a chained call like a.getB().getC().doStuff() can add a dependency. If B changes the return type of getC(), callers that chain through B may break. The fix: add a.doStuffOnC() and let A handle the chain internally. This is the Law of Demeter in practice.
Test Your Understanding
Quick Checks
- Question: What is the field test? Answer: A field reference is association; a method-scoped reference is dependency.
- Question: Does a static call create a dependency? Answer: Yes, even though no instance is passed.
- Question: Why prefer an interface sometimes? Answer: It narrows the contract and allows meaningful substitution.
- Question: What does a dashed UML arrow mean? Answer: The source temporarily uses the target.
Quick Recap
- Dependency is the weakest class relationship: one class temporarily uses another through method parameters, local variables, return types, or static calls.
- UML notation: a dashed arrow (
..>) from the dependent to the dependency. Solid arrows mean association (stronger, persistent). - The field test: if the reference is stored in a field, it is association. If it only lives within a method scope, it is dependency.
- Four forms: parameter dependency, local variable dependency, static method call, and return type dependency. Parameter is the cleanest; static is the hardest to test.
- Depend on interfaces, not concretions. This is the Dependency Inversion Principle, and it keeps dependencies as weak as possible.
- The Law of Demeter prevents transitive dependencies: do not chain calls through objects your class was not directly given.
- In interviews, draw dashed arrows for dependencies and solid arrows for associations. Getting the arrow type right signals you understand coupling at a precise level.
Related OOP Concepts
- Association, aggregation, and composition: more persistent or ownership-specific relationships built on references.
- Abstraction and interfaces: narrow the contract a dependent class needs to know.
- Dependency Inversion Principle: places stable abstractions between high-level policy and low-level details.
- Law of Demeter: limits knowledge of transitive collaborators.
- Composition over inheritance: assembles dependencies instead of using inheritance for implementation reuse.
Related Articles
Understand association in OOP: how objects reference each other without ownership, with examples of unidirectional, bidirectional, and multiplicity relationships.
Inheritance models IS-A but couples tightly. Composition models HAS-A and stays flexible. Learn when each is right and how to migrate from one to the other.
Abstraction separates what an object can do from how it does it. Callers depend on contracts, not implementations, which makes systems extensible and independently testable.