The more I work with microservices, the more I realize that many of the patterns used for Object Oriented Design (OOD) can be applied to the development of microservices. Whenever I get stuck building a microservice, looking at it from the point of view of OOD principles seems to help enormously to get me un-stuck.
One of the main tenets of OOD are the SOLID design principles. This acronym defines a set of design principles that are extremely useful when designing an extensible, maintainable object oriented system. Let’s look at how these might be applied to write SOLID microservices.
S: Single Responsibility Principle
The Single Responsibility Principle states that a class should do one thing and one thing only. When a class does too many things, it becomes harder to maintain and evolve over time.
This applies equally well to microservices.
As Ive written before', the "one thing" a class or microservice is responsible for can be broadly defined. However you define it, there is usually a reasonable boundary for the service. If you have multiple teams working on the same microservice for different reasons, that may be a sign that the microservice is too big and responsible for too many things.Remember Robert Martin’s definition, if there are multiple reasons to change, the service should probably be broken into multiple services.
O: Open-Closed Principle
This is a shorthand way of saying that a class should be extensible, but you can’t modify the original or override it’s behavior. This is, in many ways, the essence of encapsulation. There’s a black box that has well defined extension points, but you can’t change what’s in the box.
For a microservice, this usually means that you can only “extend” it by interacting with its public APIs. This might mean writing an aggregation or wrapper microservice. This wrapper service may perform some logic before delegating to the original service or it may listen for events from the original and keep another data store in sync.
Keep the data store (e.g. a database) private, accessible only via APIs. This is analogous to the way a class only allows access to its private member variables through public methods.
L: Liskov Substitution Principle
The main idea here is that subclasses maintain the correctness of the contract defined by the parent class. This means that subclasses shouldn’t override a method that does one thing to do something completely different. Code that uses the superclass should be able to use the subclass and not know the difference.
Microservices don’t really have the notion of inheritance, but the idea is still applicable here. If I decide to completely replace the implementation of a service, the API should be maintained. That is, other applications shouldn’t know or care that the service has been written or rewritten in Java, Node.js or Ruby. One implementation can be transparently substituted for the other.
I: Interface Segregation Principle
If you’ve ever worked with a class or interface that has 20 public methods, and you only needed to call one, you’ll understand the usefulness of this principle. Instead of creating one big uber-interface, create lots of smaller, narrowly defined interfaces. A single underlying class may implement all of them, but the calling code doesn’t have to know that.
The same is true with microservices. You may think it’s easier to write a single API that returns the full domain object, but for many callers it’s overkill. For clients that only care about one or two fields, that’s extra network overhead as well as the time required to query for that data in the first place.
Instead, create narrowly targeted interfaces, each defined with the client’s needs in mind, not those of the service writer. This may mean you have more API endpoints to maintain, but they’ll be targeted and easier to change over time.
D: Dependency Inversion Principle
If you’ve worked with Spring or Guice, you’re familiar with this principle. The main idea is that objects should depend on abstractions, and not low level implementations. These abstractions are injected, not created by the class that needs them.
Most microservice architectures rely heavily on a service discovery framework. Such a system allows services to find and call each other without having to hard-code a lot of configuration. If service A needs to call service B, it asks the service discovery framework to route the request accordingly. The service discovery framework has essentially injected the dependency on service B into service A.
SOLID Conclusion
Good design principles work, regardless of the language or specific system to which they’re applied. Hopefully you no see that the SOLID principles apply equally well to object relationships or microservices.
The next time you run into difficulty figuring out how to design a particular microservice, take a step back. Think about the concepts you’ve learned for object oriented design and how they might apply to your microservice. I think you’ll find that they translate pretty well.
Question: What other Object Oriented Design techniques can you apply to microservices?