Service talking to another Service

There is not a lot of fun in having only a single micro-service. Let’s see discover how the micro-services can work together to achieve a task.

Consider using micro-service patterns when the operational costs are manageable. The size of the micro-service should not be smaller than needed to avoid creation of unreasonable operational overhead.
This kata is showing ways of connecting micro-services and not the best practices of how to design them. You have to apply system analysis and system architectural patterns to identify the best ways to split functionality into multiple services.

In this Kata we’re going to let Accounts service connect and use data provided by Profiles service.

Getting Started

The starting source code is located in kata6/start folder

We have to services: Accounts service and Profiles service. The accounts service is managing list of user accounts in our system and profiles service is in charge of managing user social profiles.

In this kata we’re going to teach Accounts service to extract some personal information of a user from Profiles service when it is asked about user account.

simple connection

Eureka

Eureka: Service Catalog

In distributed systems there should be a way for one service to connect to another. This can be facilitated through a couple of different ways:

  1. Configuration of an IP address

  2. Using a DNS record

  3. Using s Service Catalog/Registry

Going forward we’re going to use Eureka from Netflix as our Service Catalog.

Spring Cloud provides a very simple way to build your own Eureka-based service catalog by embedding eureka in one of your services.

Let’s build one of our own. Create an empty Spring Boot application.

Add Eureka starter package into your class pass by adding the following compile dependency into your build:

eureka-service/build.gradle
compile("org.springframework.cloud:spring-cloud-starter-eureka-server:$springCloudVersion")

Annotate application class with @EnableEurekaServer attribute:

src/main/java/msvcdojo/ProfilesServiceApplication.java
@EnableEurekaServer
public class EurekaServiceApplication {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(EurekaServiceApplication.class, args);
    }
}

Build and launch application. Ensure that you have Configuration Service running prior the launch (see Externalizing Configuration kata).

Open your browser and navigate to http://localhost:8761/. You’re going to see Eureka dashboard:

eureka dashboard

Registering with Service Catalog

From now on each instance of a service should register with Eureka on start and continue keeping it record up-to-date throughout it’s lifecycle.

Add the Eureka starter package as compile dependency for a service:

profiles-service/build.gradle
compile("org.springframework.cloud:spring-cloud-starter-eureka:$springCloudVersion")

Annotate application class with @EnableEurekaClient attribute:

src/main/java/msvcdojo/ProfilesServiceApplication.java
@SpringBootApplication
@EnableEurekaClient
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class ProfilesServiceApplication {

What’s left is to tell the Eureka client where to find the Eureka server. We need to add the configuration block below to all our Web services by putting it into a shared configuration in application.yml shipped by Configuration service from GitHub:

eureka:
  client:
    registryFetchIntervalSeconds: 5
    instanceInfoReplicationIntervalSeconds: 5
    initialInstanceInfoReplicationIntervalSeconds: 5
    serviceUrl:
      defaultZone: ${service-registry.uri:http://127.0.0.1:8761}/eureka/
  instance:
    leaseRenewalIntervalInSeconds: 3
    prefer-ip-address: true
    ip-address: ${external.ip:${spring.cloud.client.ipAddress:localhost}}
    metadataMap:
      instanceId: ${spring.application.instance_id:${random.value}}

Build and start your service. Navigate to Eureka dashboard and you’re going to see your service registered with Eureka:

eureka dashboard with service
Repeat the same process for all of your Web Services from now on.

Service Discovery via Discovery Client

Remember to enable Eureka client for Accounts service.

Now we’re going to let the Accounts Service locate the Profiles Service via Discovery client:

src/main/java/msvcdojo/AccountsServiceApplication.java
@Component
class ProfilesClient {

    private final DiscoveryClient discoveryClient;

    @Autowired
    public ProfilesClient(DiscoveryClient discoveryClient) {
        this.discoveryClient = discoveryClient;
    }

    public URI getProfileUri(Account account) {

        InstanceInfo instance = discoveryClient.getNextServerFromEureka(
                "profiles-service", false);

        String url = instance.getHomePageUrl();

        return UriComponentsBuilder.fromHttpUrl( url + "/profiles/{key}")
                .buildAndExpand(account.getUsername()).toUri();
    }
}

There are a couple of Discovery clients, so make sure you are using this:

src/main/java/msvcdojo/AccountsServiceApplication.java
import com.netflix.discovery.DiscoveryClient;

Now each time an Account resource is retrieved we’d like to augment it with additional information:

src/main/java/msvcdojo/AccountsServiceApplication.java
@Component
class AccountResourceProcessor implements ResourceProcessor<Resource<Account>> {

    ProfilesClient profilesClient;

    @Autowired
    public AccountResourceProcessor(ProfilesClient profilesClient) {
        this.profilesClient = profilesClient;
    }

    @Override
    public Resource<Account> process(Resource<Account> accountResource) {

        Account account = accountResource.getContent();

        URI profileUri = this.profilesClient.getProfileUri(account);

        if (null != profileUri) {
            Link profileLink = new Link(profileUri.toString(), "profile");
            accountResource.add(profileLink);
        }
        return accountResource;
    }
}

Build and start Accounts Service. Request the account information by ID 1:

$ curl http://localhost:8100/accounts/1
{
  "username" : "john",
  "role" : "admin",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8100/accounts/1"
    },
    "account" : {
      "href" : "http://localhost:8100/accounts/1"
    },
    "profile" : {
      "href" : "http://192.168.99.100:8101/profiles/john"
    }
  }
}

