The Anatomy of a Microservice, Defining the Domain
As we work our way to microservice deployment, we’ll begin with a review of microservice development principles and patterns.
Join the DZone community and get the full member experience.Join For Free
Part 1 of this series, Microservice Definition and Architecture, I provided an overview and some guidelines for consideration when building and deploying microservices. As we work our way to microservice deployment, we’ll begin with a review of microservice development principles and patterns. As mentioned in Part 1 of this series, the project is written in Java using the Quarkus framework and is available GitHub at https://github.com/relenteny/microservice.
You may also like: Microservice Definition and Architecture
I have been around long enough to know and will be the first one to admit, that every development project is opinionated. To that end, the patterns demonstrated in this project reflect my personal experience in software development. Through the years, as technologies and frameworks evolve, patterns that stand the test of time easily adapt to this constant evolution. You may use a different language, or you may construct your projects differently.
That’s what makes our profession great. Based on our experiences, we all apply knowledge gained to further our craft. So, if you’d like, feel free to take this sample project and refactor it to your pattern. If you do so, I’d love to hear about your approach!
I have been around long enough to know, and will be the first one to admit, that every development project is opinionated.
For simplicity, the GitHub repository contains three projects. Within an organization, I would have created a separate repository for each project as they are related but can independently evolve, which is one of the principles in microservice development. Each service and component can evolve on its timeline.
The project format is Maven. Whether the choice is Maven or Gradle, selecting a portable project format is critical to an overall Continuous Integration and Continuous Delivery process. IDEs are great, but once code is ready for an automated build process, IDE project formats become troublesome as support is not as robust in build systems for IDE project formats as compared to Maven and Gradle. Also, teams and/or individuals will have a preference toward one IDE or another.
Since all IDEs support importing Maven and Gradle projects, developers can choose their favorite IDE. Of course, those who enjoy working from the command line are free to do so with Maven and Gradle directly. To that end, my practice is to never commit IDE projects files in a Git repository. The only project files committed are Maven POMs (or Gradle build scripts).
Whether the choice is Maven or Gradle, selecting a portable project format is critical to an overall Continuous Integration and Continuous Delivery process.
The three projects are as follows:
Even when developing with a well-designed Maven project, working to maintain dependencies and versions in larger or more complex projects can get challenging. A Maven bill of materials (BOM) provides a single source containing dependencies, versions, and properties that may be shared across multiple, independent projects.
The microservice-platform-bom project demonstrates how a BOM might be developed within an organization. In keeping with independent microservice evolution, BOMs are versioned just as any other Maven project, so teams are free to evolve to later releases of the BOM as their schedule allows.
The sample microservice we’re using is a simple media server repository. It provides basic query access to movies, television shows, and audio tracks. The media-domain contains the definition of the media service components and interfaces. Later in this article, we’ll begin to take a closer look at this module.
The media-server project contains two API servers; RESTful and gRPC. They leverage media-domain modules in providing external entry points to the media service. We will look at these projects in a subsequent article.
Defining a Microservice
As we begin our examination of the sample code, for review I’ve included the microservice layered diagram presented in Part 1 of this series.
As we define the microservice, a few perspectives are taken into consideration. As mentioned earlier, Quarkus has been selected as the framework on which the microservice is built. That addresses the Framework Capabilities layer of the architecture. The next layer of the diagram is labeled Microservices. This layer defines the domain models and associated business services. The media-domain project encompasses the microservice layer.
Media-domain Project Layout
The median-domain project has two modules; domain-definition and domain-implementation. The domain-definition module is a parent module to common interface and class modules used by service implementations and service consumers. Within domain-definition, there are two sub-modules; model and protobuf. These two modules are where the actual microservice definition code resides. Before getting into the details of these modules, I’ll address a question you may be considering, “Why so much granularity?”
Key to a microservice architecture is including only what is needed. The codebase of a robust microservice providing a sophisticated business service may not be small, but it doesn’t need to make itself any larger than it needs to be. Also, the more modules on which code is dependent, the more work required to manage those dependencies.
What may be most important though is that more code often means a larger attack surface providing more opportunity for vulnerabilities to creep in. Only include what you need. This can be more challenging than you might think. In your Maven POMs, it’s much easier to “throw in the kitchen sink” than it is to be deliberate in what you include.
Key to a microservice architecture is including only what is needed... This can be more challenging than you might think. In your Maven POMs, it’s much easier to “throw in the kitchen sink” than it is to be deliberate in what you include.
The model module defines the most used service definitions. In this simple example, there are definitions for the domain models; Movie, Audio, and Television Show, and the business service; MediaService. All are written as interfaces to provide implementation flexibility.
For this sample microservice, the domain model is straightforward:
The diagram also shows implementations of the domain model interfaces. As a convenience to service implementations and consumers, providing a set of default implementations is often helpful. Here, I’m using Immutables to generate implementations of the domain model. I’ve grown to really like the functionality Immutables provides. An example can be seen in the Movie interface.
For demonstration purposes, the MediaService itself is simple; defining only read operations:
Similar to the domain model, providing a convenience implementation of the interface is helpful, specifically to implementers of the interface. Here, the AbstractMediaService provides base functionality each implementation of the interface should (would) need to provide; namely metric tracking and a basic level of diagnostic logging. This is also where I ran into my first Quarkus nuance. In other frameworks, annotations, such as metric annotations can be placed at the interface level and processed at runtime.
However, in anticipation of a packaged runtime distributable, Quarkus assembles code in such a way where annotations that would have been interpreted at runtime are essentially discarded (that’s an oversimplification and not a bad thing). To be a good citizen within the Quarkus framework, and to support a common metric and diagnostic logging pattern for implementations of MediaService, the AbstractMediaService defines wrapper methods, which allow it to perform and report a common set of metrics and diagnostic logging regardless of implementation.
The protobuf module contains the definition of protobuf message types and RPC (remote procedure call) definitions for use with gGRPC services. This module is used by both server and client implementations of a gGPC interface. Under the covers, Quarkus leverages Vert.x, which supports gRPC. This will be discussed more in-depth when we look at the gRPC entry point for the media service.
As I stated in Part 1 of this series, “To effectively function in an iterative development methodology, deployment patterns must be defined and implemented by the time the first line of code is written.” It’s in this very first module where this principle starts to take shape. I just discussed the common metrics and logging provided by the AbstractMediaService providing a consistent pattern for reporting on the behavior of MediaService implementations
To assist automated processes in determining their health, microservices should also provide “readiness” and “liveness” checks. Readiness checks indicate to an orchestration system that a microservice is ready to accept requests. Liveness checks indicate to an orchestration system that the microservice is running in a healthy state.
The definition of each of these states is up to the use case. In this example, leveraging the Quarkus readiness and liveness capabilities, functionality is provided via MediaServiceReadinessCheck and MediaServiceLivenessCheck. Note that the implementation of the MediaService is injected to maintain a separation between implementation and interaction with the runtime environment.
As the deployment of the microservice is considered, metric and diagnostic reporting, and readiness and liveness checks are established at the outset. This does not prohibit interface implementations from providing their metrics and logging in addition to what’s already provided. In fact, for more complex implementations, it would be encouraged.
Next Up, Implementing the Microservice
In this article, I’ve outlined a pattern for creating and laying out the declaratory aspect of a microservice project. We examined project structure, design principles and initial considerations for deployment. Given the interface definition specifying MediaService capabilities, in my next article, we will explore the need and purpose for multiple MediaService interface implementations. We’ll begin to consider key aspects to maintainability through sample data and common testing of interface implementations.
Stay tuned as we continue our journey to building and deploying using sustainable patterns for microservices.
Opinions expressed by DZone contributors are their own.