Slimming the Microservice – Proxy Technique

Current State of Backend Engineering

Microservices are the big player when it comes to system design/implementation. Splitting monoliths into compose-able parts is the backbone in the technology evolution business. Naturally this is a welcomed changed across all areas of software. From a development perspective, microservices allow teams can work in parallel on separate services. In the Continuous Integration/Deployment/Deployment (CI/CD) services can be deployed separately. Lastly in the cloud/devops world, microservices can scale separately on demand. But if microservices were so great, what’s the point of writing this article? Well microservices are a great practice, but there is a part of software development that turn microservices into monoliths quickly….REQUIREMENTS. 

B….But I like Jira

I do too, but Jira (or any other tracking software you use), doesn’t actually care about how small your microservice is. There are three parts of requirement. Vetting them, understanding them, and implementing them. That third part is where things get interesting. I’m a visual learner, so lets start with something basic.

Requirement 1: Give customers the ability to verify a shipping address

Seems simple enough, lets code something:

public class AddressVerificationService {
   public AddressValidation verifyAddress(AddressObject address) {
      return GoogleAPI.isAddressValid(address);
   }
}

Easy enough as a start. Our service will utilize Google and return a AddressValidation object. Lets keep adding requirements:

Requirement 2: If the address is invalid for these specific reasons, “Invalid Zip Code, Invalid Country”, call account services to escalate account risk level.

public class AddressVerificationService {
   private static final String INVALID_ZIP = "Invalid Zip Code";
   private static final String INVALID_COUNTRY = "Invalid Country";
   public AddressValidation verifyAddress(AddressObject address) {
     AddressValidation validatedAddress = GoogleAPI.isAddressValid(address);
     if(validatedAddress.getStatus() != "FAILED") {
       return validatedAddress;
     } 

     String failureReason = validatedAddress.getReason();
     if(failureReason.equals(INVALID_ZIP) || failureReason.equals(INVALID_COUNTRY))
       /*functions to inform account services here*/
     return validatedAddress;
  }
}

Now I know what you might be thinking…

  • This doesn’t follow the Single Responsibility Principle
  • Making multiple functions would make this code cleaner

Both of these are solid questions, however we still have a problem. A single requirement has added the contacting of other microservices into our service. The single responsibility principle doesn’t just extend to functions, but to classes as well. The AddressVerificationService with all the refactoring in the world, can’t remove the conditional call to another service (naturally removing this single responsibility of the service). Oddly enough, I’ve only presented two requirements. As requirements grow, think about how this service would scale, even with the best SOLID principles enforced.

So we can’t achieve perfection? YOCO

(you only code once)

Lets refrain from cowboy coding and think of this problem from a different angle. If we think about this as a lack of orchestration problem, we can approach this a different way. If we could keep the services simple but extract the orchestrated parts, then this can reduce our code growth. Our Swiss army knife here will be the Proxy pattern. Without going into too much detail, the Proxy pattern acts as an interface to underlying classes. Using this pattern, lets refactor our above code:

Our Service:

public class AddressVerificationService {
  public AddressValidation verifyAddress(AddressObject address) {
    return GoogleAPI.isAddressValid(address);
  }
}

Our Proxy:

public class AddressProxy {
  public WhateverObject business(AddressObject address) {
    AddressValidation addressValidation = AddressVerificationService.verifyAddress(address);
    String failureReason = validatedAddress.getReason();
    
    if(failureReason.equals(INVALID_ZIP) || failureReason.equals(INVALID_COUNTRY))
      /*functions to inform account services here*/  
    }
   
    /*More area to add new orchestration*/
}

Now this solution is more verbose, but definitely the most scale-able.  Here’s where this pattern shines:

  • The AddressVerificationService doesn’t implement outside service logic, therefore it is still very compose-able and keeps it’ microservice nature.
  • Adding proxies classes in front of microservices allows for easier refactoring because the class handles logic/orchestration only.

Hopefully this will aid in common issues when figuring the best way to refactor your microservices.  For more information on building microservices and patterns for maintaining a codebase, please check out the resources below:

  1. Microservices.io
  2. Microservice Patterns
  3. Cloud Native Java

Leave a comment