Ribbon

Service Discovery via Ribbon load balancer

Discovery client allows you to communicate with Eureka and receive a full list of available instance of another service. Then you can make your own decision which one of these IPs to call. Sometimes it is an easy decision, like random one, but, in most cases you want to have smarter algorithm. Such algorithms can take into consideration geo-location of the services, stability, network latency and other attributes. This means you need to have a pretty smart Load Balancer.

Lucky for us, Netflix guys have build one called Ribbon. Ribbon Load Balancers have extensible architecture which allows you to plug in your own decision logic, but today we’re going to use a default Ribbon Load balancer.

Mofify ProfilesClient to use Ribbon load balanecer:

src/main/java/msvcdojo/AccountsServiceApplication.java
@Component
class ProfilesClient {

    private final LoadBalancerClient loadBalancer;

    @Autowired
    public ProfilesClient(LoadBalancerClient loadBalancer) {
        this.loadBalancer = loadBalancer;
    }

    public URI getProfileUri(Account account) {

        ServiceInstance instance = loadBalancer.choose("profiles-service");
        if (instance == null)
            return null;

        return UriComponentsBuilder.fromHttpUrl( (instance.isSecure() ? "https://" : "http://") +
                instance.getHost() + ":" + instance.getPort() + "/profiles/{key}")
                .buildAndExpand(account.getUsername()).toUri();
    }
}

If you build and start, you should get exactly the same behavior as before:

$ curl http://localhost:8100/accounts/1
{
  "username" : "john",
  "role" : "admin",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8100/accounts/1"
    },
    "account" : {
      "href" : "http://localhost:8100/accounts/1"
    },
    "profile" : {
      "href" : "http://192.168.99.100:8101/profiles/john"
    }
  }
}

Let’s go a little further and augment returned Account information with the information received from Profiles service. Let’s add a fullName field to the returned Account structure.

So we need to create a Profile entity that will wrap the response of the Profiles service:

src/main/java/msvcdojo/AccountsServiceApplication.java
class Profile {

    private String key;
    private String fullName;
    private Integer photoCount;

    public void setKey(String key) { this.key = key; }
    public void setFullName(String fullName) { this.fullName = fullName; }
    public void addPhotoCount(Integer photoCount) { this.photoCount = photoCount; }

    public String getKey() { return key; }
    public String getFullName() {
        return fullName;
    }
    public Integer getPhotoCount() { return photoCount; }
}

Teach the ProfilesClient to extract profile information:

src/main/java/msvcdojo/AccountsServiceApplication.java
public ResponseEntity<Profile> getProfile(URI profileUri) {

    RestTemplate restTemplate = new RestTemplate();
    return restTemplate.getForEntity(profileUri, Profile.class);
}

As soon as our service will receive a Profile information, we’re looking to update the Account entity. Add the following code to the Account entity:

src/main/java/msvcdojo/AccountsServiceApplication.java
@Transient
private String fullName;
@Transient
private Integer photoCount;

public String getFullName() {
    return fullName;
}
public Integer getPhotoCount() { return photoCount; }

public void updateWithProfileData(Profile profile) {
    this.fullName = profile.getFullName();
    this.photoCount = profile.getPhotoCount();
}

