Loading learning content...
Not all object relationships are created equal. While association represents an ongoing structural bond, dependency represents the lightest, most transient form of relationship—one object uses another temporarily, without maintaining any lasting reference.
Think of it this way: if association is a marriage (a committed, ongoing relationship), dependency is asking a stranger for directions (a brief interaction that ends immediately afterward). The stranger helped you, but you don't exchange numbers. You don't maintain a relationship. You simply used their knowledge in that moment.
Dependency is the relationship of minimal coupling. Understanding it—and knowing when to use it instead of association—is crucial for building systems that are flexible, testable, and maintainable.
By the end of this page, you will understand: (1) The precise definition of dependency and how it differs from association, (2) How to identify dependencies vs associations from design decisions, (3) Types of dependencies: parameter, local, static, and return type, (4) How dependencies appear in code and diagrams, (5) The relationship between dependency and coupling, and (6) Techniques for managing and reducing dependencies.
Dependency is a semantic relationship where a change in one element (the supplier) may affect the semantics of another element (the client), but not necessarily vice versa.
In simpler terms: Class A depends on Class B if A uses B in some way without maintaining an ongoing reference to B. The "usage" is transient—it happens during a method call and then ends.
In UML, a dependency is a relationship that signifies that a single or a set of model elements requires other model elements for their specification or implementation. The dependent element (client) is incomplete without the element(s) it depends on (supplier).
Key Characteristics of Dependency:
Transient Usage: The client uses the supplier temporarily—typically within a single method call. No reference is stored as an instance variable.
No Navigation Path: Unlike association, there's no permanent path from client to supplier. The client finds the supplier through parameters, local instantiation, or static access.
Minimal Coupling: Dependency is the weakest form of coupling. Changing the supplier might break the client, but the connection is localized to specific methods.
One-Directional Awareness: The client knows about and uses the supplier. The supplier typically has no knowledge of the client.
| Aspect | Dependency | Association |
|---|---|---|
| Duration | Transient (during method execution) | Persistent (stored in instance variable) |
| Reference Storage | None—used and discarded | Maintained as a field |
| UML Notation | Dashed arrow (------→) | Solid line (——————) |
| Coupling Strength | Weak—changes affect specific methods | Stronger—affects class structure |
| Example | Method receives Logger as parameter | Order holds reference to Customer |
| Typical Lifespan | Method call duration | Object lifetime |
Dependencies manifest in several distinct forms, each with different implications for coupling and design flexibility. Understanding these types helps you make conscious choices about how your classes interact.
new. The client depends on the concrete class.12345678910111213141516171819202122232425262728293031323334353637383940
public class PaymentProcessor { // 1. PARAMETER DEPENDENCY // Logger is passed in—explicit, visible, testable public void processPayment(Payment payment, Logger logger) { logger.info("Processing payment: " + payment.getId()); // Process payment... } // 2. LOCAL VARIABLE DEPENDENCY // Validator is created locally—hidden, harder to test public boolean validateCard(CreditCard card) { CardValidator validator = new CardValidator(); // Hidden dependency! return validator.isValid(card); } // 3. STATIC/GLOBAL DEPENDENCY // Depends on static method—hidden, hard to replace public Money convertCurrency(Money amount, Currency targetCurrency) { double rate = ExchangeRateService.getRate( // Static dependency! amount.getCurrency(), targetCurrency ); return amount.multiply(rate); } // 4. RETURN TYPE DEPENDENCY // PaymentResult must be known to callers of this method public PaymentResult executeTransaction(Transaction tx) { // Process transaction... return new PaymentResult(status, message); // Return type dependency } // 5. INSTANTIATION DEPENDENCY // Directly instantiates concrete class—tight coupling public Receipt generateReceipt(Payment payment) { PdfGenerator generator = new PdfGenerator(); // Concrete class! return generator.create(payment); }}Local variable, static, and instantiation dependencies are concerning because they're hidden from the class's public interface. A class that appears to have no dependencies might actually be tightly coupled to half a dozen concrete implementations internally. This makes testing and refactoring extremely difficult.
In UML class diagrams, dependency is represented by a dashed arrow pointing from the client (dependent) to the supplier (dependency). The arrow indicates direction: "Client depends on Supplier."
┌──────────────┐ ┌──────────────┐
│ Client │ -------> │ Supplier │
└──────────────┘ └──────────────┘
│ │
│ │
Depends on Required by
UML Dependency Stereotypes:
UML allows optional stereotypes to specify the nature of the dependency:
| Stereotype | Meaning | Example |
|---|---|---|
| «use» | Client uses features of supplier | Method calls another class |
| «create» | Client instantiates supplier | Factory creates objects |
| «instantiate» | Client creates instances of supplier class | new Supplier() |
| «derive» | Client's value is derived from supplier | Calculated property based on other class |
| «call» | Client invokes operations on supplier | Method invocation during execution |
| «permit» | Client has special access to supplier | Friend class or internal access |
Reading Dependency Diagrams:
When you see a dashed arrow from A to B, read it as:
When deciding whether to draw a solid line (association) or dashed line (dependency), ask: "Does the source class store a reference to the target as a field?" If yes, use association (solid). If the class only uses the target temporarily in methods, use dependency (dashed).
Coupling refers to how tightly two modules or classes are connected. High coupling means changes in one class ripple to others; low coupling means classes can change independently.
Dependency is inherently about coupling—every dependency is a coupling point. The goal is not to eliminate all coupling (impossible—software must connect somewhere) but to:
| Relationship | Coupling Level | Change Impact | Testability |
|---|---|---|---|
| Dependency on Interface | Very Low | Minimal—interface changes are rare | Excellent—easily mocked |
| Dependency on Abstract Class | Low | Low—implementation changes don't affect client | Good—substitutable |
| Association | Medium | Moderate—structural changes propagate | Moderate—requires setup |
| Dependency on Concrete Class | Medium-High | High—any change might break client | Difficult—hard to isolate |
| Composition | High | High—lifecycle is tied | Challenging—complex setup |
| Inheritance | Very High | Very High—base class changes break all subclasses | Fragile—hard to test in isolation |
High-level modules should not depend on low-level modules. Both should depend on abstractions. This means your business logic (PaymentProcessor) shouldn't directly depend on infrastructure (MySQLDatabase). Both should depend on an abstraction (PaymentRepository interface). This inverts the typical dependency direction and dramatically reduces coupling.
1234567891011121314151617181920212223242526272829303132333435
// ❌ HIGH COUPLING: Direct dependency on concrete classpublic class OrderService { public void placeOrder(Order order) { // Direct dependency on concrete database implementation MySQLDatabase db = new MySQLDatabase(); db.save(order); // Direct dependency on concrete email sender SmtpEmailSender emailer = new SmtpEmailSender(); emailer.send("Order placed: " + order.getId()); }} // ✅ LOW COUPLING: Dependency on abstractionspublic class OrderService { private final OrderRepository repository; // Depends on interface! private final NotificationService notifier; // Depends on interface! // Dependencies are injected, not created public OrderService(OrderRepository repository, NotificationService notifier) { this.repository = repository; this.notifier = notifier; } public void placeOrder(Order order) { repository.save(order); // Works with any implementation notifier.notify("Order placed: " + order.getId()); // Flexible! }} // Now we can substitute implementations:// - Use InMemoryRepository for testing// - Use MySQLRepository for production// - Use PushNotificationService for mobile// - Use EmailNotificationService for webOne of the most common design decisions is choosing between dependency and association. The choice has significant implications for coupling, testability, and design flexibility.
The fundamental question: Does the client need to remember the supplier, or just use it momentarily?
Case Study: Report Generation
Consider a ReportGenerator that produces PDF reports. Should it have a dependency or association with a PDFWriter?
1234567891011121314151617181920212223242526272829303132333435
// Option A: Association (if PDFWriter is used throughout the class)public class ReportGenerator { private final PDFWriter pdfWriter; // Stored reference public ReportGenerator(PDFWriter pdfWriter) { this.pdfWriter = pdfWriter; } public Report generate(ReportData data) { pdfWriter.startDocument(); pdfWriter.addHeader(data.getTitle()); pdfWriter.addBody(data.getContent()); pdfWriter.addFooter(data.getFooter()); return pdfWriter.finishDocument(); }} // Option B: Dependency (if PDFWriter is just a method parameter)public class ReportGenerator { // No stored reference to PDFWriter public Report generate(ReportData data, PDFWriter pdfWriter) { pdfWriter.startDocument(); pdfWriter.addHeader(data.getTitle()); pdfWriter.addBody(data.getContent()); pdfWriter.addFooter(data.getFooter()); return pdfWriter.finishDocument(); }} // Which is better? It depends:// - If ReportGenerator always uses the same PDFWriter → Association// - If different callers need different writers → Dependency// - If PDFWriter needs configuration that varies → Dependency// - If testing requires substituting PDFWriter → Either works with DIIf you're unsure whether to use dependency or association, lean toward dependency. It's easier to upgrade a dependency to an association later than to downgrade an association to a dependency. Start with the weakest coupling that works.
Unmanaged dependencies lead to systems that are difficult to test, modify, and understand. Here are established techniques for managing dependencies effectively:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
// CONSTRUCTOR INJECTION (Recommended)public class OrderProcessor { private final PaymentGateway paymentGateway; private final InventoryService inventory; private final EmailService emailService; // All dependencies injected through constructor public OrderProcessor( PaymentGateway paymentGateway, InventoryService inventory, EmailService emailService ) { this.paymentGateway = paymentGateway; this.inventory = inventory; this.emailService = emailService; } public OrderResult process(Order order) { // Use injected dependencies—easily testable! if (!inventory.isAvailable(order.getItems())) { return OrderResult.outOfStock(); } PaymentResult payment = paymentGateway.charge(order.getTotal()); if (!payment.isSuccess()) { return OrderResult.paymentFailed(); } inventory.reserve(order.getItems()); emailService.sendConfirmation(order); return OrderResult.success(); }} // COMPOSITION ROOT: Wire everything at startuppublic class Application { public static void main(String[] args) { // Create concrete implementations PaymentGateway paymentGateway = new StripePaymentGateway(); InventoryService inventory = new DatabaseInventoryService(); EmailService emailService = new SmtpEmailService(); // Inject dependencies OrderProcessor processor = new OrderProcessor( paymentGateway, inventory, emailService ); // Use the fully-wired processor processor.process(order); }} // For testing, inject mocks:public class OrderProcessorTest { @Test void successfulOrder() { // Arrange PaymentGateway mockPayment = mock(PaymentGateway.class); when(mockPayment.charge(any())).thenReturn(success()); InventoryService mockInventory = mock(InventoryService.class); when(mockInventory.isAvailable(any())).thenReturn(true); EmailService mockEmail = mock(EmailService.class); OrderProcessor processor = new OrderProcessor( mockPayment, mockInventory, mockEmail ); // Act & Assert OrderResult result = processor.process(testOrder); assertEquals(OrderResult.success(), result); }}new hides them from the class's interface. Prefer injection for anything that might need substitution.While Service Locator can manage dependencies, it often becomes a "God object" that everything depends on. It hides dependencies (you can't tell what a class needs from its constructor) and makes testing harder (every test needs to configure the locator). Prefer constructor injection in new code.
Dependency represents the minimal form of relationship between objects. Mastering dependencies is essential for building flexible, testable systems.
new inside methods hide dependencies from the class interface.Coming Up Next:
We've covered association (ongoing relationship) and dependency (transient usage). Next, we dive into Aggregation—a special form of association that models whole-part relationships where the parts can exist independently of the whole. Think of a Department with Employees—if the Department closes, the Employees don't cease to exist.
You now understand dependency—the lightest form of object relationship. You can identify dependency types, represent them in UML, and apply techniques like Dependency Injection to manage coupling effectively.