The Discovery Service is a platform service that maintains a service registry that is redistributed to applications in the Service Discovery Zone. For this example, we’ll stand up a Eureka Server from the Spring Cloud Netflix.

For the Edge Service, we are again drawing from the Spring Cloud Netflix project to embed a Zuul reverse proxy that acts as a single gateway to each microservice. For front-end applications, we can bind to this Edge Service and use it as a single REST API that provides endpoints for every independent microservice.

The Edge Service is a platform service that uses the service registry from the Discovery Service to provide a public API gateway to the REST APIs exposed by the microservices. We’re using this Edge Service in a similar way as the Legacy Edge Service , but exposing it to the public internet zone. The Edge Service will compose each microservice into a single unified REST API that enforces the OAuth2 client specification. Any consumer of this service will be forced to use the authorization_code grant type, which requires user-level authentication.

The User Service is a platform service that contains an OAuth2 authorization and resource server for accessing any protected resources of our microservices. The User Service will manage and secure how all consumers can access resources from our microservices. Here we are using Spring Cloud Security OAuth2 to issue and validate access tokens for each microservice. The added benefit of using Spring Cloud Security is that access tokens will be relayed in requests microservice-to-microservice, securing an entire chain of requests for resources as a feature of the application framework.

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.

@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.

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.

@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.

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 .

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.

@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); } }