Skip to content
Spring Boot sb design-patterns 3 min read

Builder Pattern

The builder pattern separates the construction of a complex object from its representation, letting you assemble it step by step through a fluent, readable API instead of a long positional constructor. Spring and its ecosystem lean on builders constantly — for your own DTOs via Lombok, and throughout the framework for URIs, HTTP clients, security users, and test requests.

Why builders

A constructor with many parameters — especially several of the same type — is unreadable and easy to get wrong:

// Which boolean is which? Easy to transpose arguments.
new UserDto(1L, "Ada", "[email protected]", true, false);

A builder names every value, makes order irrelevant, and lets callers set only what they need:

UserDto user = UserDto.builder()
        .id(1L)
        .name("Ada")
        .email("[email protected]")
        .active(true)
        .build();

Lombok @Builder for DTOs and entities

In Spring Boot you almost never hand-write a builder — Lombok generates it. This is the most common use of the pattern in application code:

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class UserDto {
    private Long id;
    private String name;
    private String email;
    @Builder.Default
    private boolean active = true;
}

It pairs naturally with mapping an entity to a response DTO in a service:

@Service
public class UserMapper {
    public UserDto toDto(User entity) {
        return UserDto.builder()
                .id(entity.getId())
                .name(entity.getName())
                .email(entity.getEmail())
                .active(entity.isActive())
                .build();
    }
}

The dedicated Lombok @Builder page covers @Builder.Default, @Singular for collections, and toBuilder for copy-and-modify. For DTOs specifically, see the DTO pattern.

Tip: Combine @Builder with @Getter (not @Setter) for immutable response DTOs. For Java records, you usually do not need a builder — the canonical constructor is already concise — though a builder still helps when a record has many optional fields.

Framework builders you use every day

Spring exposes the builder pattern through many fluent APIs. The shape is always the same: a static entry point, chained configuration calls, and a terminal build().

BuilderWhat it buildsTypical use
UriComponentsBuilderA URI with path/query paramsConstructing outbound URLs safely
RestClient.builder()A configured HTTP clientNew synchronous HTTP clients
WebClient.builder()A reactive HTTP clientNon-blocking HTTP calls
MockMvcRequestBuildersA simulated HTTP requestWeb-layer tests
User.builder()A Spring Security UserDetailsIn-memory / test users
ResponseEntity.ok()...build()An HTTP responseReturning status + headers + body

Building a URI

import org.springframework.web.util.UriComponentsBuilder;

URI uri = UriComponentsBuilder
        .fromUriString("https://api.example.com")
        .path("/products/{id}")
        .queryParam("expand", "reviews")
        .build(42);
// https://api.example.com/products/42?expand=reviews

Building an HTTP client

import org.springframework.web.client.RestClient;

RestClient client = RestClient.builder()
        .baseUrl("https://api.example.com")
        .defaultHeader("Accept", "application/json")
        .build();

This is exactly the @Bean factory method shown in Factory & Abstract Factory — a builder producing a managed bean.

Building a Security user

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

UserDetails admin = User.builder()
        .username("admin")
        .password("{noop}secret")     // {noop} = no encoding, demo only
        .roles("ADMIN")
        .build();

Building a test request

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;

mockMvc.perform(post("/api/users")
        .contentType(MediaType.APPLICATION_JSON)
        .content("{\"name\":\"Ada\"}"))
       .andExpect(status().isCreated());

Note: Notice the recurring grammar: a static factory entry (builder(), fromUriString, post), one chained call per option, and a closing build() (or perform). Once you recognise the pattern, every new Spring fluent API reads the same way.

Last updated June 13, 2026
Was this helpful?