AlgoMaster Logo

Interfaces

Last Updated: February 12, 2026

Ashish

Ashish Pratap Singh

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.

1. What is an Interface?

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".

2. Key Properties of Interfaces

Interfaces are more than just method declarations, they are the foundation of flexible software design.

Here are their most important characteristics:

a) Defines Behavior Without Dictating Implementation

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.

b) Enables Polymorphism

Different classes can implement the same interface in different ways.This allows your code to work with multiple implementations interchangeably.

c) Promotes Decoupling

Code that depends on interfaces is insulated from changes in the concrete classes that implement them.

This makes your code easier to:

  • Extend (add new implementations without modifying existing ones),
  • Test (mock interfaces in unit tests),
  • Maintain (fewer ripple effects from code changes).

Example:

As long as all payment providers implement the PaymentGateway interface, the CheckoutService can use any of them without changing its own code.

3. Code Example: Payment Gateway Interface

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.

Defining an Interface

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.

Implementing an Interface

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.

Programming to the Interface

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.

Runtime Flexibility

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.

Output:

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.

4. Practical Example: Notification Service

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.

Why This Design Works

  • Adding a new channel is trivial. Need PagerDuty notifications? Create a PagerDutyNotifier class that implements NotificationService. The AlertService works with it immediately, no modifications needed.
  • Each notifier is independently testable. You can unit test EmailNotifier to verify it formats messages correctly, without involving Slack or webhooks.
  • The alert service is channel-agnostic. It doesn't import any notifier classes. It only knows about the NotificationService interface. This means you could move all the notifier implementations to a separate package or module, and AlertService would still compile without changes.
  • Configuration drives behavior. In a real system, you'd read the notification channel from a config file or environment variable, create the appropriate notifier, and inject it. The alerting logic stays completely untouched regardless of which channel is active.