Last Updated: February 12, 2026
In object-oriented design, interfaces play a foundational role in building systems that are extensible, testable, and loosely coupled.
They define what a component should do, not how it should do it.
This separation of definition and implementation allows different parts of your system to work together through well-defined contracts, without needing to know each other’s internal details.
At its core, an interface is a contract: a list of methods that any implementing class must provide. It specifies a set of behaviors that a class agrees to implement but leaves the details of those behaviors up to each implementation.
In other words:
An interface defines the "what", while classes provide the "how".
Consider a remote control. It exposes a standard set of buttons:
play()pause()volumeUp()powerOff()The person using the remote doesn’t care if it controls a TV, a soundbar, or a projector, they all understand the same set of commands.
The remote is the interface. The devices (TV, soundbar, projector) are the implementations.
Each device behaves differently when you press play(), but the contract remains consistent.
Interfaces are more than just method declarations, they are the foundation of flexible software design.
Here are their most important characteristics:
An interface only declares what operations are expected. It doesn’t define how they are carried out.
This gives freedom to implementers to provide their own version of the logic, while still honoring the same contract.
Different classes can implement the same interface in different ways.This allows your code to work with multiple implementations interchangeably.
Code that depends on interfaces is insulated from changes in the concrete classes that implement them.
This makes your code easier to:
As long as all payment providers implement the PaymentGateway interface, the CheckoutService can use any of them without changing its own code.
Let’s say you’re designing a payment processing module that supports multiple providers like Stripe, Razorpay, and PayPal.
You don’t want your business logic to depend on a specific provider. You just want a common way to initiate a payment.
Here's the PaymentGateway interface that defines a single operation: initiate a payment with a given amount.
This interface defines the contract. Every payment gateway must provide a initiatePayment() method. But it doesn’t specify how each provider processes payments.
Now let's create two classes that fulfill this contract: one for Stripe and one for Razorpay. Each processes payments differently, but both satisfy the PaymentGateway interface.
Both classes implement the same interface, but their internal logic is completely different. StripePayment talks to the Stripe API, RazorpayPayment talks to Razorpay. The interface guarantees that both provide the initiatePayment() method, so any code that depends on PaymentGateway can work with either one.
Here's where the real payoff happens. Instead of having CheckoutService depend on StripePayment or RazorpayPayment, it depends on the PaymentGateway interface. It doesn't know or care which implementation it's using.
Look at the CheckoutService constructor. It takes a PaymentGateway, not a StripePayment. This single decision is what decouples the service from any specific provider.
The checkout() method calls initiatePayment() on whatever gateway was injected. It could be Stripe, Razorpay, a mock for testing, or a provider that doesn't even exist yet.
This pattern is called dependency injection: instead of creating its own dependencies, the class receives them from the outside. And it only works because the dependency is typed as an interface, not a concrete class.
The final piece is wiring everything together. At runtime, you choose which implementation to inject, and you can even swap it out on the fly.
The CheckoutService didn't change between the two calls. The only thing that changed was which implementation was plugged in. That's the power of programming to interfaces: the calling code is completely insulated from implementation details.
Let's apply interfaces to a different domain. Imagine you're building an alerting system for a DevOps platform. When something goes wrong (server down, high CPU, disk full), the system needs to send notifications. Some teams prefer email, others use Slack, and some have custom webhook integrations.
The alerting service shouldn't know or care which channel is being used. It just sends the notification through whatever channel was configured.
Here's the class diagram for this design:
The pattern is identical to the payment gateway example: one interface, multiple implementations, and a service that depends only on the interface. Let's see the full working code.
PagerDutyNotifier class that implements NotificationService. The AlertService works with it immediately, no modifications needed.EmailNotifier to verify it formats messages correctly, without involving Slack or webhooks.NotificationService interface. This means you could move all the notifier implementations to a separate package or module, and AlertService would still compile without changes.