Building Spring Cloud Microservices That Strangle Legacy Systems

Profile Service

The Profile Service is a microservice that extends the domain data of the legacy Customer Service. This is the microservice that is strangling the domain data of the Customer Service—and in the process—slowly transitioning the system of record away from the large shared database in the legacy system. The Profile Service exposes protected domain resources as a REST API, using the Spring Cloud Security project to implement the OAuth2 client workflow.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@RestController
@RequestMapping(path = "/v1")
public class ProfileControllerV1 {

 private ProfileServiceV1 profileService;

 @Autowired
 public ProfileControllerV1(ProfileServiceV1 profileService) {
 this.profileService = profileService;
 }

 @RequestMapping(path = "/profiles/{username}", method = RequestMethod.GET)
 public ResponseEntity getProfile(@PathVariable String username) throws Exception {
 return Optional.ofNullable(profileService.getProfile(username))
 .map(a -> new ResponseEntity<>(a, HttpStatus.OK))
 .orElseThrow(() -> new Exception("Profile for user does not exist"));
 }

 @RequestMapping(path = "/profiles/{username}", method = RequestMethod.POST)
 public ResponseEntity updateProfile(@RequestBody Profile profile) throws Exception {
 return Optional.ofNullable(profileService.updateProfile(profile))
 .map(a -> new ResponseEntity<>(a, HttpStatus.OK))
 .orElseThrow(() -> new Exception("Profile for user does not exist"));
 }
}

In the code snippet above we see the ProfileControllerV1 class, which is a REST controller that provides an endpoint for retrieving the Profile of a user. The Profile object we are retrieving here will extend fields from the Customer object, after retrieving domain data from the legacy Customer Service. To do this, we will call directly to the Customer Service in the legacy application zone using a SOAP client.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CustomerClient extends WebServiceGatewaySupport {

 private static final Logger log = LoggerFactory.getLogger(CustomerClient.class);

 private static final String ROOT_NAMESPACE = "http://kennybastani.com/guides/customer-service/";
 private static final String GET_CUSTOMER_NAMESPACE = "getCustomerRequest";
 private static final String UPDATE_CUSTOMER_NAMESPACE = "updateCustomerRequest";

 public GetCustomerResponse getCustomerResponse(String username) {
 ...
 }

 public UpdateCustomerResponse updateCustomerResponse(Profile profile) {
 ...
 }
}

In the snippet above we find the definition of the CustomerClient. This class will provide the Profile Service with a capable SOAP client that can retrieve a Customer record from the legacy Customer Service. We’ll use this client from the ProfileServiceV1 class below to retrieve the Customer domain data that we will be extending in the Profile object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class ProfileServiceV1 {

 private ProfileRepository profileRepository;
 private CustomerClient customerClient;

 @Autowired
 public ProfileServiceV1(ProfileRepository profileRepository, CustomerClient customerClient) {
 this.profileRepository = profileRepository;
 this.customerClient = customerClient;
 }

 public Profile getProfile(String username) {
 ...
 }

 public Profile updateProfile(Profile profile) {
 ...
 }
}

The code snippet above contains the definition of the ProfileServiceV1 class. This bean will conditionally call the legacy Customer Service by making a SOAP request from the CustomerClient. The getProfile method is called by the ProfileControllerV1 class, returning a Profile object that extends domain data from the legacy Customer object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Profile getProfile(String username) {

 
 Profile profile = profileRepository.getProfileByUsername(username);

 if (profile == null) {
 
 profile = Optional.ofNullable(customerClient.getCustomerResponse(username)
 .getCustomer())
 .map(p -> new Profile(p.getFirstName(), p.getLastName(),
 p.getEmail(), p.getUsername()))
 .orElseGet(null);

 if (profile != null) {
 
 profile = profileRepository.save(profile);
 }
 }

 return profile;
}

As a part of this workflow, the Profile Service looks to its attached MySQL database using the ProfileRepository to find a Profile record with username as the lookup key. If the Profile for the requested user does not exist in the database, a request to retrieve the Customer object is made to the Customer Service. If the Customer Service returns a Customer record in the response, the base domain data returned from the legacy Customer Service will be used to construct a new Profile record, which is consequently saved by the Profile Service to the attached MySQL database.

Using this workflow, the Profile Service only needs to call the legacy system once for each Profile that is requested. Since we’ve re-routed all requests from other legacy applications to use the Legacy Edge Service, we can safely transition the system of record for domain data away from the legacy Customer Service without performing any risky database migrations. Further, to support backward compatibility in the “large shared database”, we can replicate any updates to the base Customer domain data by scheduling tasks asynchronously to call the Customer Service when a change is made to a Profile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public Profile updateProfile(Profile profile) throws IOException {

 Assert.notNull(profile);

 
 User user = oAuth2RestTemplate.getForObject("http://user-service/uaa/v1/me", User.class);

 
 Profile currentProfile = getProfile(user.getUsername());

 if (currentProfile != null) {
 if (currentProfile.getUsername().equals(profile.getUsername())) {
 
 profile.setId(currentProfile.getId());
 profile.setCreatedAt(currentProfile.getCreatedAt());
 profile = profileRepository.save(profile);

 
 amqpTemplate.convertAndSend("customer.update",
 new ObjectMapper().writeValueAsString(profile));
 }
 }

 return profile;
}

The snippet above is the implementation of updateProfile. In this method we are receiving a request to update the profile of a user. The first step is to ensure that the profile being modified is the user who is currently authenticated. To make sure that only a user that owns the profile can update the domain resource, we check to see if the requested change is different from the profile of the authenticated user. |**|To support backward compatibility with the legacy system, we’ll need to support a different workflow for validating the authenticated user, since the Legacy Edge Service uses client_credentials for authorization.|

After updating the profile, we need to replicate the write back to the customer service. To make sure that the cloud-native application is able to scale writes without dependency on the legacy system, we want to be able to durably replicate the write in an async workflow. By sending a durable message to a RabbitMQ queue, we can use the Profile Service to send back updates to the Customer Service asynchronously without tying up thread and memory resources of the web server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RabbitListener(queues = {"customer.update"})
public void updateCustomer(String message) throws InterruptedException, IOException {
 Profile profile = objectMapper.readValue(message, Profile.class);

 try {
 
 UpdateCustomerResponse response =
 customerClient.updateCustomerResponse(profile);

 if (!response.isSuccess()) {
 String errorMsg =
 String.format("Could not update customer from profile for %s",
 profile.getUsername());
 log.error(errorMsg);
 throw new UnexpectedException(errorMsg);
 }
 } catch (Exception ex) {
 
 throw new AmqpIllegalStateException("Customer service update failed", ex);
 }
}

The snippet above is our message listener on the Profile Service that will asynchronously issue writes back to the Customer Service in the legacy system. Since the network is prone to failure, this workflow ensures that there will be no loss of data since the RabbitMQ message can only be acknowledged after an attempt to update the Customer Service was a success.