In this tutorial, we will solve the a real-world problem and solve it with the template pattern.

The real-world business problem that I had in my real job

We had one microservice that just worked as a proxy to Salesforce. 

Our proxy service had multiple endpoints such as getCarsByCriteria ,  getPersonsByCriteria,  getCarsWithExpiredInsurance, etc. Inside the proxy service, we transform the API request into the Salesforce queries(similar to the SQL queries), call Salesforce through HTTP, and convert that result to the proxy service response. So simple.

When I joined the company, that service was already written and it was in terrible shape. The code inside the proxy service looked like this:

public class CarService {
    
    private SalesForceClient salesForceClient;

    public List<CarResponse> getCarsWithExpiredInsurance() {
        salesForceClient.get("select id,car_name__c,car_brand from car where expired = true");
        //mapping to the reponseList
        return reponseList;
    }
    
    public List<CarResponse> getAllCars() {
        salesForceClient.get("select id,car_name__c,car_brand from car");
        //mapping to the reponseList
        return reponseList;
    }
    
    public List<CarResponse> getAllCarsWithCertainBrand(String brand) {
        salesForceClient.get("select id,car_name__c,car_brand from car where brand = '" + brand + "'");
        //mapping to the reponseList
        return reponseList;
    }
}

Problems of this approach

As you can see, pure and simple string concatenation. Problems are:

  • With every new query, there was no reusing of old code. When you want to put strings inside where part, strings are surrounded by ' but when you put dates, they should not be surrounded by '. There are a bunch of other small things that you cannot reuse from query to query.
  • There is no structure at all. When adding a new query that needs to fetch a person and its cars, developers were uncertain if they should add that feature to CarServicePersonService, or maybe create a new service.
  • If you want to add pagination to all queries, you need to append that in all queries or somehow wrap all services with some magic to support pagination.
  • There is no way to unit test only query creation because CarService breaks the Single Responsibility principleCarService creates a query and then calls the Salesforce client. Those two things need to be separated.

Resolve problem step by step

Firstly, I helped one of my colleagues apply builder and composite patterns to this string concatenation. You can read more about composite here and about builder here. The result of applying those patterns is that now we can create queries in the following fashion:

QueryBuilder queryBuilder = QueryBuilder.builder()
        .select(Arrays.asList("id", "name", "age"))
        .from("persons")
        .where(
                new NotEqualsCondition("id", 5)
                        .and(new EqualsCondition("name","Rick"))
                        .and(new LikeCondition("address", "Street%"))
                        .and(new HigherEqualsCondition("age",18))
                        .and(new EqualsCondition("hairColor", "Black"))
                        .or(new InCondition("city",String.join(",", Arrays.asList("Manchester", "London"))))
                        .or(new InCondition("city",String.join(",", Arrays.asList("Paris", "Nice"))).not())
        ).groupBy("country").orderBy("creationDate").limit(10).build();

QueryBuilder class is a result of the builder pattern and all of those *Condition classes are consequences of the composite pattern.

This looks much better than string concatenation. But still, we have a problem that for every new query, we need to create a new method, put that method in some service, there is still no structure between queries, etc.

Now, let’s think. Every query has a similar template. It must contain select and from parts and optional parts are wheregroup by, and order by. Every query should override those parts specifically.

Template pattern applied

Let’s create that template first:

public abstract class QueryCreator {

  public abstract List<String> select();

  public abstract String from();

  public abstract <T> Condition where(T request);

  public String groupBy() {
    return null;
  }

  public <T> String createQuery(T request) {
    QueryBuilder.Builder builder = QueryBuilder
            .builder()
            .select(select())
            .from(from())
            .where(where(request))
            .groupBy(groupBy());

    String soql = builder.build().getQuery();
    return soql;
  }
}

and let’s implement that template for one concrete query(getAllCarsWithCertainBrand for example)

public class GetAllCarsWithCertainBrand extends QueryCreator {
  @Override
  public List<String> select() {
    return asList("id", "car_name__c","car_brand");
  }

  @Override
  public String from() {
    return "car";
  }

  @Override
  public <T> Condition where(T request) {
    GetAllCarsWithCertainBrandRequest req = (GetAllCarsWithCertainBrandRequest) request;
    return new EqualsCondition("brand",req.getBrand());
  }
}

This is how we use our creator:

public class CarService {

    private GetAllCarsWithCertainBrand getAllCarsWithCertainBrand;


    public List<CarResponse> getAllCarsWithCertainBrand(GetAllCarsWithCertainBrandRequest request) {
        salesForceClient.get(getAllCarsWithCertainBrand.createQuery(request));
        //mapping to the reponseList
        return reponseList;
    }
}

Important to notice is that abstract template logic is inside the QueryCreator class and the specific implementation of the template is in concrete classes that extend QueryCreator (in our example we have one query GetAllCarsWithCertainBrand). As you can see, the responsibility to call salesforce is now inside CarService. Responsibility to create a query is inside GetAllCarsWithCertainBrand which means that we satisfy the Single responsibility principle.

Conclusion

All listed problems are solved with this approach:

  • You implemented salesforce syntax inside your builder so you don’t need to implement that again and again.
  • There is a simple structure(our template). When you want to support a new query, create a new subclass of QueryCreator and that’s it.
  • Since now the responsibility of QueryCreator is to create a query, you can create a unit test for it.
  • If you want to support pagination for all queries, you can do that inside createQuery method. You can also expose one orderBy method from QueryCreator class and in every query you can implement a different order. That is one place. You don’t need to duplicate pagination code across multiple classes.