Builder Design Pattern

A good builder takes your input the way you understand the object. And, builds the object the way it needs to be built technically. In short, it makes your building process easy irrespective of the underlying complexities !

The Builder is a simple and useful creational pattern to custom build objects and services consisting of multiple parameters.

Comparing it with the factory, the factory has the responsibility of providing the instances of its family of products. But, the builder has the goal to simplify the object building process. Hence, if the underlying products of a factory are complex, we may use a builder to build them.

It’s a widely used pattern to provide a convenient way to build complex components:

  • It may be a multi-parameter based service call like a http service call.
  • It can be a highly configurable component like a spring boot application.
  • Or it might be a multiple-input based complex object structure like a query object.

In all such cases, a builder pattern can provide a simplified and client friendly object building interface. One of the common and simple example in java being the StringBuffer, to append or do other manipulations on multiple strings.

In this article we will first look at why and how to use the builder pattern. And, then follow it up with some real world code snippets to understand its usages better.

Why and How to use the Builder ?

Why should we use it ?

When we design, we do it from a technical point of view keeping the design principle in mind. But, when we use it or write a test case for the system, we hope it were as simple and close to our business requirements. The builder helps us achieve it by bridging the gap. In other words, it provides a business friendly interface, hiding the underlying technical complexities.

 

Let’s say we want to create usage limits for cards in a banking application. In order to create limits for our multiple usage types, we have a separate UsageLimit object.

Even with this simple object structure, below is the difference in creating the CardUsageLimit for our test cases for instance.

 

package spectutz.dp.creational.builder.client;

import spectutz.dp.creational.builder.CardUsageLimitBuilder;
import spectutz.dp.creational.builder.UsageLimitBuilder;
import spectutz.dp.creational.builder.card.CardType;
import spectutz.dp.creational.builder.card.CardUsageLimit;
import spectutz.dp.creational.builder.card.UsageType;

public class CardUsageUsingBuilder {
	
	public static void main(String[] args) {
        
		//Build your usage limit the same way as you understand your business
		CardUsageLimit cardUsageLimit = 
				
				new CardUsageLimitBuilder("2334-675-6775", CardType.DEBIT)
				
				.addUsageLimit(new UsageLimitBuilder(UsageType.ATM)
						           .isAllowed(true).setMaxLimit(25000).build())
				.addUsageLimit(new UsageLimitBuilder(UsageType.MERCHANT_OUTLET)
						           .isAllowed(true).setMaxLimit(20000).build())
				.addUsageLimit(new UsageLimitBuilder(UsageType.ONLINE)
						           .isAllowed(false).build())
				.build();

		System.out.println(cardUsageLimit.toString());
		
	}
	
}
// Output 
/*
Card Usage for Card No: 2334-675-6775 AND Card Type : DEBIT

Card Usage Limits :
Usage Type: ATM; Is Allowed: true; Max Limit: 25000
Usage Type: ONLINE; Is Allowed: false; Max Limit: 0
Usage Type: MERCHANT_OUTLET; Is Allowed: true; Max Limit: 20000
*/
package spectutz.dp.creational.builder.client;

import spectutz.dp.creational.builder.card.CardType;
import spectutz.dp.creational.builder.card.CardUsageLimit;
import spectutz.dp.creational.builder.card.UsageLimit;
import spectutz.dp.creational.builder.card.UsageType;

public class CardUsageWithoutBuilder {
	
	public static void main(String[] args) {
		
		//Build your usage limit as per your underlying data structure
		CardUsageLimit cardUsageLimit = new CardUsageLimit();
		cardUsageLimit.setCardNumber("2334-675-6775");
		cardUsageLimit.setCardType(CardType.DEBIT);
		
		UsageLimit atmUsageLimit = new UsageLimit();
		atmUsageLimit.setUsageType(UsageType.ATM);
		atmUsageLimit.setAllowedFlag(true);
		atmUsageLimit.setMaxLimit(25000);
		
		UsageLimit merChantOutLetUsageLimit = new UsageLimit();
		merChantOutLetUsageLimit.setUsageType(UsageType.MERCHANT_OUTLET);
		merChantOutLetUsageLimit.setAllowedFlag(true);
		merChantOutLetUsageLimit.setMaxLimit(20000);
		
		UsageLimit onlineUsageLimit = new UsageLimit();
		onlineUsageLimit.setUsageType(UsageType.ONLINE);
		onlineUsageLimit.setAllowedFlag(false);
		
		cardUsageLimit.addUsageLimit(atmUsageLimit);
		cardUsageLimit.addUsageLimit(merChantOutLetUsageLimit);
		cardUsageLimit.addUsageLimit(onlineUsageLimit);

		System.out.println(cardUsageLimit);
	}
	
}

