What is loose coupling?
Quick Answer
Loose coupling means a class depends on what another component does (an abstraction) rather than how it does it (a concrete implementation). Loosely coupled classes can be changed, replaced or tested independently. Tight coupling is the opposite — a class creates and depends on specific concrete types, so a change in one ripples into the other.
Loose coupling means my class depends on an abstraction — an interface — instead of a concrete class. So OrderService takes an IEmailSender, not a SmtpEmailSender. It doesn't know how email actually gets sent, which means I can swap the implementation, or pass in a fake for testing, without changing OrderService at all. Tight coupling is when a class news up its dependencies directly, so any change to those classes forces a change here too. Dependency Injection is really just the practical way we achieve loose coupling — the interface is the boundary and the container fills in the concrete type.
Coupling describes how much one piece of code needs to know about another. With tight coupling, OrderService does var email = new SmtpEmailSender(); — it is bound to SMTP, to that class's constructor, and it can't be tested without sending real email.
With loose coupling, OrderService depends on IEmailSender. It doesn't know or care whether the implementation is SMTP, a queue, or a fake in a test. You can:
- swap
SmtpEmailSenderforSendGridEmailSenderwithout touchingOrderService; - inject a mock
IEmailSenderin a unit test and assert it was called; - develop the two classes in parallel once the interface is agreed.
Dependency Injection is the mechanism that makes loose coupling practical: the interface is the seam, and the container supplies the concrete type at runtime. Loose coupling pairs with high cohesion — each class does one well-defined job.
// Tight coupling — bound to SMTP, hard to test
public class OrderService
{
private readonly SmtpEmailSender _email = new SmtpEmailSender();
public void Place(Order o) { /* ... */ _email.Send(o.CustomerEmail, "..."); }
}
// Loose coupling — depends on an abstraction
public class OrderService
{
private readonly IEmailSender _email;
public OrderService(IEmailSender email) => _email = email;
public void Place(Order o) { /* ... */ _email.Send(o.CustomerEmail, "..."); }
}
- Change or replace an implementation without touching its consumers
- Unit-test a class in isolation by injecting fakes
- Reduce the blast radius of a change — fewer files break
- Let teams work in parallel against an agreed interface
Practise the whole thread the way an interviewer walks it — each question builds on the last.