LLD Dojo

Builder

Core. Expect to meet this one, and expect to be asked for it by name.

Start with the problem

A shopping cart needs a catalogue to price its items. Everything else about a cart has a reasonable default: no coupons, no promotions, free delivery. A constructor that only asks for what a cart cannot work without is the obvious starting point.

Cart cart = new Cart(catalog, new CouponBook(Map.of()), List.of(), new Totals(NO_DELIVERY_CHARGE));

That reads fine once. The catalogue is required, and the rest are the defaults spelled out at the call site because there is nothing else to pass yet.

Watch where it goes

A seasonal promotion arrives: buy two, get one free. The constructor grows a parameter for it. Every existing call site that built a cart now has to restate the three arguments it was never changing, to add the fourth.

A second promotion stacks on top of the first: a loyalty discount. The constructor would need a list this time, since one promotion parameter cannot hold two. Whoever writes that list has to build it before the call. Every caller that wants neither promotion still has to pass an empty list, because the constructor has no way to leave it out.

The real problem shows up once two carts in the same codebase want different combinations. A test wants a cart with only the loyalty discount. A demo wants both. Neither wants to write a four-or-five-argument constructor call and get the order right by memory. A constructor also gives no room to add "one more promotion" later without touching every caller that already has one.

The move

Split construction into a class whose whole job is collecting the parts, one call at a time, before handing back a finished cart.

public final class CartBuilder {

    private final Catalog catalog;
    private final List<Promotion> promotions = new ArrayList<>();
    private CouponBook coupons = new CouponBook(Map.of());
    private ShippingPolicy shipping = NO_DELIVERY_CHARGE;

    public CartBuilder(Catalog catalog) {
        this.catalog = Objects.requireNonNull(catalog, "catalog");
    }

    public CartBuilder coupons(CouponBook coupons) {
        this.coupons = Objects.requireNonNull(coupons, "coupons");
        return this;
    }

    public CartBuilder plusPromotion(Promotion promotion) {
        promotions.add(Objects.requireNonNull(promotion, "promotion"));
        return this;
    }

    public ShoppingCartApi build() {
        return new Cart(catalog, coupons, List.copyOf(promotions), new Totals(shipping));
    }
}

This is corpus/shopping-cart/reference/src/CartBuilder.java. A test that wants only the loyalty discount calls plusPromotion once. A demo that wants both calls it twice. Neither one writes a list by hand, and neither one restates a default it is not changing.

What modern Java changes here

The catalogue argument sits in the constructor, not in a catalog(Catalog) method the way the other parts do. That is the part of this pattern people leave out. Leaving it out is what turns a builder into a constructor a caller can forget to finish. Whatever a cart genuinely cannot work without belongs in new CartBuilder(catalog), where the compiler enforces it. Only the genuinely optional parts get their own chained method.

Records give a builder less to do than it used to have. A value with three or four required fields and no accumulation needs no builder at all. This corpus's LineRequest and Sku are both that shape: a record constructor already states every field once, and nothing accumulates. The pattern earns its file specifically when a caller needs to add an unbounded number of one kind of thing, the way plusPromotion does. A plain constructor has no way to grow like that later without breaking every caller that already uses it.

When naming it is wrong

LineRequest and Sku each take two or three required fields and nothing accumulates. Building a SkuBuilder for either one costs more code for no guarantee a plain constructor does not already give. It also opens a window where a half-built value can exist that could not exist before: call new SkuBuilder(), forget .price(...), and .build() either throws at runtime or returns something silently wrong.

The threshold: reach for a builder when three things are true together. One part is genuinely required, several parts have real defaults, and at least one part can be added more than once at the same call site. Two or three required parameters with nothing that accumulates is a constructor, and adding a builder there is exactly the D3 failure the Standard calls over-engineered (premature interface).

Where this lives in the app

Syllabus item B3 measures CartBuilder against three requirement changes on corpus/shopping-cart: a second stacking voucher, a buy-two-get-one offer, and a new member pricing tier. Each one lands as a new Promotion file rather than an edit to Cart.

All reference pages