// Output 
/*
Card Usage for Card No: 2334-675-6775 AND Card Type : DEBIT

Card Usage Limits :
Usage Type: ATM; Is Allowed: true; Max Limit: 25000
Usage Type: ONLINE; Is Allowed: false; Max Limit: 0
Usage Type: MERCHANT_OUTLET; Is Allowed: true; Max Limit: 20000
*/

In the above example, we can clearly figure out how a builder makes our client interface compact and much more readable. Moreover, it keeps the client interface independent of the underlying data structure.

How does it work ?

A builder pattern basically consists of two different types of APIs :

  1. Fluent and Business Friendly APIs to Collect a Series of Inputs :
    • Regardless of the underlying data structure, we can design these apis in a business friendly manner.
      • The fluent apis makes the builder code highly readable.
    • For example, even for complex queries and expression objects these apis can still look non-technical.
  2. A Build API to Create and Return the Desired Object :
    • This is where we do the final processing, if any, to build and return the desired object.

With the client code shown above, below are the objects and their builders for the card usage example.

package spectutz.dp.creational.builder;

import spectutz.dp.creational.builder.card.CardType;
import spectutz.dp.creational.builder.card.CardUsageLimit;
import spectutz.dp.creational.builder.card.UsageLimit;

public class CardUsageLimitBuilder {
	
    CardUsageLimit cardUsageLimit;	

    public CardUsageLimitBuilder(String cardNumber, CardType cardType){
    	cardUsageLimit = new CardUsageLimit();
    	cardUsageLimit.setCardNumber(cardNumber);
    	cardUsageLimit.setCardType(cardType);
    }
    public CardUsageLimitBuilder addUsageLimit(UsageLimit usageLimit){
    	cardUsageLimit.addUsageLimit(usageLimit); 
    	return this;
    }
    
    public CardUsageLimit build() {
    	return cardUsageLimit;	
    }
    
}
package spectutz.dp.creational.builder;

import spectutz.dp.creational.builder.card.UsageLimit;
import spectutz.dp.creational.builder.card.UsageType;

public class UsageLimitBuilder {
	
    UsageLimit usageLimit;	

    public UsageLimitBuilder(UsageType usageType){
    	this.usageLimit = new UsageLimit();
    	usageLimit.setUsageType(usageType);
    }
    
    public UsageLimitBuilder isAllowed(boolean isAllowed){
    	usageLimit.setAllowedFlag(isAllowed);
    	return this;
    }
    public UsageLimitBuilder setMaxLimit(int maxLimit){
    	usageLimit.setMaxLimit(maxLimit);
    	return this;
    }
    public UsageLimit build(){
    	return usageLimit;
    }
    
}
package spectutz.dp.creational.builder.card;

import java.util.HashMap;
import java.util.Map;

public class CardUsageLimit {	
	private String cardNumber;
	private CardType cardType;
    private Map<UsageType, UsageLimit> usageLimits = new HashMap<UsageType, UsageLimit>();
    
	public String getCardNumber() {
		return cardNumber;
	}
	public void setCardNumber(String cardNumber) {
		this.cardNumber = cardNumber;
	}
	public CardType getCardType() {
		return cardType;
	}
	public void setCardType(CardType cardType) {
		this.cardType = cardType;
	}
	public Map<UsageType, UsageLimit> getUsageLimits() {
		return usageLimits;
	}
	public void addUsageLimit(UsageLimit usageLimit) {
		this.usageLimits.put(usageLimit.getUsageType(), usageLimit);
	}
	
