When to use interfaces?
You cannot use interfaces everywhere, decoupling every class from another has not much sense, so when to use interfaces?.
Interfaces exist for a lot of things, but I will focus here in these:
Hide/Defer the implementation of a behavior to the caller.
Seam model
Decouple release from deployments
Hide/Defer the implementation
When do I need to do this?. Let’s first focus first on defer the implementation. If you are writing the code for a function, and you realize that you need a collaborator that helps you to reduce the complexity of your current class, then you can use an interface to defer the implementation of that behavior.
Let’s say that you are writing a rest controller, in GRASP patterns a controller is:
The controller pattern assigns the responsibility of dealing with system events to a non-UI class that represents the overall system or a use case scenario. A controller object is a non-user interface object responsible for receiving or handling a system event.
Problem: Who should be responsible for handling an input system event?
Solution: A use case controller should be used to deal with all system events of a use case, and may be used for more than one use case. For instance, for the use cases Create User and Delete User, one can have a single class called UserController, instead of two separate use case controllers. Alternatively a facade controller would be used; this applies when the object with responsibility for handling the event represents the overall system or a root object.The controller is defined as the first object beyond the UI layer that receives and coordinates (”controls”) a system operation. The controller should delegate the work that needs to be done to other objects; it coordinates or controls the activity. It should not do much work itself. The GRASP Controller can be thought of as being a part of the application/service layer[4] (assuming that the application has made an explicit distinction between the application/service layer and the domain layer) in an object-oriented system with common layers in an information system logical architecture.
https://en.wikipedia.org/wiki/GRASP_(object-oriented_design)
Let’s take a design decision, I don’t want to put all the logic of my behavior in the controller. The controller is just an intermediary between the outside world and the inside world of my application. This is going to help me to test and decide what type of test to use where.
But if I’m focused on writing a controller, it’s not yet the time to think on how to solve the business problem. In someway, I want to reduce the cognitive load of the things I have to implement, I want to focus just in one thing.
I can decide that the business behavior will not be executed by the rest controller, so I will create a collaborator, but I don’t need to focus on how to do it, just on how I’m going to interact with that collaborator.
This is one thing an interface can help you to do, just to focus on the signature of the method to call, and defer the implementation of that behavior to the future.
But how to identify this, apart from this example?
One way to identify when and what behaviors to defer is through tests, some smells/clues can help you to understand if you should introduce an interface to defer the implementation:
You start seeing signals of an explosion of tests.
Your tests have a lot of setup about different infrastructures to check the behavior.
You are tempted to create a test double for a collaborator.
What is explosion of tests?
If I am testing a method or a function, and I need a lot of tests, some of them watching different aspects of the method, then my method probably is not honoring the Single Responsibility Principle. A collaborator can help (another entity in charge of that sub-behavior that produces the explosion of tests), test it independently, and you can use a test double in the first one, this will solve the explosion of tests.
If you have a lot of setup for your tests, your tests are perhaps too coupled with non-simple parts. Sometimes this reflects that your code is too coupled and the only way to test it is through integrated tests.
Interfaces are great ways to set up borders between different modules of your application.
Test doubles
If you need to create a test double for a collaborator, you are basically saying this is too complicated to be tested in the same test suite as the caller of that collaborator.
Then instead of creating that collaborator and implementing it, you can just focus on the signature of the method using an interface.
So that you can create a test double in your test to defer the implementation of the real thing.
Quarantine the infrastructure virus
Interfaces act as borders or "airlocks," allowing your pure business logic to remain decoupled from side effects.
This ensures that if the owner of a library makes a breaking change, or you decide to switch databases, your upper core domain remains untouched.
In functional programming, a side effect is any change to the state of the system or any observable interaction with the outside world that occurs during the execution of a function, other than returning a value.
In functional programming, a side effect is any change to the state of the system or any observable interaction with the outside world that occurs during the execution of a function, other than returning a value.
To understand this, it helps to compare a “pure” function with one that has side effects.
1. The Pure Function (No Side Effects)
A pure function is like a math equation. If you give it the same input, it always returns the same output, and it doesn’t touch anything outside of its own scope.
Predictable: It doesn’t rely on or change hidden variables.
Testable: You only need to check the input and output.
2. What counts as a Side Effect?
If a function does anything besides calculating a return value, it has side effects. Common examples include:
Modifying a global variable or an input argument.
Printing to the console or logging to a file.
Writing to a database or making an API call.
Throwing an exception that halts the program.
Triggering an external process (like a hardware action).
Basically, infrastructure is about side effects (integration tests) and pure behaviors is about your business logic (unit tests).
Seam model
The seam model refers to a strategic architectural approach used to change software behavior safely, particularly when dealing with legacy code or large-scale transitions.
Definition of a Seam
A seam is defined as a specific place in a program where behavior can be altered without the need to edit the code in that exact location. It acts as a “junction point” or a boundary where different implementations can be swapped in or out
How to Create a Seam?
Creating a seam is a disciplined process, often performed using automatic refactoring tools in an IDE to ensure safety. The steps typically include:
• Extraction: Identifying the code you want to change and extracting it into its own method
• Delegation: Moving that method into a new, separate class, which creates a “collaborator” for the original class
• Abstraction: Extracting an interface from that collaborator and using that interface in the main class
• Injection: Passing the interface as a parameter (usually via a constructor) so that the main class no longer decides which implementation to use
3. Practical Applications
The seam model is the technical foundation for several key practices mentioned in the sources:
• Branch by Abstraction: This is a technique for making large-scale changes gradually. By identifying a seam, you can develop a “New Behavior” implementation of an interface while the “Old Behavior” is still running in production
• Feature Toggles: At the seam, you can implement a factory or static method that checks a toggle; if the toggle is “on,” the system uses the new behavior, and if it is “off,” it reverts to the old one.
• Safe Refactoring: Seams allow you to “isolate the old behavior” so it can be tested and eventually deleted without breaking the surrounding system
• Testing with Test Doubles: Test doubles (like mocks or stubs) are often injected at seams. Identifying these seams helps define the “borders” between pure business logic and the “virus” of infrastructure
Strategic Benefits
Using seams allows a team to decouple deployment (putting code in production) from releasing (putting features in front of users). This reduces the need for long-lived feature branches, as unfinished work can be merged into the main branch behind a seam and kept “hidden” until it is ready for use.
Analogy: Think of a seam like a modular component in a car’s engine. If the engine is one solid piece of metal, you have to cut it open (edit the code) to change how it works. By creating a seam (like a standardized plug or socket), you can simply unplug the old part and plug in a new, high-performance version without ever having to damage or redesign the rest of the engine.
If you want to apply the seam model, then you need interfaces.
Decoupling release from deployments
Interfaces help decouple release from deployment by providing a technical “seam”—a place where behavior can be altered without editing the code in that specific location. This allows developers to integrate unfinished code into the main branch and deploy it to production without making the new features visible to users.
Based on the sources, here is how interfaces facilitate this decoupling:
Creating “Branch by Abstraction”
Interfaces are the core mechanism for the Branch by Abstraction technique. Instead of using a long-lived feature branch in a version control system, you create an interface (an abstraction) for the behavior you want to change. This allows you to:
Maintain the old behavior through one implementation of the interface.
Develop the new behavior in a separate implementation class of the same interface.
Deploy both versions to production simultaneously, as the new code is “hidden” because it is not yet connected to the main execution flow.
Enabling Feature Toggles
At the junction point (the seam) where the interface is used, you can implement a factory or static method that uses a feature toggle to decide which implementation to instantiate.
Deployment: You push your commit and the executable code goes to production. If the toggle is “off,” the system continues using the old behavior.
Release: When the team is satisfied with the new behavior, you simply “toggle on”. This puts the new feature in front of users without requiring a new deployment.
Reducing Risk and Lead Time
By using interfaces to decouple these processes, teams can move away from “Big Bang” releases where a huge batch of features is deployed at once. This approach provides several benefits:
Fast Rollbacks: If a bug is discovered in a new feature after it is released, you don’t need to roll back the entire deployment; you just toggle the feature off to return to the stable old behavior.
Trunk-Based Development: It allows every developer to merge their work into the mainline at least daily, which is the essence of Continuous Integration.
Continuous Feedback: Teams can release a feature to a small subset of users (Canary Release) or collect data without showing results to users (Dark Launching) to validate hypotheses with real production data.
Conclusion: Interfaces transform your version control system from being the only way to release code into a tool that supports Continuous Deployment. They allow the “unit of work” to be the commit rather than the feature, ensuring that the system is “always green” and releasable.
Analogy: Using an interface to decouple release from deployment is like installing a bypass valve in a plumbing system. You can install all the new pipes and tanks (deploy the code) while the water is still flowing through the old ones. Once you are sure the new system has no leaks, you just turn the valve (the toggle) to switch the flow. If something goes wrong, you can turn the valve back immediately without having to rip out the new plumbing.
Apart from all of this. Interfaces are expansion points, you can always apply the decorator or composite pattern to add new things to the current implementation without changing the existing code.
I see software as a constant changing beast, if you try with this vision, you will realize that you can always remove interfaces when they are not required anymore. They are simple tools that help you to decouple things, to design as you write code.


