Implementation Best Practices: Microservice API With Spring Boot
In this article, I share some useful tips for implementing Microservices API with Java and Spring Boot, along with a sample code to refer to.
Join the DZone community and get the full member experience.
Join For FreeTechnical Architecture
First, let's turn to the architecture, which will be explained in detail. Let's look at each of these tiers in detail.
Let me explain the architecture in detail. These components are commonly associated with the architecture of applications that follow the principles of Domain-Driven Design (DDD) and Model-View-Controller (MVC) or similar architectural patterns. Let me cover this one by one:
Entities
Entities represent the core business objects or concepts in your application. They encapsulate data related to the business domain. For example, in an Employee Management System, an employee entity might have attributes like name, email, and salary related to an employee.
Repositories
Repositories are responsible for handling the data access logic. They provide an abstraction over the data storage, allowing the application to interact with the data without worrying about the underlying storage details. For example, an EmployeeRepository would handle operations like storing, retrieving, updating, and deleting employee records in the database.
Services
Services contain business logic that doesn't naturally fit within the methods of an entity. They orchestrate interactions between entities and repositories to fulfill higher-level use cases. For example, an EmployeeService might have methods to calculate bonuses, process employee transfers, or handle complex business rules involving multiple entities.
Mappers
Mappers are responsible for transforming data between different layers of the application. They convert data from database entities to domain objects and vice versa. For example, an EmployeeMapper might convert an Employee entity to a data transfer object (EmployeeRequest) that can be sent over the network or used by the presentation layer.
Controllers
Controllers handle incoming requests from the user interface or external systems. They interpret user input, invoke the necessary services or business logic, and prepare the response to be sent back. In a web application, a controller receives an HTTP request, extracts the necessary data, and delegates the request to the appropriate service. It then formats the service response and sends it back to the client.
Frontend: You have the option of building Native Apps like Android and iOS. Desktop browser apps or mobile browser apps can be built using React or Angular frameworks.
Best Practices for Implementation of the Architecture
Entity: Useful Tips
- Name the package as “entities” under the feature name
- Set id as Long and generation type as identity
- Name the class and table in plurals like users
- Use Lombok for constructor and getter/setter code
- Have a length for every String field
- Set nullable to either true/false
- Use references to other tables like
@ManyToOne
. Remember, the table created is automatic, and what you write in the entity matters. - Use
@OneToMany
bidirectional if you wish to save the values in multiple tables in one call. - Use
@ManyToMany
to join tables. Create a separate Join class if there are fields in the join table apart from join id columns. - Identify the right inheritance type for is-a relationship. Pick between single table, class table, and concrete table inheritance based on the number of fields in every class.
Example
package org.project.feature.entities;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class Users {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 100, nullable = false)
private String firstName;
@Column(length = 100, nullable = false)
private String lastName;
@Column(length = 100, nullable = false, unique = true)
private String email;
}
Repository: Useful Tips
- Name the package as “repositories” under the feature name
- Extend the
JpaRepository
with the entity name and id as Long - As much as possible, use the style as a method for querying the entity like
findByEmail
- Use batch operations for multiple entries to the database, like
saveAll
- Use Optional for the return type as much as possible
Example
package org.project.feature.repositories;
import org.project.feature.entities.Users;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UsersRepository extends JpaRepository<Users, Long> {
}
Service: Useful Tips
- Name the package as “services” under the feature name
- Create an interface for all the operations within the service and create an implementation class
- Use
@AllArgsConstructor
for@Autowired
annotation - Accept the Request object and return the Response object from the model’s package.
- If multiple repositories need to be called, it should be called in a transaction unless you wish to start a new transaction.
- If you wish to call multiple services, the particular service has to be named as an aggregate service and within a transaction.
- Do not return the ResponseEntity from the service; it is the job of the controller tier.
Example
package org.project.feature.services;
import org.project.feature.models.UsersRequest;
import org.project.feature.models.UsersResponse;
import java.util.List;
public interface UsersService {
UsersResponse createUser(UsersRequest usersRequest);
UsersResponse getUserById(Long userId);
List<UsersResponse> getAllUsers();
UsersResponse updateUser(Long id, UsersRequest users);
void deleteUser(Long userId);
}
package org.project.feature.services.impl;
import lombok.AllArgsConstructor;
import org.project.feature.entities.Users;
import org.project.feature.mappers.UsersMapper;
import org.project.feature.models.UsersRequest;
import org.project.feature.models.UsersResponse;
import org.project.feature.repositories.UsersRepository;
import org.project.feature.services.UsersService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
@AllArgsConstructor
public class UsersServiceImpl implements UsersService {
private final UsersRepository usersRepository;
private final UsersMapper usersMapper;
@Override
public UsersResponse createUser(UsersRequest usersRequest) {
Users users = usersMapper.convertRequestToEntity(usersRequest);
Users saved = usersRepository.save(users);
return usersMapper.convertEntityToResponse(saved);
}
@Override
public UsersResponse getUserById(Long userId) {
Optional<Users> optionalUser = usersRepository.findById(userId);
return usersMapper.convertEntityToResponse(optionalUser.get());
}
@Override
public List<UsersResponse> getAllUsers() {
List<Users> users = usersRepository.findAll();
List<UsersResponse> usersResponses = new ArrayList<>();
for (Users user : users)
usersResponses.add(usersMapper.convertEntityToResponse(user));
return usersResponses;
}
@Override
public UsersResponse updateUser(Long id, UsersRequest usersRequest) {
Optional<Users> user = usersRepository.findById(id);
Users existingUsers = user.orElse(null);
existingUsers.setFirstName(usersRequest.getFirstName());
existingUsers.setLastName(usersRequest.getLastName());
existingUsers.setEmail(usersRequest.getEmail());
Users updatedUsers = usersRepository.save(existingUsers);
return usersMapper.convertEntityToResponse(updatedUsers);
}
@Override
public void deleteUser(Long userId) {
usersRepository.deleteById(userId);
}
}
Mappers: Useful Tips
- Name the package as “mappers” under the feature name
- Create an interface called Mapper using Generics and convert the entity to a model and vice versa using this mapper
- Do not use the entity as a return object in the controller tier
Example
package org.project.feature.mappers;
import org.project.feature.entities.Users;
import org.project.feature.models.UsersRequest;
import org.project.feature.models.UsersResponse;
import org.springframework.stereotype.Component;
@Component
public class UsersMapper {
public UsersResponse convertEntityToResponse(Users users) {
UsersResponse usersResponse = new UsersResponse();
usersResponse.setId(users.getId());
usersResponse.setFirstName(users.getFirstName());
usersResponse.setLastName(users.getLastName());
usersResponse.setEmail(users.getEmail());
return usersResponse;
}
public Users convertRequestToEntity(UsersRequest usersRequest) {
Users users = new Users();
users.setFirstName(usersRequest.getFirstName());
users.setLastName(usersRequest.getLastName());
users.setEmail(usersRequest.getEmail());
return users;
}
}
Model: Useful Tips
- Name the package as “models” under the feature name
- All requests and response objects will be stored here
- Use
@Data
annotation for the model classes - The model should act as a frontend for the API, and the service should convert the model to an entity for talking to the repository.
Example
package org.project.feature.models;
import jakarta.persistence.Column;
import lombok.Data;
import java.io.Serializable;
@Data
public class UsersRequest implements Serializable {
private String firstName;
private String lastName;
private String email;
}
package org.project.feature.models;
import jakarta.persistence.Column;
import lombok.Data;
import java.io.Serializable;
@Data
public class UsersResponse implements Serializable {
private Long id;
private String firstName;
private String lastName;
private String email;
}
Controller: Useful Tips
- Name the package as “controllers” under the feature name
- Try to create an API for every resource under a bounded context
- Make the resource names in plural, like /API/users
- For CRUD Operations:
- Use HTTP POST for create an operation with Request as the body
- Use HTTP PUT for update operation
- Use HTTP GET for retrieve all records
- Use HTTP GET with /{id} for retrieve with an identifier
- Use HTTP DELETE with /{id} to delete the record
- For operation other, the CRUD try avoiding a verb as much as possible
- Implement error handling at the controller tier
- Implement validation at the controller tier with @Valid
- Realize the difference between API thinking and RPC thinking. It is key to understanding APIs.
Example
package org.project.feature.controllers;
import lombok.AllArgsConstructor;
import org.project.feature.models.UsersRequest;
import org.project.feature.models.UsersResponse;
import org.project.feature.services.UsersService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@AllArgsConstructor
@RequestMapping("/api/users")
public class UsersController {
private final UsersService usersService;
@PostMapping
public ResponseEntity<UsersResponse> createUser(@RequestBody UsersRequest usersRequest){
UsersResponse savedUsers = usersService.createUser(usersRequest);
return new ResponseEntity<>(savedUsers, HttpStatus.CREATED);
}
@GetMapping("{id}")
public ResponseEntity<UsersResponse> getUserById(@PathVariable("id") Long userId){
UsersResponse users = usersService.getUserById(userId);
return new ResponseEntity<>(users, HttpStatus.OK);
}
@GetMapping
public ResponseEntity<List<UsersResponse>> getAllUsers(){
List<UsersResponse> users = usersService.getAllUsers();
return new ResponseEntity<>(users, HttpStatus.OK);
}
@PutMapping("{id}")
public ResponseEntity<UsersResponse> updateUser(@PathVariable("id") Long userId,
@RequestBody UsersRequest usersRequest){
UsersResponse updatedUsers = usersService.updateUser(userId, usersRequest);
return new ResponseEntity<>(updatedUsers, HttpStatus.OK);
}
@DeleteMapping("{id}")
public ResponseEntity<String> deleteUser(@PathVariable("id") Long userId){
usersService.deleteUser(userId);
return new ResponseEntity<>("User successfully deleted!", HttpStatus.OK);
}
}
Conclusion
In a long-term vision, as and when the code base becomes bulky, you might need to use the Strangler pattern to take out some of the services and deploy them as a separate microservice. This kind of coding structure will help then. If you get your basics right from the very beginning, then later on, the ride will be smooth.
Opinions expressed by DZone contributors are their own.
Comments