The Anatomy of a Microservice, Satisfying the Interface
The Anatomy of a Microservice, Satisfying the Interface
This article builds upon the ideas of interface implementation with unit testing, client implementations, and beyond.
Join the DZone community and get the full member experience.Join For Free
In The Anatomy of a Microservice, Defining the Domain, I presented a pattern for defining microservice interfaces and domain objects. If you have not read that article, I would encourage you to do so. This article builds upon its discussion points. As a reminder, the sample code for this series can be found on GitHub at https://github.com/relenteny/microservice.
You may also like: The Role of APIs in a Microservices Architecture
With the microservice contract defined, it’s time to develop implementations. Implementations plural? Yes, and this is before we get to exposing the microservice via RESTful or gRPC entry points. There are multiple reasons why you would create multiple implementations of a microservice interface.
There are multiple reasons why you would create multiple implementations of a microservice interface.
Below is a sampling of reasons. After reading this list, other use cases may come to mind:
Unit Testing and MockingFor unit testing, you can use one of the very good Java mocking frameworks, but where possible, I prefer to develop a basic, yet fully functioning implementation of the interface that can be used throughout the development cycle. It’s not just about having an implementation for unit testing.
A mock service is great when testing service access patterns; whether that be through injection or some type of web service call. No need to get a database connection or some other external system involved just to exercise integration points. MockMediaService provides an implementation of MediaService using sample data loaded from resource files. Sample data will be discussed later in this article.
Remote Client Implementations
As microservices often need to communicate with one another, consumers of a microservice will be grateful to see remote client implementations. Instead of having to write REST or gRPC code, client implementations of microservices can be injected just like any other class or interface implementation would be. This is a win-win situation.
The consumer of the API simply injects the client implementation of the microservice, and the microservice implementation team controls the details of how the microservice is invoked providing a more consistent interaction with remote clients. In the sample code, RESTful and gRPC client implementations are provided via RestMediaService and GrpcMediaService.
The Production Implementation
Ultimately there’s got to be code that does the “real work” of the microservice. Here, this is done via JPAMediaService. The example is a very basic JPA implementation of MediaService. Since it uses standard JPA features, it supports multiple database engines.
For certain test scenarios, it’s good to have an implementation that goes through the motions of interacting with a database. Most test cases in this project use H2 as a database with a set of sample data. Using H2 you can leverage an easy to load, in-memory database for more thorough testing scenarios.
The implementation hierarchy is straightforward:
Each implementation supports a specific use case; however, each implementation also shares a common ancestry with AbstractMediaService. AbstractMediaService provides common and convenience functionality to MediaService implementations allowing implementations to focus on delivering functionality. In this example, AbstractMediaService is providing logging and metric support to all subclasses.
It’s more than just convenience functionality to implementations. AbstractMediaService provides common patterns for logging and metrics for all implementations. Therefore, when deployed, regardless of implementation, downstream systems can reference and report on the service using the same attributes. For example, a metrics reporting system can build a single dashboard view based on common metric attributes.
At the same time, while downstream attributes are common across implementations, each attribute is tagged with a reference to the actual implementation. This allows for further refinement of downstream reporting. The design offers a pattern for additional downstream attributes based on implementation.
Beyond Unit Testing
Unit testing in an application is fundamental. As mentioned above, beyond unit testing I find having test implementations that do more than simple mocks valuable. When a project begins, getting a team productive quickly is imperative. A mock implementation that can be injected into all aspects of an application aids in getting parallel streams of work underway.
While a production implementation is being developed, the mock can be used during the development of API entry points, which leads to validating a user interface and testing of client access more quickly.
When a project begins, getting a team productive quickly is imperative. A mock implementation that can be injected into all aspects of an application aids in getting parallel streams of work underway.
Based on a defined configuration property, the MockServiceProducer produces one of two MediaService implementations. Both have been described; the MockMediaService and the JpaMediaService. Regardless of the implementation, a set of sample data is provided. It’s nice to have enough and varied data to deal with different scenarios.
Working with optional and required fields, data transformation to and from wire formats, special characters, and so on, are all part of testing. Sample data comes from CSV files exported from my Plex Media Server and is loaded by sample data loader classes responsible for each media type.
It stands to reason that the more implementations exist, the more unit testing is required. You could copy and paste the unit test code from one project to another. This is tedious, and as the interface evolves, it eventually gets unmanageable. The mock module includes an abstract class that exercises any implementation.
MediaServiceTestBase will exercise any MediaService implementation. This centralizes service test cases, which makes it very easy to expand. At the same time, concrete unit test classes can add any unit tests further exercising an implementation. The diagrams below demonstrate this:
Combined, the diagrams show four unit test classes extending MediaServiceTestBase. Having the abstract class part of an exportable module allows the class to be used by other modules. Here, the classes TestMediaStreamResource and TestGrpcMediaService are part of the media-server module.
In the previous article, I stated that, when establishing module dependencies for your microservice, “Key to a microservice architecture is including only what is needed.” To that end, each of the above implementations is built and deployed as its own module from the project’s implementation directory. For example, there’s no need to include references to the JPA framework when the RESTful implementation is to be used.
Aside from the main objective of including only what is needed, a significant additional benefit will be better overall project design.
I will admit that, at first, this requires a bit of effort. When creating independent modules, you’ll need to deal with circular dependencies, code duplication, testing patterns, configuration patterns, etc; however, I’m sure you’d agree with me that, regardless of the distribution model, these are good design considerations. Aside from the main objective of including only what is needed, a significant additional benefit will be better overall project design.
With the domain defined and service implementations developed, the next step is to create API Server Interfaces to the service. The media-server project demonstrates exposing the MediaService through both RESTful and gRPC entry points. In my next article, we’ll examine and exercise these implementations.
Opinions expressed by DZone contributors are their own.