Now let’s teach the AccountResourceProcessor to call the Profiles service and update Account entity with the appropriate information:

src/main/java/msvcdojo/AccountsServiceApplication.java
ResponseEntity<Profile> profile = this.profilesClient.getProfile(profileUri);
if (null != profile)
    accountResource.getContent().updateWithProfileData(profile.getBody());

Build, lunch and hit your service:

$ curl localhost:8100/accounts/1
{
  "username" : "john",
  "role" : "admin",
  "fullName" : "John Smith",
  "photoCount" : 0,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8100/accounts/1"
    },
    "account" : {
      "href" : "http://localhost:8100/accounts/1"
    },
    "profile" : {
      "href" : "http://192.168.99.100:8101/profiles/john"
    }
  }
}

Note that now we’ve received the fullName information in the Account entity.

Feign

Inter-Service communication via Feign client

This was a lot of work so far and we’re lucky that Netflix guys, after using this for quite a while, have thought about an easier way of wrapping Inter-Service communication.

They’ve provided a Feign library.

Let’s remove ProfilesClient::getProfile method. Now we’re going to create a Feign client:

src/main/java/msvcdojo/AccountsServiceApplication.java
@FeignClient("profiles-service")
interface ProfilesServiceProxy {

    @RequestMapping(method = RequestMethod.GET, value = "/profiles/{key}")
    ResponseEntity<Profile> getProfile(@PathVariable("key") String key);
}
Note that the Feign client is an interface description of a remote service. It’s a description and not an implementation.

Modify AccountResourceProcessor constructor to receive an Feign proxy auto-generated for you wrapper:

src/main/java/msvcdojo/AccountsServiceApplication.java
ProfilesServiceProxy profilesServiceProxy;

@Autowired
public AccountResourceProcessor(ProfilesClient profilesClient, ProfilesServiceProxy profilesServiceProxy) {
    this.profilesClient = profilesClient;
    this.profilesServiceProxy = profilesServiceProxy;
}

Change the previous call to getProfile in the AccountResourceProcessor::process function to this new shiny one:

src/main/java/msvcdojo/AccountsServiceApplication.java
ResponseEntity<Profile> profile = profilesServiceProxy.getProfile(account.getUsername());
if (null != profile)
    accountResource.getContent().updateWithProfileData(profile.getBody());

Build, launch and hit the service:

$ curl localhost:8100/accounts/1
{
  "username" : "john",
  "role" : "admin",
  "fullName" : "John Smith",
  "photoCount" : 0,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8100/accounts/1"
    },
    "account" : {
      "href" : "http://localhost:8100/accounts/1"
    },
    "profile" : {
      "href" : "http://192.168.99.100:8101/profiles/john"
    }
  }
}

Troubleshooting Feign communication

As you can see we’re getting total magic. Sometimes magic is way too obscure and, in cases, you’d like to see what is happening under the covers, you can turn on the Feign logging.

Add the following code to control what will be logged (in this example we’d like to see everything):

src/main/java/msvcdojo/AccountsServiceApplication.java
@Configuration
class AccountsServiceApplicationConfiguration {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

And then for each Feign client you need to add log-level control in the applicaiton.yml:

accounts-service/src/main/resources/application.yml
logging:
  level:
    msvcdojo.ProfilesServiceProxy: DEBUG

Now if you’ll rerun your service and when you’ll hit it with a REST request, the similar to the following lines will appear in your logs:

2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] ---> GET http://profiles-service/profiles/john HTTP/1.1
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] ---> END HTTP (0-byte body)
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] <--- HTTP/1.1 200 OK (139ms)
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] Transfer-Encoding: chunked
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] Server: Jetty(9.2.14.v20151106)
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] X-Application-Context: profiles-service
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] Date: Thu, 04 Feb 2016 17:32:44 GMT
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] Content-Type: application/hal+json; charset=UTF-8
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile]
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] {
  "fullName" : "John Smith",
  "photoCount" : 0,
  "key" : "john",
  "_links" : {
    "self" : {
      "href" : "http://192.168.99.100:8101/profiles/john"
    },
    "profile" : {
      "href" : "http://192.168.99.100:8101/profiles/john"
    },
    "photos" : {
      "href" : "http://192.168.99.100:8101/profiles/john/photos"
    }
  }
}
2016-02-04 DEBUG [...] [ProfilesServiceProxy#getProfile] <--- END HTTP (340-byte body)