Builder and singleton
Part of the free Low-Level Design course on LogicWiz, in the chapter: Creational patterns. The class-diagram exercises run in the app.
Week 5, Day 3: The order with nine arguments
"new Order(cartId, userId, null, null, false, null, 0, slotId, true). Arjun, which of these nulls is the coupon?"
Priya, reading Arjun's pull request
The constructor nobody can read
A SwiftKart order has two required parts: the cart and the user. It also has many optional parts:
- a coupon code,
- a delivery slot, for orders scheduled later,
- a tip for the rider,
- contactless delivery,
- cash on delivery.
Arjun's first attempt was one constructor with every argument. Callers pass null or false for everything they do not use. Two String arguments next to each other can be swapped by mistake, and the compiler cannot notice. His second attempt was telescoping constructors: one constructor with three arguments, another with four, another with five. That grows fast and still has the same problem.
A third idea is an empty constructor plus setters. Now the order can exist half-built, and nothing checks rules that involve two fields together. For example, SwiftKart does not allow contactless delivery with cash on delivery: the rider must meet the customer to take the cash.
Builder: name each part, check once at the end
A builder is a separate object that collects the parts of another object step by step, with a named method for each part. A final build() call checks the rules and creates the object.
public final class Order {
// final fields, and a private constructor Order(Builder b) that copies them
public static Builder builder(String cartId, String userId) {
return new Builder(cartId, userId);
}
public static final class Builder {
private final String cartId;
private final String userId;
private String couponCode;
private boolean contactless;
private boolean cashOnDelivery;
private Builder(String cartId, String userId) { this.cartId = cartId; this.userId = userId; }
public Builder coupon(String code) { this.couponCode = code; return this; }
public Builder contactless() { this.contactless = true; return this; }
public Builder cashOnDelivery() { this.cashOnDelivery = true; return this; }
public Order build() {
if (contactless && cashOnDelivery) {
throw new IllegalStateException("Contactless orders cannot be paid in cash");
}
return new Order(this);
}
}
}
The call site now reads like a sentence:
Order order = Order.builder("cart-81", "user-7")
.coupon("FLAT50")
.contactless()
.build();
Three details make this a good builder:
- Required parts go in
builder(...), so nobody can forget them. build()checks rules across fields once, in one place.- The
Orderis immutable: its fields arefinal, so no code can change it after it is built.
Do not use a builder for everything. A class with two or three required fields needs a plain constructor, or a Java record. A builder is one more class to keep in step with the fields. In a machine-coding round, a builder for the one complex object is a good touch. A builder for every class is noise.
You have used builders already. Java's StringBuilder collects text step by step, and HttpRequest.newBuilder() in java.net.http builds an HTTP request the same way.
Singleton: one instance, and what it costs
Now a different problem. SwiftKart keeps a cache of stock levels in memory. Building it is slow, and two copies would disagree about how much milk is left. So you want exactly one in the process.
The classic answer is the singleton pattern: a class that allows only one instance and gives global access to it.
public class InventoryCache {
private static InventoryCache instance;
private final Map<String, Integer> stock = new HashMap<>();
private InventoryCache() { }
public static InventoryCache getInstance() {
if (instance == null) {
instance = new InventoryCache();
}
return instance;
}
public int available(String skuId) {
return stock.getOrDefault(skuId, 0);
}
}
Any class can now call InventoryCache.getInstance(). That ease of access is the problem. A singleton has three costs.
- Global state. Any code can read or change it, and method signatures do not show it.
new CartService()looks like it needs nothing, but it reads stock in secret. - Tests. You cannot pass a fake. Stock left by one test leaks into the next, so tests pass alone and fail together.
- Threads. The
getInstanceabove is not thread-safe. Two threads can both seenulland create two caches.
There is a smaller cost too. A lazy singleton is created by its first caller. That caller may be a customer request, which then pays the slow start-up time.
The thread problem has known fixes. Make the field static final and create it eagerly. Or use the holder idiom: a nested class whose static field the JVM creates on first use, safely. Or use an enum with one constant. Double-checked locking also works since Java 5, but only if the field is volatile. It is easy to get wrong, so prefer the other fixes. None of them removes the first two costs.
The alternative: create one, pass it in
You want one instance. You do not need the class itself to enforce it. Create the object once in main and pass it to every class that needs it.
public final class Main {
public static void main(String[] args) {
Inventory inventory = new InMemoryInventory();
CartService carts = new CartService(inventory);
CheckoutService checkout = new CheckoutService(inventory, new FakeGateway());
carts.add("user-7", "milk-500", 2);
System.out.println(checkout.checkout("user-7"));
}
}
main here is the composition root: the one place where objects are created and connected. Both services share the same inventory. A test creates its own fresh one. Every constructor shows what the class needs. This is the constructor injection from Week 4, used for "only one".
A candidate in a mock interview said, "There is only one parking lot, so ParkingLot is a singleton." The interviewer asked two questions. How do you reset it between tests? And what happens when the company opens a second lot? Creating one instance in main answers both.
Can someone still create a second inventory by mistake? Yes, in principle. In practice, only main creates it, and a reviewer can check that in one file. That is a small price for code you can test.
A singleton is acceptable when the object has no changing state and is truly process-wide, such as a logger. If you write one, the enum form is the simplest safe version.
💡 Tip: In an interview, say "I will create one instance in main and inject it." It shows you know the pattern and its costs.
📌 Summary: Use a builder for objects with many optional parts: named steps, checks in build(). Avoid singletons for shared state; create one instance in main and pass it in.
What Arjun learns next
Arjun can now create objects cleanly. Next week he faces classes he cannot change at all: two payment SDKs with different shapes. That is where adapter and facade come in.
Check your understanding
- An Order has 3 required fields, 6 optional ones, and a rule linking two of them. Which design fits best?
- Money has exactly two required fields, amount and currency, and no optional ones. Should it have a builder?
- Every test passes when run alone, but some fail when the whole suite runs. The code calls InventoryCache.getInstance() everywhere. What is the likely cause?
- Two threads call the lazy getInstance() at the same moment, before any instance exists. What can happen?
- An interviewer asks: "Should ParkingLot be a singleton, since there is only one lot?" What is the strongest answer?