Think twice before implementing Interface classes in Microservices

Wai Loon
w:Logs
Published in
4 min readMay 9, 2018

Some developers like to use interface class. In general, interface class enables polymorphism. It acts as a contract of functionalities that every implementation class must have; and hides unnecessary methods away.

However, is it really a good practice to create an interface class for every service class in your application?

Why using Interface class?

For instance, an AuditLogger interface defines that any audit logger implementation must be able to perform send() to send a log message. Then you can have multiple implementation classes to send log message using different broker:

  1. ActiveMqAuditLogger — send log message using Apache ActiveMQ
  2. KafkaAuditLogger — send log message using Apache Kafka
public interface AuditLogger {
void send(String logMessage);
}
public class ActiveMqAuditLogger implements AuditLogger {
@Override
public void send(String logMessage) {
// codes to send log message to ActiveMQ
}
}
public class KafkaAuditLogger implements AuditLogger {
@Override
public void send(String logMessage) {
// codes to send log message to Kafka
}
}

When another class (e.g. SomeService) needs to perform audit logging, in Spring, it can just auto-wire the AuditLogger interface and use @Qualifier to specify which message broker to be used. The advantage is that the developer who writes this class does not need to know what happens under the hood. And it can be easily reconfigured to send audit log using another broker simply by updating the value at @Qualifer. This is an example of polymorphism.

@Service
public class SomeService {
@Autowired
@Qualifier("KafkaAuditLogger")
private AuditLogger auditLogger;
public void someMethod() {
// The log message will be sent to Kafka
auditLogger.send("My audit log message");
}
}

But interface can be redundant. Why?

In the example below, we have MyRepositoryImpl class implementing MyRepository interface. The interface here is redundant, especially when you’re developing microservices.

@Controller
public class MyController() {

@Autowired
private MyService myService;
@GetMapping("/")
public String findNameById(int id) {
return myService.findNameById(id);
}
}
@Service
public class MyService() {

@Autowired
private MyRepository myRepository;
public String findNameById(int id) {
return myRepository.findNameById(id);
}
}
@Component
public interface MyRepository {
String findNameById(int id);
}
@Repository
public class MyRepositoryImpl implements MyRepository {
public String findNameById(int id) {
// Some codes to retrieve name from a database
return name;
}
}

1. Stop dreaming about “future” polymorphism

First, the benefits of polymorphism does not exist when there is only ONE implementation class. Someone might argue that there might be multiple implementation here in the future. NO!

Evaluate these:

  1. What if multiple implementation never happen to this class in the future?
  2. What are the chances that you would need to keep the old implementation when a new implementation is introduced? Referring to the example, let’s say the old implementation retrieves name by ID from MySQL, and a new implementation retrieves it from MongoDB. Due to data integrity, your source of data should be only one. That means nobody should ever be retrieving name from MySQL anymore!

2. Nobody uses your implementation class except your team

There are a number of situations in software engineering when it is important for disparate groups of programmers to agree to a “contract” that spells out how their software interacts. Each group should be able to write their code without any knowledge of how the other group’s code is written. Generally speaking, interfaces are such contracts. — Java Documentation

In a monolithic application, multiple teams are working on the same piece of source code. Let’s take banking app as example, there can be Team A working on customer profile functionality, Team B working on fund transfer functionality, and Team C working on bill payment functionality. To invoke a service, developers of other functionalities refer to interface classes.

Translating the same use case to microservice (MS) landscape, these 3 functionalities will become 3 individual microservices: Customer MS, Transfer MS, Payment MS. By architecture, every individual microservice is isolated in its own source code, under its own git project. Naturally, no other team will need to read your microservice’s source code. When Transfer MS needs to retrieve customer profile, it calls a GET endpoint of Customer MS. REST API is the contract between microservices. To invoke a service, developers of other microservices refer to the API specs from documentation, instead of referring to interface classes.

What is the point of adding interface class when the person who reads the implementation class is always be you or your teammate?

3. Interface is not always a best practice

Someone might still say implementing interface classes is a best practice so we should write it regardless. This statement is actually not true.

The real best practice is actually to follow KISS principle. Make your application simpler to achieve high readability and maintainability. Plus, microservices are meant to be lean.

To conclude, interface class exists for a purpose, but it is not something that is suitable for all use cases. It is a fallacy to think that interface should be implemented in every service classes.

BONUS TIPS!

When using IntelliJ, ⌘B (Windows: Control + B) on an interface class will navigate you to the interface source code. To quickly navigate to the implementation class, use ⌥⌘B (Control+Alt+B) instead!

Thanks for reading.

--

--

Wai Loon
w:Logs
Editor for

Developer | Spring Boot, Microservices, Kubernetes, DevOps, Architecture | https://vxavictor513.github.io/resume/