Software Architecture Software Development

To Interface or Not To Interface?

You make extensive use of interfaces, but the system remains tightly coupled. What gives? Learn when interfaces are actually useful, and how.

I often come across the generic recommendation of using interfaces as a way to reduce coupling and enable Dependency Inversion or even Dependency Injection.

Suddenly, almost everything in a software is an interface. Even so, the system is tightly coupled and the dependencies may have been injected but not inverted.

You can end up creating coupling even with interfaces, e.g. by injecting a database interface into a domain class. And that kind of dependency doesn’t make sense.

The truth is that interfaces are not a requirement to achieve these goals.

So when to use interfaces? We will see that there are two very specific cases:

1. When the interface represents a real abstraction

And I say “real” because you don’t need an interface to have an abstraction. Any class is an abstraction of some concept.

A Product class is an abstraction of the product sold in an e-commerce, mapping only the attributes and operations relevant to performing the required tasks.

The question, then, is when an abstraction is of the right type to be represented by an interface in software, in a way that it yields real benefits.

“Phone” as an abstraction. Each model as an implementation.

Clues that an interface represents a real and relevant abstraction:

  • Has more than one implementation;
  • You can use it in compositions, such as in a Decorator Pattern (more on this later);
  • LSP (Liskov Substitution Principle) holds true for all of its implementations.

2. At the physical boundaries of communication between systems

Another case where the interface is relevant is when it appears at the physical boundaries between two systems, separating the rest of the system from infrastructure concerns such as storage (files and database) and network (http and others). In other words, when you have IO.

Here we apply SOLID’s D: Dependency Inversion. Supposing a Controller, you make it depend on an interface that abstracts the database or repository, rather than relying directly on these resources.

Again, this will be beneficial for wrapping the implementation with a LoggingDecorator or CacheDecorator.

The Company Analogy

Think of a company. What makes it unique is the people, their culture, their way of doing things. Not the building, the chairs, or the internet provider it hires.

The first part is intrinsic. The company can improve, evolve, but not simply exchange one culture for another. The same is true for domain entities and business rules, in most cases.

The second part is a detail. It is necessary, but the company can move from one building to another or change all of its chairs and, in essence, it remains the same. By the way, this is LSP (Liskov). The same idea is true for a repository or mailing service.

When the use of interfaces actually pays off

If an interface doesn’t have a clear and real benefit, it’s just useless code. Remove it.

Two examples of real benefit cases:

Example 1: MediatR

As stated in its Github Page, this is a library to support in-process messaging, such as request/response, commands, queries, notifications and events.

One of its use would be to dispatch side-effects in response to domain events, such as sending a confirmation email after an order is placed.

To do this, you could use the INotification interface to define the event:

public class OrderPlaced : INotification
    public Guid OrderId { get; set; }

And a handler to capture this event and send the email:

public class SendConfirmationEmailWhenOrderIsPlaced : INotificationHandler<OrderPlaced>
    public Task Handle(OrderPlaced notification, CancellationToken cancellationToken)
        var message = BuildOrderConfirmationMessage(notification.OrderId);

It is important to note that you can have as many handlers for this type of notification as you want. Handlers you could add in this specific case, depending (as always) on your business rules:

  • Update product stock
  • Notify logistics to prepare and deliver the product
  • Check if the customer is eligible for a promotion based on total spent in the last 30 days

Also, note that each of these handlers would be a separate class, with logic that is technically decoupled from the code that originated the event, which is beautiful. This is easy to read, easy to follow, easy to maintain.

But, getting back to the topic at hand, one piece is missing, which is the bridge the connects the OrderPlaced event to all these handlers, right?

Well, you need an implementation of the IMediator interface, injected into your Controller or Service, so that you can dispatch the event out to the universe:

public class OrderService
	private readonly IDatabase database;
	private readonly IMediator mediator;
	public OrderService(IDatabase database, IMediator mediator)
		this.database = database;
		this.mediator = mediator;
	public async Task PlaceOrder(OrderDetails details)
		var order = details.AsDomainModel();
		var event = new OrderPlaced { OrderId = order.Id };
		await mediator.Send(event);

Finally, how does the mediator know to send this event to all handlers that are waiting for it?

You just need a line of code to automatically scan your code base and detect all the handlers available for each type of event (INotification). The specific implementation depends on the type of DI Container you’re using and here’s a list of samples for all major containers.

A very important note is that all INotificationHandler<OrderPlaced> implementations are real abstractions:

  • Various implementations
  • All implementations obey LSP, since you can add or remove as many as you want without altering correctness of the software (as long as these implementations represent corresponding requirements that have been added or removed as well)

Example 2: Composition with the Decorator Pattern

Suppose you have a ProductRepository and now you want to add caching to it. For the sake of simplicity, our interface will define just a single method.

public interface IProductRepository
    ReadOnlyCollection<ProductDto> ListProductsByCategory(int categoryId);

public class ProductRepository : IProductRepository
    public ReadOnlyCollection<ProductDto> ListProductsByCategory(int categoryId)
        var sql = "SELECT * FROM Product WHERE CategoryId = @categoryId";
        var products = connection.Query<ProductDto>(sql, new { categoryId });
        return products.ToList().AsReadOnly();

You may think of implementing caching directly in the class’ implementation, but that would violate two SOLID principles: the Single Responsibility and Open/Closed.

Decorator Pattern comes to the rescue.

We just need to add another implementation of IProductRepository that will be solely responsible for caching. Like this:

public class CachingProductRepository : IProductRepository
    private readonly IProductRepository repository;
    private readonly ICache cache;

    public CachingProductRepository(IProductRepository repository, ICache cache)
        this.repository = repository;
        this.cache = cache;

    public ReadOnlyCollection<ProductDto> ListProductsByCategory(int categoryId)
        var key = $"{nameof(ListProductsByCategory)}_{categoryId}";
        if (!cache.ContainsKey(key))
            var products = repository.ListProductsByCategory(categoryId);
            cache.Add(key, products);
        return cache.Get<ReadOnlyCollection<ProductDto>>(key);

There you have it: caching without changing the actual repository code.

And you could go on. If you now have to implement logging for exceptions, you add another implementation that will take care of it.

As long as these extra implementations receive another IProductRepositoryin their constructors, you can compose any number of IProductRepository implementations. Of course, you’ll want to the care of the wiring of these classes where you configure your Dependency Injection.

This technique gets even better when you’re applying CQRS, as you can compose directly with the generic IQuery interface and add logging + caching to all queries in one go.


Interfaces are very powerful and they create space for very powerful techniques, but you shouldn’t be creating interfaces for everything just for the sake of doing so.

Use interfaces when you have a clear purpose for them in mind, otherwise you’ll pollute your code and will just be throwing tools around.

By Phillippe Santana

Passionate about writting code that people can understand, I'm a software developer, a project manager, an entrepreneur, and people/culture enthusiast. Find me on [Linkedin]( and on [Medium](