RESTful Microservices with Spring Boot and Kubernetes
Microservices architecture is one of the most popular ways of building a modern software system. At its core, microservice architecture involves building software applications using smaller cohesive services. Rather than building a large monolith, you build small independently deployable services. As a result, a single team can own end-to-end functionality.
What is microservices architecture?
A microservice architecture consists of many (often hundreds) of small, autonomous services. Each service is self-contained. A microservice is built around business capability. Some of the important characteristics of microservice architecture are:
- Microservices are modeled around business capabilities.
- Microservices are independently deployable.
- A microservices encapsulates the data it owns. If one microservice needs to get data from another microservice then it should call API.
- A microservice should be small in size.
The de facto standard for a Java microservice is Spring Boot.
What is Spring Boot?
Spring Boot is one of the most popular frameworks to build a microservice. It comes with smart defaults. Creating microservices is as simple as defining few dependencies. Spring Boot support for embedded web servers makes it very easy to write RESTFul APIs. You can integrate other frameworks with a few configurations.
Recipe for building a microservice
In this article, I am going to discuss how to build a real-life microservice using Spring Boot. First, I’ll explain how to identify microservice boundaries. After that, I’ll explain how to design the APIs using the API-first approach. And then, I’ll explain how to code and deploy microservices on Kubernetes. In this process, I’ll touch upon
- Domain-driven design
- API design
- API documentation
- Containerizing a microservice
- Deployment on Kubernetes
Building a microservice around business capabilities
One of the primary benefits of microservice architecture is developer agility. It gives the product team greater agility to build smaller feature increments. Therefore, it’s important to design microservices in a way that they are independent of each other. Modeling microservices around business capabilities is one such design technique.
One of the techniques of building microservices around business capability is by using Domain-driven design (DDD). Eric Evans popularized DDD in his book Domain-driven design.
DDD is the framework of modeling complex business domains and breaking them into manageable subdomains and bounded context.
Let’s look at the e-commerce domain through the lens of DDD. An e-commerce domain can constitute of following sub-domain and bounded context.
DDD has two phases:
- Strategic Design: In strategic design, you focus on the business domain as a whole. You identify Bounded Context, subdomains, ubiquitous language, and context map between different bounded contexts.
- Tactical Design: The tactical design gives you a set of design patterns to model the business domain. During tactical design, you identify entities, services, aggregates root, repository, etc.
DDD is an iterative process. This typically includes domain experts, developers, and other stakeholders.
In DDD, the process of identifying microservices can look like this:
An ‘ aggregates root’ can be a good candidate for a microservice, as it represents a functionally cohesive unit within a transaction boundary. Also, domain services, as it’s stateless, can be another good candidate for a microservice.
- Domain: a company’s overall business domain.
- Subdomain: a fine-grained area of the business domain. All the subdomains together add up to the company’s business domain.
- Ubiquitous language: a language shared by the team, developers, domain experts, and other participants.
- Bounded Context: a logical boundary of the business context in a domain.
- Entity: a domain object that has an identity. For example, a product can be identified using a product id.
- Value Object: an object (for example, money) that’s identified by its value.
- Aggregate: a root entity representing the hierarchy of objects. Generally, it represents a transaction boundary.
- Service: an object that implements some logic that doesn’t fit into a single entity. Services are stateless.
- Repository: objects that retrieve or persist domain objects to or from the data store.
- Factory: a factory is an object with methods for directly creating domain objects.
DDD with Event Storming
Event storming is a workshop-based technique of conducting DDD. It’s a lightweight and effective way of doing domain modeling. An event storming workshop involves developers, domain experts, and other stakeholders. It’s based on identifying domain events and commands that caused those domain events. Participants use sticky notes to capture events, commands, aggregates, etc.
A domain event is anything important that happens that is of interest to a domain expert. The domain expert is not interested in implementation detail such as databases, web sockets, or design patterns. The domain expert is more interested in the business domain of the things that have to happen. Domain events capture those facts in a way that doesn’t specify a particular implementation. You can use orange sticky notes to mark events in the timeline.
With your events outlined, you can begin to evaluate each event for its causes and consequences. Events can be triggered by a user or an external system. For example, a user checking out a product from the cart can trigger an’ Product Checkedout’ event.
One of the goals of the event storming workshop is to identify aggregates. The aggregate accepts commands that cause domain events. After that, you group aggregates together into bounded contexts. Along the way, you identify users, data, UI, and user goals. Finally, you discover the relationships between bounded contexts.
An outcome of an event storming workshop for the e-commerce domain can look like below. This is not a full picture — it’s just me thinking over events vs a workshop involving domain experts and other stakeholders.
As mentioned above, aggregates and application services can be good candidates for microservice. After the Event Storming workshop, you can identify some of the microservices as:
- Product Catalog service for product management.
- Cart Service for cart management.
- Order Service for order management
- and so on.
For more information, you can check the blog Event Storming — Lessons Learnt From The Trenches.
I just touched the surface of DDD. I am not going to cover DDD in detail as it requires an article of its own. You can read Eric Evan’s Domain-Driven Design (or Domain-Driven Design Quickly, a concise version of the book) for in-depth knowledge. Another good reference is Implementing Domain-Driven, Domain-Driven Design Distilled by Vaughn Vernon.
After identifying microservice, you need to design application programming interfaces (API). APIs are the interface of your application. APIs let your application communicate with other applications without knowing the implementation. In recent times, API-fist design has become one of the popular approaches for designing APIs.
In short, an API-first approach assumes the design and development of an API come before the implementation. After the API has been designed and documented, the team will rely on the API to build the rest of the application.
While designing an API you need to keep users and their goals in mind. The API should be designed from a consumer perspective. A consumer of API can be a user interacting with your application or another application calling your APIs.
Identifying API’s goals
In this paragraph, I will explain the API Goal Canvas approach. This approach is mentioned in the book “The Design of Web APIs” by Arnaud Lauret.
To identify API’s goal, you need to determine what its user can achieve by using it. You start with identifying users of the APIs. After that, you try to list down what users can do. Then, you identify how a user does that. In the process, you find out inputs needed by the user to achieve that.
It is an iterative process, and it looks something like this:
For example, let’s answer the below questions. In this case, an API user is a seller or an admin managing a product catalog on an e-commerce platform.
- Who are the users?
- What can they do?
- How do they do it?
- What do they need to do it?
- What do they get in return?
- Where do the inputs come from?
- How are the outputs used?
By answering the above questions, you can arrive at the API goal canvas as:
Along with DDD and API goal canvas, you get the full picture of your domain. You also identify microservices and capabilities they expose through APIs. The next step is to design APIs and start documenting them.
In the following section, I’ll discuss the APIs of the product-catalog microservice.
Designing and documenting APIs
Once you have identified API’s goal, you can start designing and documenting APIs. The de-facto standard of designing and implementing a public API is REStFul API.
What is a RESTFul API?
A RESTful API is an API that conforms to the constraints of REST architectural style and allows for interaction with RESTful web services. REST stands for representational state transfer, and it is an architectural constraint.
REST considers application as a network of web resources. These web resources can be accessed by URI such as http://www.example.com/products/1234. A user can do operations on resources, such as GET or POST, to transition state (application state transitions).
One common misconception about REST is that its API protocol and implemented as JSON over HTTP. REST is not a protocol. You can implement REST in many ways. But, by far the most popular method to implement REStFul API is the JSON payload on top of the HTTP protocol.
HTTP resources are represented as nouns. For example,
/orders represent product and order resources (also called collections). You can use HTTP methods (also knows as verbs) to do some operations on resources.
The most commonly used HTTP methods are:
- POST: create a new object based on the data provided. For example,
POST /productscreates a new product.
- GET: return the object. For example,
GET /products/1234returns product identified by id 1234.
- DELETE: delete an object. For example,
DELETE /products/1234deletes product identified by id 1234.
- PUT: replace an object, or create a new object. For example,
PUT /productscreates a new product if the product does not exist or replaces the object.
- PATCH: apply a partial update to an object.
Apart from this, there are other HTTP methods, such as HEAD, OPTIONs. You can see details of methods in HTTP 1.1 spec.
Documenting RESTFul API using OpenAPI spec
OpenAPI Specification provides a standard for defining and documenting APIs.
The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for HTTP APIs, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic. When properly defined via OpenAPI, a consumer can understand and interact with the remote service with a minimal amount of implementation logic.
Tools for documenting OpenAPI spec
Typically, OpenAPIs are documented in YAML or JSON. So, you need a tool as writing YAML/JSON specs is not easy. There are many tools available to document and design your APIs. Some of these tools are:
Some of these tools also have a feature to collaborate during API design. For example, SwaggerHub allows you to create APIs and share them with your team for review.
You can see OpenAPI spec for product catalog service in the GitHub repo. On a high level, the OpenAPI spec for the product catalog in Swagger editor looks like this:
Organizing code for microservices
One of the concerns you need to solve is how to organize the code of microservices. Typically, there can be two possible approaches to organize code for microservices.
In this approach, you put all microservices in a single repository. For Maven, you can use the maven multi-module approach to organize code for microservices. Similarly, for Gradle, you can use a multi-project build. This approach has the benefit that all code is organized in one place. The downside of this approach is that you need advanced DevOps practices and tooling. This is particularly true when the codebase is very large.
Curious to know more? You can check the article Our experience: Monorepo with Java, Maven, and GitHub Actions.
2. Single repo for each microservice
In this approach, each microservice has its own repository. This is a much simpler approach. In the monorepo approach, you can accidentally introduce coupling between microservices as all codes are in one place. But, this is no a concern for the ‘single repo for each microservice’ approach. Again, the biggest downside of this approach is something as trivial as upgrading Spring Boot dependency needs to be performed against all repo with many commits.
Creating microservices in Spring Boot
The simplest way to create a microservice in Spring Boot is by defining the project in Spring Initializer.
Some important configurations, that you need to understand, are:
- Build: choose Gradle or Maven. It builds a Spring Boot application in a deployable jar.
- Spring Web: used for RESTFul web API. By default, it adds embedded Apache Tomcat as a dependency.
- Lombok: use Lombok to stop writing biolerplate getter and setter.
- Spring Data JPA: provides convenient APIs on the top of JPA.
- H2 Database: in-memory database with low memory footprint. For prod, you should use some real database.
After you have configured the Spring Boot project, download the zip by selecting ‘Generate’. Finally, extract zip and import project in IDE.
Implementing microservices using hexagonal architecture
The hexagonal architecture allows you to have a separation of concerns. In short, it says the application and domain layer contains core business logic. Therefore, it should not depend on infrastructure concerns such as database and messaging. You are free to change your technology choices while still keeping business logic intact.
Hexagonal architecture is also known as ports and adapters. This architecture defines ports (interfaces) in the application/domain layer and provides the implementation in different layers. Therefore, the application layer is completely unaware of implementation. This allows you to change, for instance, the database without changing business logic.
For example, you can define a repository interface
ProductCatalogRepository in the domain layer. The implementation of this interface can be done in the persistence layer. To summarize, domain layer code is only aware of interface, not implementation.
In Hexagonal architecture, you can organize code in layers per package approach as:
The first step of implementing a microservice in Spring Boot is to implement a controller. The controller provides an implementation of APIs. To implement a controller, you need to define a class with
@RestController annotation. To implement APIs, you need to define methods with appropriate annotation. For example,
@PostMapping for POST API and so on.
You can implement
POST /products API as:
Let’s understand what’s being done here -
- The annotation
@PostMapping("/products")means its implementation of the API
- The mandatory request header sellerId is mapped to the variable
- To maintain separation of concern, we map the API request object
CreateProductRequestto the domain object
Product. Here, we have used MapStruct, which provides simple annotation-based code to define the mapping.
- Commands are responsible for enforcing application workflow. We get
productCatalogCommandby calling factory as
- Then we call the command to add the product to the product catalog as
- Once we get productId, we return the response as
new ResponseEntity<>(productResponse, HttpStatus.CREATED). Here,
HttpStatus.CREATEDrepresents HTTP status code 201.
You can find the source code of the example mentioned in the article at GitHub.
The sample code contains:
- API implementation.
- Spring Data JPA implementation of repositories.
- A basic error handling.
- Aggregate root product catalog implementation.
Testing in Postman
You can run the Product catalog Spring Boot application locally from IDE. Then to test the APIs in Postman, import OpenAPI spec and define a variable
baseUrl as localhost:8080.
In Postman you can define dynamic variables from the response. This helps you in testing the value received from the response. For example, you can read productId from the response and set it as a collection variable in
Tests section as:
const jsonResponse = pm.response.json(); pm.collectionVariables.set("productId", jsonResponse.productId);
You can import the collection from the GitHub repo. This contains pre-defined postman variables.
In this article, we covered a fair bit of ground. We understood -
- A microservice is built around business capability.
- Domain-driven design is a technique to design microservice architecture.
- Event storming is a lightweight framework for DDD.
- API-first approach mandates that the design and development of an API come before the implementation.
- While designing an API you need to keep users and their goals in mind. API goal canvas is a practical way of identifying user API goals.
- Spring Boot makes implementing microservice super easy.
To be continued.
You can read the next article — Deploying a RESTful Spring Boot Microservice on Kubernetes.
Originally published at https://techdozo.dev on August 9, 2021.