In our previous lesson, we established a foundational definition of a dependency and saw how a class can take on a dependency by instantiating it directly with the new keyword. You learned to spot this pattern in code, from simple console examples to the UserViewModel in a WPF application. We concluded by noting that this direct instantiation leads to a design that can be problematic.
This lesson addresses the why. We will explore the core software design concepts of tight and loose coupling. You will learn to define these terms, understand their consequences, and see precisely how direct dependency creation (new MyService()) results in tightly coupled code that is resistant to change and difficult to test. This understanding is critical for appreciating the architectural benefits that dependency injection provides.
What is Coupling?
In software architecture, coupling refers to the degree of interdependence between software modules. Think of it as a measure of how much two components know about each other and rely on each other's internal details.
A simple, practical way to think about coupling is to ask a question when considering two components, A and B, where A uses B: "If I change the implementation of B, do I also have to change A?" The more often the answer is "yes," the more tightly coupled they are.
The following video provides a concise introduction to this concept.
Loose & Tight Coupling: Why Code is Hard to Change
This video from the channel Bran van der Meer offers a clear definition of coupling and introduces the useful question we just discussed.
Please watch the opening segment from this clip to get a formal definition and a practical way to think about coupling.
Our goal is generally to minimize unnecessary coupling, leading to a system composed of independent, replaceable parts. This brings us to the two main types of coupling: tight and loose.
Tight Coupling: The Rigid Design
Tight coupling occurs when one class is highly dependent on another, often having knowledge of its concrete implementation. The most common cause of tight coupling is exactly what we saw in the last lesson: a class directly instantiating its own dependencies.
When OrderService contains the line _repository = new OrderRepository();, it is permanently locked to that specific OrderRepository class. It doesn't just know that it needs a repository; it knows exactly which repository to create and how to create it.
This article provides a C# example that should look very familiar.
Dependencies and Layering - DEV Community
This DEV Community post by "tak089" shows a clear "before" picture of a tightly coupled design.
Please read the first section, "Understanding Dependencies in C#". Pay close attention to the code block labeled "Example of Tight Coupling" and the three problems it lists immediately after.
As the article points out, this design creates several significant problems:
- It's hard to change. If you want to switch from a SQL Server-based repository to a NoSQL one (or even an in-memory one for a different environment), you are forced to modify the
OrderServiceclass itself. This violates the Open/Closed Principle, which states that software entities should be open for extension but closed for modification. - It's difficult to test. Unit testing
OrderServicein isolation is nearly impossible. Because it creates its ownOrderRepository, any test ofOrderServiceis also an integration test ofOrderRepository, which likely involves a real database connection. You cannot easily substitute a "fake" or "mock" repository for testing purposes. - It's not flexible. The
OrderServiceandOrderRepositoryare effectively a single, inseparable unit. This rigidity spreads through the application, making the entire system brittle.
This next video uses a CSV parsing system to illustrate the same problem. When the reporting logic explicitly calls the CSV parsing logic and works with internal CSVRow objects, the two become tangled together.
This video from Scott Bailey uses a great analogy of a data processing pipeline to show how implementation details can "permeate" through a system.
Watch the segment from this example, where he describes a tightly coupled system for parsing a file and generating a report.
Loose Coupling: The Flexible Alternative
Loose coupling is the opposite. It describes a design where components are independent and interact through well-defined, stable interfaces or abstractions. A class that is loosely coupled can be consumed and tested independently of other concrete classes.
Instead of depending on a specific SqlOrderRepository, our OrderService should depend on an abstraction, like an IOrderRepository interface. It no longer knows or cares about the specific implementation details; it only cares that it receives some object that fulfills the contract defined by the interface.
The following diagram provides a fantastic visual metaphor for this concept. The "Direct SQL Query" path is tight coupling—the OrderService knows too much about the database. The path through a "Proper API" is loose coupling—the OrderService only needs to know the API contract of the UserService, not how it works internally.

This article provides a clear side-by-side comparison of C# code demonstrating both styles.
Understanding Interfaces Via Loose Coupling And Tight Coupling
This C-Sharp Corner article by Aankur Singla gives a concise code demonstration of moving from a tightly coupled to a loosely coupled design.
First, read the definition and code example under the heading "Tight Coupling". Then, read the definition and code under the heading "Loose Coupling". Notice how the CustomerRepository's dependency changes from the concrete Database class to the abstract IDatabase interface.
By depending on IDatabase, the CustomerRepository is now decoupled from any specific database technology. As the example shows, you could provide a SqlDatabase or an XMLDatabase without changing a single line of code in CustomerRepository.
This flexibility is the primary benefit of loose coupling. It leads to code that is:
- Easier to maintain and extend: You can add new features or change existing ones by swapping implementations. This is highly relevant in your work with hardware integrations, where you might need to support a new device model by simply providing a new class that implements a common
IDeviceinterface, without touching the core application logic. - More testable: During testing, you can easily provide a "mock" or "stub" implementation of an interface that returns predictable data without hitting a real database or network service.
- More reusable: Components that don't depend on concrete implementations are easier to reuse in different contexts.
A Note on Nuance: Coupling is a Trade-Off
While we generally strive for loose coupling, it's not an absolute law to be applied in every situation. Achieving loose coupling requires introducing abstractions (like interfaces), which adds a layer of indirection to your code. Sometimes, this can be "premature abstraction," adding complexity for a flexibility you may never need.
The decision of when to decouple is a matter of engineering judgment. As you build a system, it's wise to ask yourself a few questions:
- Is this component likely to change in the future? (e.g., swapping a payment gateway, a logging framework, or a data source).
- Do I need to test this component in isolation?
- Is the cost of creating an abstraction worth the flexibility it provides?
For critical, volatile, or external dependencies (like databases, web services, or file systems), the answer is almost always "yes." For small, stable, internal helper classes, the answer might be "no."
This final video provides an excellent summary of this trade-off.
Scott Bailey concludes his video with a thoughtful discussion on the practical trade-offs of loose coupling.
Please watch from this segment to understand that engineering is about weighing trade-offs, not blindly applying rules.
Conclusion
In this lesson, we've defined the critical concepts of tight and loose coupling and seen how direct instantiation of dependencies leads to a rigid, tightly coupled design.
Here are the key takeaways:
- Coupling is the measure of interdependence between software modules.
- Tight Coupling occurs when classes depend directly on each other's concrete implementations. It makes code hard to change, test, and maintain. A key sign of tight coupling is a class creating its dependencies with the
newkeyword. - Loose Coupling is achieved when classes depend on abstractions (like interfaces) rather than concrete types. This promotes flexibility, testability, and maintainability.
- The goal is not to eliminate all coupling but to manage it intentionally. Decoupling has a cost (abstraction) and should be applied where the benefits are clear.
We have now identified dependencies and understood the problem of tight coupling. In the next lesson, we will introduce the key terminology used in Dependency Injection to solve this problem. We'll formally define the terms Service, Dependency Graph, and Composition Root, which together form the conceptual framework for how DI containers work.