Spring Application Deployed with Kubernetes
Step by step building an application using Spring Boot and deployed via Docker on Kubernetes with Helm
full course- Setup: IDE and New Project
- Create the Data Repository
- Building a Service Layer
- Create a REST Controller
- Logging, Tracing and Error Handling
- Documentation and Code Coverage
- Database as a Service
- Containerize the Service With Docker
- Docker Registry
- Automated Build Pipeline
- Helm for Deployment
- Setting up a Kubernetes Cluster
- Automating Deployment (for CICD)
- System Design
- Messaging and Event Driven Design
- Web UI with React
- Containerizing our UI
- UI Build Pipeline
- Put the UI in to Helm
- Creating an Ingress in Kubernetes
- Simplify Deployment
- Conclusion and Review
In this stage we’re going to build out the service or logical layer of our microservice. I’ll explain adapter and port or hexagonal architecture and covering the usage of mapstruct.
Service Design
Ports and adapter or hexagonal design is a well discussed concept, so I’m not going to go into the pro and con of the design. Amazingly, the concepts work at the class, service or enterprise application scale. Additionally, spring’s use of interfaces makes implementing hexagonal architecture extremely easy. I’ll explain quickly the design of our service before we go much further and show how hexagonal design will be applied.
Ideally, we would like our service layer to only ‘talk’ in the concept of a model object. As we push the REST and DB concepts to the edges we will maintain a different structure, even though they may look exactly the same. The reason for this is that our java beans tend to get loaded up with annotations that are specific to the port or adapter that they function with.
For example, our CustomerEntity
object is currently loaded up with spring data annotations used for mapping to the database fields. These are only relevant for the DB interaction (completely useless at the rest endpoint). Additionally, loading up our model with annotations for every port or adapter makes the model object difficult to read, maintain and evolve.
However, this does come at a cost, in that we need to map our model object to the structures used by the ports and adapters. Mapping or translating is not to be taken lightly as this becomes a very risky portion of our application in terms of its evolution (but actually helps with service governance, which we will cover later). Additionally, writing bean mappers is tedious. So we’re introducing tedious and error prone code, how do we mitigate that? We can use mapstruct framework to do the heavy lifting.
Introducing Mapstruct
Mapstruct is a framework that is designed for mapping data between beans. It is very useful for handing simple cases, but it is also powerful enough to handle complex or custom mapping. Take a look at the documentation and examples.
Using Mapstruct
Create the Model
Lets start by creating our model object in com.brianrook.medium.customer.service.model
. Create a java class called Customer
and add this content:
package com.brianrook.medium.customer.service.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Customer {
private Long customerId;
private String firstName;
private String lastName;
private String phoneNumber;
private String email;
}
This looks exactly like the
CustomerEntity
without the persistence annotations, right? It sure does. I actually copy/pasted most of that class into this class and removed the annotations. But that’s the point: now my model object doesn’t have those persistence annotations in it and is more useful because of that.
Create the Mapper
Create a package called com.brianrook.medium.customer.dao.mapper
. In that package create a java interface called CustomerEntityMapper
. In that interface add this content:
package com.brianrook.medium.customer.dao.mapper;
import com.brianrook.medium.customer.dao.entity.CustomerEntity;
import com.brianrook.medium.customer.service.model.Customer;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface CustomerEntityMapper {
CustomerEntityMapper INSTANCE = Mappers.getMapper(CustomerEntityMapper.class);
CustomerEntity customerToCustomerEntity(Customer customer);
Customer customerEntityToCustomer(CustomerEntity customerEntity);
}
This is a very simple mapper because the field names in the classes are the same and mapstruct knows how to map fields that are named the same automatically. As you can see we can translate both ways between the model and the entity.
Create the Service
In com.brianrook.medium.customer.service
create a java class called CustomerService
. In that class add this content:
package com.brianrook.medium.customer.service;
import com.brianrook.medium.customer.dao.CustomerDAO;
import com.brianrook.medium.customer.dao.entity.CustomerEntity;
import com.brianrook.medium.customer.dao.mapper.CustomerEntityMapper;
import com.brianrook.medium.customer.service.model.Customer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class CustomerService {
@Autowired
CustomerDAO customerDAO;
public Customer saveCustomer(Customer customer) {
return persistCustomer(customer);
}
private Customer persistCustomer(Customer customer) {
CustomerEntity customerEntity = CustomerEntityMapper.INSTANCE.customerToCustomerEntity(customer);
CustomerEntity storedEntity = customerDAO.save(customerEntity);
Customer returnCustomer = CustomerEntityMapper.INSTANCE.customerEntityToCustomer(storedEntity);
return returnCustomer;
}
}
This is a very simple service but you can see how we’re using mapstruct. I’ve isolated the code that is used for transforming a customer to a customer entity into its own method and tied it to the logic around the
CustomerDAO
. That makes thesaveCustomer
method very clean and it would be very easy to add validation logic, message publishing or any other functionality to that service method.
At this point, I probably should write tests, especially around the mapping logic since I’ve already called that out as a very risky bit of code. However, I’m going to save that for the next step. Lets make sure this builds and the tests still pass.
Build and Commit
git checkout -b service
mvn clean install
git add .
git commit -m "Building the service layer"
git push --set-upstream origin service
git checkout master
git merge service
git push
0 comments on “Building a Service Layer”Add yours →