Simplifying Object Creation With The Builder Pattern

Reed Odeneal
Analytics Vidhya
Published in
4 min readNov 22, 2020
If you Builder it…they will read your code easier.

Design patterns are reusable and well-accepted strategies for solving common problems in software architecture. In this post, I’m going to talk about the Builder pattern. One of the Gang of Four design patterns, the Builder pattern can be used to encapsulate and control the creation of a complex object by implementing a separate, concrete Builder class to delegating object creation to rather than trying to build directly through a convoluted constructor. Not only will following this pattern make it easier to instantiate your complex class but it can produce code that is much easier to read and follow — potentially preventing bugs.

I’ll be showing an example of a problem that I ran into recently where I had to create instances of a rather complex object due to some requirements that called for additional, optional constructor parameters.

Example

We have an Event class here that has three required fields and some optional fields for controlling the retryability of an Event:

public class Event {
private final String type;
private final String sender;
private final Map<String, Object> payload;

private Date created;
private boolean retryable;
private int maxRetries;
private int attemptCount;
private Date lastUpdated;

public Event(String type, String sender, Map<String, Object> payload) {
this.type = type;
this.sender = sender;
this.payload = payload;
this.created = new Date();
}

// additional methods omitted for example

}

In this Event class, we have three final fields that we are required to set in our constructor. Our optional fields are here to help us control the retryability of an Event if needed.

We could add them to a new constructor, like so:

public Event(String type, String sender, Map<String, Object> payload, boolean retryable,
int maxRetries, int attemptCount, Date lastUpdated) {
this.type = type;
this.sender = sender;
this.payload = payload;
this.created = new Date();
this.retryable = retryable;
this.maxRetries = maxRetries;
this.attemptCount = attemptCount;
this.lastUpdated = lastUpdated;
}

This starts to add complexity and makes our code less readable and could get messy if we need more control mechanisms to pass to an Event. Another option is to add some setter methods to set the optional retry fields on our Event class.

public void setRetryable(boolean retryable) {
this.retryable = retryable;
}

public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}

public void setAttemptCount(int attemptCount) {
this.attemptCount = attemptCount;
}

public void setLastUpdated(Date lastUpdated) {
this.lastUpdated = lastUpdated;
}

With the setter method approach, if we add even more additional fields in the future we need to remember to call every setter method for our optional fields. Forgetting one could create a runtime exception without some kind of null check in our implementation. Additionally, this could leave an Event in a partial state in between the constructor and setter method class which could have further implications in multithreading situations. So, what do?

Enter, the Builder class. Here, we create a nested, public Builder class inside of our Event class. We create it inside of our Event class so we don’t have to stray far from our code which, in the future, will allow you or another developer to more easily remember to update the Builder when changing the interface of the Event object.

public class Event {

private final String type;
private final String sender;
private final Map<String, Object> payload;

private Date created;
private boolean retryable;
private int maxRetries;
private int attemptCount;
private Date lastUpdated;

private Event(String type, String sender, Map<String, Object> payload) {
this.type = type;
this.sender = sender;
this.payload = payload;
this.created = new Date();
}

public Event(String type, String sender, Map<String, Object> payload, boolean retryable,
int maxRetries, int attemptCount, Date lastUpdated) {
this.type = type;
this.sender = sender;
this.payload = payload;
this.created = new Date();
this.retryable = retryable;
this.maxRetries = maxRetries;
this.attemptCount = attemptCount;
this.lastUpdated = lastUpdated;
}

public void setRetryable(boolean retryable) {
this.retryable = retryable;
}

public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}

public void setAttemptCount(int attemptCount) {
this.attemptCount = attemptCount;
}

public void setLastUpdated(Date lastUpdated) {
this.lastUpdated = lastUpdated;
}

public static class Builder {

private final String type;
private final String sender;
private final Map<String, Object> payload;

private boolean retryable = false;
private int maxRetries = 0;
private int attemptCount = 0;
private Date lastUpdated = new Date();

public Builder(String type, String sender, Map<String, Object> payload) {
this.type = type;
this.sender = sender;
this.payload = payload;
}

public Builder isRetryable(boolean retryable) {
this.retryable = retryable;
return this;
}

public Builder withMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
return this;
}

public Builder withAttemptCount(int attemptCount) {
this.attemptCount = attemptCount;
return this;
}

public Builder withLastUpdated(Date lastUpdated) {
this.lastUpdated = lastUpdated;
return this;
}

public Event build() {
Event event = new Event(type, sender, payload);
event.setRetryable(retryable);
event.setAttemptCount(attemptCount);
event.setMaxRetries(maxRetries);
event.setAttemptCount(attemptCount);
event.setLastUpdated(lastUpdated);
return event;
}
}

}

The Builder class we have created encapsulates, assembles, and creates our Event object now and is called like this:

Map<String, Object> payload = new HashMap<>();
payload.put("message", "Hi!");
Event event = new Event.Builder("myType", "Reed", payload)
.isRetryable(true)
.withMaxRetries(2)
.build();

This is a cleaner approach rather than slugging through a long constructor creation.

Delegating Control

You can take things a step further and delegate control of Event creation to the Builder class by making the Event constructor private leaving the Builder public constructor the sole interface to creating a new Event object.

private Event(String type, String sender, Map<String, Object> payload) {
this.type = type;
this.sender = sender;
this.payload = payload;
this.created = new Date();
}
// disables the ability to call new Event() outside this class

Conclusion

The Builder pattern, a member of the Gang of Four design patterns, can be used to streamline the creation of complex objects making your code more readable and less prone to developer error by removing the need to call a long constructor or call multiple setter methods to create an object.

Links:

--

--