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.
Most candidates draw association arrows everywhere and call it a day. They miss the weakest (and most common) relationship entirely: dependency. A class that accepts a Formatter as a method parameter, calls one method on it, and never stores a reference. That is dependency. It is temporary, fleeting, and it still creates coupling. Understanding it is the difference between a clean class diagram and a tangled one.
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.
For your interview: dependency is the weakest coupling between two classes. If the dependency class changes its interface, the dependent class breaks at compile time, but the dependent never stores a reference. That "temporary usage" distinction is what separates dependency from association.
Dependency is everywhere
Every time you write a method that takes an object as a parameter, you have created a dependency. It is the most common relationship in any codebase, and the one most people forget to draw on 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. I see candidates make this mistake constantly in whiteboard interviews.
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. I usually refactor this into a parameter or field to make it testable.
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 are the hardest to test because you cannot substitute a mock. The class name is hardcoded at the call site.
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 on the return type. The dependency propagates outward through the return 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 zero 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 exactly what you should 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.
I use this heuristic in code reviews constantly. When I see a field that is only used inside one method, I ask: should this be a parameter instead? Pushing it to a parameter weakens the coupling from association to dependency, and that usually makes 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). This way, swapping implementations (for tests, for different environments) requires zero 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 breaks the caller.
The fix: add a method on OrderService that encapsulates the chain. orderService.closeConnection() reduces three dependencies to one.
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 instead of Formatter cannot be reused with any other format. It also cannot be tested with a mock formatter. Always depend on the abstraction.
Mistake 3: Ignoring static dependencies.
PasswordUtils.hash(raw) is a dependency on PasswordUtils, but it does not appear in the constructor or method signature. Developers forget to draw it on UML diagrams and forget it is untestable without PowerMock or refactoring. Prefer instance methods behind interfaces for anything you need to test.
Mistake 4: Chaining method calls (train wrecks).
order.getCustomer().getAddress().getCity() creates dependencies on Customer, Address, and City, all from a class that was only supposed to know about Order. Each dot is another class your code now depends on. 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
Every dot in a chained call like a.getB().getC().doStuff() is a dependency. If B changes the return type of getC(), every class that chains through B to reach C breaks. The fix: add a.doStuffOnC() and let A handle the chain internally. This is the Law of Demeter in practice.
Test Your Understanding
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.