	public String toString() {
		StringBuffer cardUsageLimit = new StringBuffer("Card Usage for Card No: "+this.cardNumber);
		cardUsageLimit.append(" AND Card Type : "+this.cardType)
		.append("\n\nCard Usage Limits :");
		
		usageLimits.forEach((k, v) -> {
			cardUsageLimit.append("\nUsage Type: " + k )
			.append("; Is Allowed: " + v.isAllowed())
			.append("; Max Limit: " + v.getMaxLimit());
		});
		
		return cardUsageLimit.toString();
	}
}
package spectutz.dp.creational.builder.card;

public class UsageLimit {
	
	private UsageType usageType;
	private boolean isAllowed;
	private int maxLimit;
	
	public UsageType getUsageType() {
		return usageType;
	}
	public void setUsageType(UsageType usageType) {
		this.usageType = usageType;
	}
	public boolean isAllowed() {
		return isAllowed;
	}
	public void setAllowedFlag(boolean isAllowed) {
		this.isAllowed = isAllowed;
	}
	public int getMaxLimit() {
		return maxLimit;
	}
	public void setMaxLimit(int maxLimit) {
		this.maxLimit = maxLimit;
	}
	

}
package spectutz.dp.creational.builder.card;

public enum CardType {	
	DEBIT, CREDIT;
}
package spectutz.dp.creational.builder.card;

public enum UsageType {	
	ATM, MERCHANT_OUTLET, ONLINE;
}

 

Some Realtime Usages

 

Use Case -1

The below is a sample code for creating an EhCache(2.8 version) which is a highly customizable component. By default, it comes with lots of default configurations but, it lets us customize many of them through CacheConfiguration.

Here, the CacheConfiguration uses the builder pattern. And, as we can see, the pattern makes it fluent and flexible to add our custom options.

//Create a singleton CacheManager using defaults
CacheManager manager = CacheManager.create();

// CacheConfiguration : Update as many configs as you want where the rest will run with the defaults.
Cache sampleCache= new Cache(
  new CacheConfiguration("sampleCache", maxEntriesLocalHeap)
    .memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU)
    .eternal(false)
    .timeToLiveSeconds(60)
    .timeToIdleSeconds(30)
    .diskExpiryThreadIntervalSeconds(0)
    .persistence(new PersistenceConfiguration().strategy(Strategy.LOCALTEMPSWAP)));

  manager.addCache(sampleCache);

 

Use Case-2

A Builder can hide a lot of complexities from you.

In case of stuffs like hibernate queries or elastic search queries, building the desired outcome manually will be lot difficult.

Here, the builder takes our input using a simpler, business friendly interface. Then, it collects them into a technically suitable intermediate object. Finally, it uses the intermediate object to produce our desired output. Moreover, sometimes we may need the output in multiple formats. The hibernate queries supporting multiple databases for instance.

Below is an example to build a search query to find laptops with RAM greater than 8GB and screen size between 15-15.9 inch.

The two tabs below show, how we build vs what we need. The difference in their complexities shows the beauty of the builder pattern.

		BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
				.must(QueryBuilders.termQuery("product-category", "laptop")) 
				.must(QueryBuilders.termQuery("RAM-GB", "4")) 
				.must(QueryBuilders.rangeQuery("screen-size").gte(15).lte(15.9)); 
		
		
		System.out.println(boolQuery.toString());

{
  "bool" : {
    "must" : [
      {
        "term" : {
          "product-category" : {
            "value" : "laptop",
            "boost" : 1.0
          }
        }
      },
      {
        "term" : {
          "RAM-GB" : {
            "value" : "4",
            "boost" : 1.0
          }
        }
      },
      {
        "range" : {
          "screen-size" : {
            "from" : 15,
            "to" : 15.9,
            "include_lower" : true,
            "include_upper" : true,
            "boost" : 1.0
          }
        }
      }
    ],
    "adjust_pure_negative" : true,
    "boost" : 1.0
  }
}

 

Conclusion

To summarize what we have seen above, the builder is a quite an useful design pattern to make our life easy. It helps us hide the unavoidable technical complexities from the end users. Irrespective of the underlying complexities, the builder can collect the inputs in a much more compact and readable format.