Object-Oriented Programming
Interfaces
Software Development
Programming Principles
Code Design

What do programmers mean when they say, Code against an interface, not an object.?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When programmers say "code against an interface, not an object," they mean your code should depend on behavior contracts instead of concrete implementations. The point is not to avoid objects. The point is to avoid hard-wiring your business logic to one specific class when several different classes could satisfy the same need.

Interface Means Contract

An interface describes what a dependency can do, not how it does it. That lets the calling code stay focused on the capability it needs.

In Java, a small example looks like this:

java
1public interface MessageSender {
2    void send(String message);
3}
4
5public class EmailSender implements MessageSender {
6    public void send(String message) {
7        System.out.println("email: " + message);
8    }
9}
10
11public class SmsSender implements MessageSender {
12    public void send(String message) {
13        System.out.println("sms: " + message);
14    }
15}

Now the rest of the application can depend on MessageSender instead of knowing whether email or SMS is underneath.

Depending on a Concrete Class Creates Friction

If you write code directly against a concrete class, you tie yourself to its construction details, extra methods, and implementation quirks.

java
1public class NotificationService {
2    private final EmailSender sender;
3
4    public NotificationService(EmailSender sender) {
5        this.sender = sender;
6    }
7
8    public void notifyUser(String text) {
9        sender.send(text);
10    }
11}

This works, but NotificationService is now locked to EmailSender. Replacing it with SmsSender requires changing the service instead of simply supplying a different implementation.

Code Against the Abstraction Instead

java
1public class NotificationService {
2    private final MessageSender sender;
3
4    public NotificationService(MessageSender sender) {
5        this.sender = sender;
6    }
7
8    public void notifyUser(String text) {
9        sender.send(text);
10    }
11}

That is the core idea. The service cares about "something that can send messages," not "this exact class with this exact implementation."

This produces three practical benefits:

  • easier substitution
  • easier testing
  • lower coupling

It Works Well with Dependency Injection

This design becomes especially useful when combined with constructor injection.

java
1public class Main {
2    public static void main(String[] args) {
3        MessageSender sender = new SmsSender();
4        NotificationService service = new NotificationService(sender);
5        service.notifyUser("system online");
6    }
7}

The calling code decides which implementation to provide. The service itself stays unchanged.

This is also why the principle is often mentioned together with dependency inversion and testability.

Testing Gets Simpler

When your code depends on an interface, tests can inject a fake implementation instead of building the real external dependency.

java
1import java.util.ArrayList;
2import java.util.List;
3
4public class FakeSender implements MessageSender {
5    public final List<String> sent = new ArrayList<>();
6
7    public void send(String message) {
8        sent.add(message);
9    }
10}

A unit test can now assert behavior without sending real email or talking to a network service.

It Does Not Mean "Always Use Interfaces"

This principle gets overstated sometimes. If there is only one implementation, no variation point, and no real cost to depending on the concrete type, adding an interface may be unnecessary ceremony.

Good engineering means using abstractions where they buy flexibility or clarity, not creating them automatically for every class.

A better interpretation is:

  • depend on stable contracts
  • expose only the behavior a consumer needs
  • avoid unnecessary knowledge of implementation details

In some languages that contract may be an interface. In others it may be a protocol, trait, or abstract base class.

Common Pitfalls

The biggest mistake is thinking the phrase means "never use concrete classes." That is not the point. The point is to keep dependencies aligned with behavior rather than implementation detail.

Another issue is creating tiny interfaces for every class without any actual substitution or testing benefit. That adds indirection without solving a real problem.

A third problem is leaking concrete-type assumptions through the interface, which defeats the purpose of the abstraction.

Summary

  • Coding against an interface means depending on a behavior contract instead of a concrete implementation.
  • This reduces coupling and makes substitution easier.
  • It usually improves testing because fake implementations are easy to inject.
  • The principle fits naturally with dependency injection.
  • Use the abstraction when it solves a real design problem, not as automatic boilerplate.

Course illustration
Course illustration

All Rights Reserved.