Sometimes we make mistakes like defining long constructors for data-intensive classes. Sometimes we are used to adding multiple constructors to fit our needs, leading to classes that are hard to read because the dev has to review the docs or the code itself to ensure what data is required and which position takes.

Not to mention that we might have to set values like null or zeroes leading to possible NPE or misbehaviors because a value was misplaced.

Take a look at the following example that illustrates the problem:

public class GameQuery {

    private final String title;
    private final Platform platform;
    private int releaseYear;
    private String genere;
    private boolean onlineRequired;
    private ESRBRating rating;


    public GameQuery(String title, Platform platform) {
        this.title = title;
        this.platform = platform;
    }

    public GameQuery(String title, Platform platform, int releaseYear, String genere, boolean onlineRequired, ESRBRating rating) {
        this.title = title;
        this.platform = platform;
        this.releaseYear = releaseYear;
        this.genere = genere;
        this.onlineRequired = onlineRequired;
        this.rating = rating;
    }

    public GameQuery(String title, Platform platform, boolean onlineRequired, ESRBRating rating) {
        this.title = title;
        this.platform = platform;
        this.onlineRequired = onlineRequired;
        this.rating = rating;
    }

}

In this example, I have two required parameters (title and platform) and a set of optional fields. So, to create an object that fits with, let's say, a database result mapping, for example, I've added a constructor that works with the record I'm getting from a database query.

Then my constructor doesn't fit with other's needs, and another constructor is needed, and so on.

We filled the class with multiple constructors that sometimes doesn't make sense for a new dev that just joined the team because creating a new instance will look like this:

GameQuery gameQuery = new GameQuery("Halo Ultimate", Platform.XBOX, 2021, "Shooter",true, ESRBRating.M);
What is true?, 2021? , What does that enum mean?

And what about this one:

GameQuery gameQuery = new GameQuery("Halo Ultimate", Platform.XBOX, -1, "Shooter",true, null);
Why is that last field set to null?, What does -1 mean?

Using a builder can help us make the instantiation less ambiguous, cleaner, and readable:

public class GameQuery {

    private final String title;
    private final Platform platform;
    private int releaseYear;
    private String genere;
    private boolean onlineRequired;
    private ESRBRating rating;
    
    public static class Builder{

        private final String title;
        private final Platform platform;
        private String genere;
        private ESRBRating esrbRating;
        private boolean onlineRequired;

        private int releaseYear;

        public Builder(String title, Platform platform){
            this.title = title;
            this.platform = platform;
        }
        public Builder releaseYear(int releaseYear){
            this.releaseYear = releaseYear;
            return this;
        }

        public Builder genere(String genere){
            this.genere = genere;
            return this;
        }

        public Builder rating(ESRBRating rating){
            this.esrbRating = rating;
            return this;
        }

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

        public GameQuery build(){
            return new GameQuery(this);
        }
    }

    public GameQuery(Builder builder) {
        title = builder.title;
        platform = builder.platform;
        releaseYear = builder.releaseYear;
        genere = builder.genere;;
        onlineRequired = builder.onlineRequired;;
        rating = builder.esrbRating;
    }


    @Override
    public String toString() {
        return "GameQuery{" +
                "title='" + title + '\'' +
                ", platform=" + platform +
                ", releaseYear=" + releaseYear +
                ", genere='" + genere + '\'' +
                ", onlineRequired=" + onlineRequired +
                ", rating=" + rating +
                '}';
    }
}

Yes, it looks like we added more code than the original approach, but we gained some benefits.

Next is how a new instance of GameQuery looks like using the builder we just implemented:

GameQuery query = new GameQuery.Builder("Persona 5", Platform.PS5)
                .releaseYear(2019)
                .onlineRequired(false)
                .genere("RPG")
                .rating(ESRBRating.T)
                .build();

It is way easier to read and maintain. You can see which parameters are required and just set the optional ones.

Also, we made our class extensible because adding a new attribute is easy as adding a new method to the builder and update a unique constructor in the GameQuery class.

Of course, it doesn't mean that you have to replace all your constructors with builders, as most programming patterns and tools are situational. In this case, you might want to use it when you know that your class will grow over time or you have many fields to populate during the instantiation (say, four or more parameters).