Solving the “Too many Swaggers” problem in a Microservice architecture

Deepak Choudhary
4 min readApr 7, 2020

--

First, some context:

Microservices are still a buzzword till date. We have thousands of teams starting their new application with a microservice architecture and the remaining trying to refactor their monoliths to a microservice architecture. Now this article is not going to talk about what microservices are or what benefits they propose. If you are here and reading this article, I expect you to understand that and I shall only be your guide in your struggle to solve the one common problem people run into.

Another thing to note is that I will be using Spring Boot here as I believe Java or let’s say the Spring framework is still riding high on popularity at least in the enterprise environment.

“If you are working in an organization that places lots of restrictions on how developers can do their work, then microservices may not be for you.”
Sam Newman, Building Microservices

Now, the Problem:

As I started slowly, yet steadily disintegrating my monolith into small microservices, each powerful in it’s own accord, I see a problem raising it’s ugly head. Earlier, I had one swagger endpoint where I found all of my API documentation. However, with the increase in services, the swagger endpoints too seem to have increased. Now I have to remember each service’s URL in order to fetch the API documentation.

BEHOLD!! The Solution:

Microservices are all about separation of concerns, services having a bounded context within which they operate.

How about we create a microservice especially to aggregate these API definitions and serve the user?

EUREKA!!!

This way I don’t have to provide multiple swagger endpoints for my entire system.

Let’s take a look at what we need to do :

  1. Create a microservice i.e. documentation-service] and register it on your registry server.[We will use Eureka for this example]
  2. Pull the swagger definition for each instance of a microservice registered on the registry server and save it on our documentation-service in a data structure which best suits a key-value pair. Any guesses ?
  3. Keep refreshing these definitions at regular intervals so as to keep em as fresh as a daisy.
  4. Create an endpoint to serve the definition to the user based on which service the user chooses to see.

Easy peasy lemon squeezy !!!

High Level Architecture of Solution

Now all we need to do is add the following classes to our documentation-service.

@Component
@Scope(scopeName= ConfigurableBeanFactory.SCOPE_SINGLETON)
public class ServiceDefinitionsContext {

private final ConcurrentHashMap<String, String> serviceDescriptions;

private ServiceDefinitionsContext(){
serviceDescriptions = new ConcurrentHashMap<String, String>();
}

public void addServiceDefinition(String serviceName, String serviceDescription){
serviceDescriptions.put(serviceName, serviceDescription);
}

public String getSwaggerDefinition(String serviceId){
return this.serviceDescriptions.get(serviceId);
}

public List<SwaggerResource> getSwaggerDefinitions(){
return serviceDescriptions.entrySet().stream().map( serviceDefinition -> {
SwaggerResource resource = new SwaggerResource();
resource.setLocation("/service/"+serviceDefinition.getKey());
resource.setName(serviceDefinition.getKey());
resource.setSwaggerVersion("2.0");
return resource;
}).collect(Collectors.toList());
}
}

Here, we are creating our local storage using a concurrent hashmap which will store the definitions fetched from the registry server.

@Component
public class ServiceDescriptionUpdater {

private static final Logger logger = LoggerFactory.getLogger(ServiceDescriptionUpdater.class);

private static final String DEFAULT_SWAGGER_URL="/v2/api-docs";
private static final String KEY_SWAGGER_URL="swagger_url";

@Autowired
private DiscoveryClient discoveryClient;

private final RestTemplate template;

public ServiceDescriptionUpdater(){
this.template = new RestTemplate();
}

@Autowired
private ServiceDefinitionsContext definitionContext;

@Scheduled(fixedDelayString= "${swagger.config.refreshrate}")
public void refreshSwaggerConfigurations(){
logger.debug("Starting Service Definition Context refresh");

discoveryClient.getServices().stream().forEach(serviceId -> {
logger.debug("Attempting service definition refresh for Service : {} ", serviceId);
List<ServiceInstance> serviceInstances = discoveryClient.getInstances(serviceId);
if(serviceInstances == null || serviceInstances.isEmpty()){ //Should not be the case kept for failsafe
logger.info("No instances available for service : {} ",serviceId);
}else{
ServiceInstance instance = serviceInstances.get(0);
String swaggerURL = getSwaggerURL( instance);

Optional<Object> jsonData = getSwaggerDefinitionForAPI(serviceId, swaggerURL);

if(jsonData.isPresent()){
String content = getJSON(serviceId, jsonData.get());
definitionContext.addServiceDefinition(serviceId, content);
}else{
logger.error("Skipping service id : {} Error : Could not get Swagegr definition from API ",serviceId);
}

logger.info("Service Definition Context Refreshed at : {}", LocalDate.now());
}
});
}

private String getSwaggerURL(ServiceInstance instance){
String swaggerURL = instance.getMetadata().get(KEY_SWAGGER_URL);
return swaggerURL != null ? instance.getUri()+swaggerURL : instance.getUri()+DEFAULT_SWAGGER_URL;
}

private Optional<Object> getSwaggerDefinitionForAPI(String serviceName, String url){
logger.debug("Accessing the SwaggerDefinition JSON for Service : {} : URL : {} ", serviceName, url);
try{
Object jsonData = template.getForObject(url, Object.class);
return Optional.of(jsonData);
}catch(RestClientException ex){
logger.error("Error while getting service definition for service : {} Error : {} ", serviceName, ex.getMessage());
return Optional.empty();
}

}

public String getJSON(String serviceId, Object jsonData){
try {
return new ObjectMapper().writeValueAsString(jsonData);
} catch (JsonProcessingException e) {
logger.error("Error : {} ", e.getMessage());
return "";
}
}
}

ServiceDescriptionUpdater.java contains code to fetch the swagger defintions from our registry server every 5 seconds.

And finally,

@Configuration
public class SwaggerUIConfiguration {

@Autowired
private ServiceDefinitionsContext definitionContext;

@Bean
public RestTemplate configureTempalte(){
return new RestTemplate();
}

@Primary
@Bean
@Lazy
public SwaggerResourcesProvider swaggerResourcesProvider(InMemorySwaggerResourcesProvider defaultResourcesProvider, RestTemplate temp) {
return () -> {
List<SwaggerResource> resources = new ArrayList<>(defaultResourcesProvider.get());
resources.clear();
resources.addAll(definitionContext.getSwaggerDefinitions());
return resources;
};
}
}

Here is where the magic happens. We are extending the documentation by providing a customer implementation of SwaggerResourcesProvider.

AND DONE!!!

You have successfully created a centralized swagger documentation server where one can find all API documentations for your different microservices.

If you liked the article and appreciate what I am doing, show some love in the form of comments and claps.

--

--

Deepak Choudhary

Technology evangelist engineering solutions on weekdays and exploring life on the weekends. The joy of life lies in the gray zone.