· 3 min read
Separating Connectors and Service (Business Logic) - A Key Principle for Service Reusability
Separating connectors from business logic in a service-oriented architecture promotes reusability and single responsibility.
Introduction
In a previous post, I talked about why and how we should separate Domain Layer from Technical code. Here’s another version of the same concerning service.
A Service that handles the tasks of a RestController isn’t very different from a rest controller.
We should separate the Service from the Rest Controllers because we want to be able to re-use the service.
But if Service handles rest controller responses, it isn’t very different.
An ideal service
Here’s how you separate connectors from the business logic.
-
Connectors are mediums we use to communicate with the outside world. Examples of connectors are Rest API connector (like RestController or RouterFunction+it’s Handler), and Kafka Connector.
-
Services are codes that implement business logic.
All conversions from Entity to DTO or DTO to an entity (i.e. handle the request event and provide a response in the format needed by the client) should be handled at Rest API Connector.
The Rest API Connector is responsible for calling the service method between the request and the response.
For the service method, the REST API connector is downstream. So, it does not care about what format of data it sends. It’s just responsible for executing the business logic and sending the data in whatever its format.
Example - Identifying Bad and Good Practices
Let’s examine two examples to illustrate the difference between poorly designed and well-structured code. Although the examples are based on Spring Boot, the principles apply to any project.
Bad Practice: Violation of Single Responsibility
// @Controller Code
@PostMapping("/users")
ResponseEntity<UserResponse> postUsers(UserRequest userRequest){
return userService.saveUser(userRequest); // 1
}
// @Service Code
ResponseEntity<UserResponse> saveUser(UserRequest userRequest){ // 2
UserEntity unsaved = objectMapper.convertValue(userRequest, UserEntity.class); // 3
unsavedUser.setUserId(sequenceService.getNextId()); // 4
UserEntity savedUser = userRepo.save(unsaved); // 5
UserResponse userResponse = objectMapper.convertValue(savedUser, UserResponse.class); // 6
return ResponseEntity.ok().body(userResponse); // 7
}
Here is why the above code is bad:
- 1:
@Controller
is just calling the@Service
and returning its value. It isn’t doing anything except@PostMapping
. If that’s the case what’s the purpose of a@Controller
, just mapping? - 2: The
@Service
is handling receiving theUserRequest
and returns aResponseEntity
ofUserResponse
.- The problem both
UserRequest
andResponseEntity<UserResponse>
are mapped to REST API. I can’t reuse the method save mechanism for other connections or from within other services. - For example, let’s say I have to save a user from another
@Service
. To use the samesaveUser
method, I’ll have to create aUserRequest
object and the parse theResponseEntity<UserResponse>
. - This is a highly inconvenient conversion.
- The problem both
- 3 to 7: Here the
@Service
isn’t singularly responsibly.- While the name of the method is
saveUser
, it is also handling the conversion of request to entity and entity to the response. @Service
isn’t just handling business logic.
- While the name of the method is
Good Practice: Preserving Single Responsibility
// @Controller Code
@PostMapping("/users")
ResponseEntity<UserResponse> postUsers(UserRequest userRequest){
UserEntity unsavedUser = objectMapper.convertValue(userRequest, UserEntity.class); // 1
UserEntity savedUser = userService.saveUser(unsavedUser); // 2
UserResponse userResponse = objectMapper.convertValue(savedUser, UserResponse.class); // 3
return ResponseEntity.ok().body(userResponse); // 4
}
// @Service Code
UserEntity saveUser(UserEntity unsavedUser){ // 5
unsavedUser.setUserId(sequenceService.getNextId()); // 6
return userRepo.save(unsaved); // 7
}
The above code is good because:
- The
@Controller
handles the conversion from Request object to Entity and Entity to Response. - The
@Service
handles the business logic - The
@Service
only deals with entities and not request, response objects and therefore can be used at many places easily.
Conclusion
- A service method should only deal with business logical.
- The connectors like
@Controller
or other channels should deal with conversion from request object to response object. - This way the service remains open to be used in mutiple channels or in multiple other services.