Opening low-level design
Progress is saved as you study.
Opening low-level design
Progress is saved as you study.
Opening your learning path
Progress is saved as you study.
Builder, Prototype & Object Pool is a depth lesson in the Strategy & Factory Patterns family of the Thita low level design course.
#1 and #4 most tested patterns: eliminate if/else chains with Strategy and create objects without specifying exact classes with Factory.
It sits on the Deep Dive path — worth reading once the Interview Soon lessons are solid, or when a role names this area explicitly.
Use these patterns when object creation creates design risk. In interviews, candidates often model behavior with Strategy or Factory, then lose points when construction is unreadable, cloning shares mutable state, or expensive resources are recreated without lifecycle control.
Builder, Prototype, and Object Pool are creational patterns. Builder controls how a complex object is assembled. Prototype creates objects from existing templates. Object Pool reuses expensive resources with capacity, cleanup, and timeout rules.
Builder fits when an object has many fields, optional parameters, validation rules, or construction steps that must happen in a readable order. A common interview failure is passing ten constructor arguments and hoping the interviewer ignores the brittle design.
Use Builder when constructors become unreadable or unsafe.
1Order order = new Order("u1", "INR", true, false, "COUPON10", null, 3);The code compiles. The call is hard to read without opening the constructor. Boolean parameters are especially risky because true, false does not explain intent.
Builder makes each value named and gives build() a single place for validation.
1Order order = Order.builder()
2 .userId("u1")
3 .currency("INR")
4 .applyDiscount(true)
5 .coupon("COUPON10")
6 .build();The object creation now explains each field.
Use Builder when you see:
A fluent builder returns this from every method, so calls can be chained. Use this style by default in interviews because it is compact and readable.
A step-by-step builder forces construction order through separate interfaces. Use it when order affects correctness, such as “payment method must be selected before confirmation.”
| Builder Style | Use When | Recommendation |
|---|---|---|
| Fluent builder | Most fields are optional or order-independent | Use by default |
| Step-by-step builder | Construction order must be enforced at compile time | Use for strict workflows |
| Director + Builder | The same construction recipe appears in multiple places | Add Director when it removes duplicated build steps |
A Director is a class that knows a standard construction sequence. It orchestrates the builder and does not store the final object.
Use a Director when the same build recipe appears in multiple places. Skip Director for a single builder().x().y().build() call because it adds ceremony without removing duplication.
Real-world example: a meal-ordering system may have VegMealDirector, KidsMealDirector, or PremiumMealDirector that apply standard item combinations.
@BuilderIn Java interviews, mention Lombok carefully. @Builder removes boilerplate. You still need to decide validation rules, defaults, and invariants.
1@Builder
2public class Order {
3 private final String userId;
4 private final String currency;
5 private final String coupon;
6}Use Lombok when the builder is mechanical. Write the builder manually when you need validation, derived fields, mandatory-step enforcement, or custom defaults.
1public class Main {
2 static final class Order {
3 private final String userId, currency, coupon;
4 private Order(Builder b) {
5 this.userId = b.userId; this.currency = b.currency; this.coupon = b.coupon;
6 }
7 public static Builder builder() { return new Builder(); }
8 @Override public String toString() { return userId + " " + currency + " " + coupon; }
9
10 interface OrderBuilder {
11 OrderBuilder userId(String id);
12 OrderBuilder currency(String code);
13 OrderBuilder coupon(String code);
14 Order build();
15 }
16
17 static final class Builder implements OrderBuilder {
18 private String userId, currency = "USD", coupon = "";
19 @Override public Builder userId(String id) { this.userId = id; return this; }
20 @Override public Builder currency(String code) { this.currency = code; return this; }
21 @Override public Builder coupon(String code) { this.coupon = code; return this; }
22 @Override public Order build() {
23 if (userId == null || userId.isBlank()) throw new IllegalStateException("userId required");
24 return new Order(this);
25 }
26 }
27 }
28
29 public static void main(String[] args) {
30 System.out.println(Order.builder().userId("u1").currency("INR").coupon("C10").build());
31 }
32}Prototype fits when creating an object from scratch is expensive, repetitive, or template-based. Clone an existing configured object and modify the fields that differ.
The interview trigger is usually wording like “many similar objects,” “preconfigured templates,” “expensive initialization,” or “copy an existing configuration.”
Factories create objects from inputs. Prototype creates objects from existing objects.
The distinction matters when setup loads metadata, parses config, builds nested defaults, or prepares a large template. Cloning can be simpler than reconstructing the same state.
Real-world examples:
A shallow copy copies object references. If the object contains a mutable list, both the original and clone may point to the same list.
A deep copy recursively copies nested mutable objects. It avoids shared mutable state and costs more for large object graphs.
| Copy Type | What Happens | Use When |
|---|---|---|
| Shallow copy | Nested objects are shared | Nested data is immutable or intentionally shared |
| Deep copy | Nested objects are copied | Mutable nested state must be independent |
| Custom copy | You choose what to share and copy | Large graphs need controlled copy cost |
The most common bug is cloning an object with a mutable collection and accidentally sharing that collection.
1Report cloned = original.clone();
2cloned.sections.add("New Section");If sections was shallow-copied, the original report now also has "New Section". In interviews, always state whether the clone is shallow or deep.
Skip Prototype when the object is cheap to create and has a clear constructor. Avoid it when clone semantics are unclear, because explicit construction is safer than unclear copying.
Use Factory or Builder if the object must be assembled from fresh request data every time.
1import java.util.*;
2
3public class Main {
4 interface Prototype<T> {
5 T copy();
6 }
7
8 static final class Report implements Prototype<Report>, Cloneable {
9 private final String title;
10 private final List<String> sections;
11
12 Report(String title, List<String> sections) {
13 this.title = title;
14 this.sections = new ArrayList<>(sections);
15 }
16
17 @Override public Report copy() {
18 return new Report(this.title, this.sections);
19 }
20
21 @Override protected Report clone() {
22 return copy();
23 }
24
25 public void addSection(String s) { sections.add(s); }
26 @Override public String toString() { return title + sections; }
27 }
28
29 public static void main(String[] args) {
30 Report template = new Report("Monthly", List.of("Summary"));
31 Report clone = template.clone();
32 clone.addSection("Revenue");
33 System.out.println(template);
34 System.out.println(clone);
35 }
36}Object Pool fits when object creation is expensive and the object can be safely reused after cleanup. In interviews, this pattern usually appears as “design a connection pool,” “reuse gRPC channels,” “thread pool,” or “limit concurrent access to expensive resources.”
A pool needs controlled lifecycle. It creates a bounded number of resources, hands them out safely, validates them on return, and blocks or times out when none are available.
Some objects are expensive because they hold external resources. Database connections, network channels, threads, and browser sessions hold OS or remote resources.
Creating them for every request causes latency, resource exhaustion, and unstable systems. Pooling spreads creation cost across many requests and enforces a concurrency limit.
Real-world examples:
A correct pool defines each stage.
A pool of two connections hands them out, makes a third caller wait with a timeout, then checks each returned object's health before reuse.
When Salesforce or similar companies ask for “a connection pool with an internal request queue,” they expect bounded blocking behavior. Candidates often create an unbounded List<Connection> and return null when empty. That design lacks capacity, blocking, and timeout behavior.
A good interview design has:
Do not pool normal objects like DTOs, commands, or small value objects. Modern runtimes allocate short-lived objects efficiently, and pooling them often makes code slower and more error-prone.
Do not use Object Pool if the resource cannot be safely reset. Reusing dirty state is riskier than paying construction cost.
1import java.util.concurrent.*;
2import java.util.function.Supplier;
3
4public class Main {
5 interface Pooled {
6 boolean isHealthy();
7 void close();
8 }
9
10 static final class Conn implements Pooled {
11 private boolean open = true;
12 @Override public boolean isHealthy() { return open; }
13 @Override public void close() { open = false; }
14 public String query() { return "ok"; }
15 }
16
17 static final class ObjectPool<T extends Pooled> {
18 private final BlockingQueue<T> queue;
19 private final Supplier<T> factory;
20
21 ObjectPool(int capacity, Supplier<T> factory) {
22 this.queue = new ArrayBlockingQueue<>(capacity);
23 this.factory = factory;
24 for (int i = 0; i < capacity; i++) queue.add(factory.get());
25 }
26
27 public T acquire(long timeoutMs) throws InterruptedException, TimeoutException {
28 T obj = queue.poll(timeoutMs, TimeUnit.MILLISECONDS);
29 if (obj == null) throw new TimeoutException("pool exhausted");
30 return obj;
31 }
32
33 public void release(T obj) {
34 if (obj == null) return;
35 if (!obj.isHealthy()) obj = factory.get();
36 if (!queue.offer(obj)) obj.close();
37 }
38 }
39
40 public static void main(String[] args) throws Exception {
41 ObjectPool<Conn> pool = new ObjectPool<>(2, Conn::new);
42 Conn c = pool.acquire(100);
43 System.out.println(c.query());
44 pool.release(c);
45 }
46}These patterns are easy to confuse because all three create or provide objects. Pick based on the problem in front of you.
| Pattern | Core Problem | Default Use |
|---|---|---|
| Builder | Object has complex construction | Build readable immutable objects |
| Prototype | Object should be copied from a template | Clone expensive or preconfigured objects |
| Object Pool | Object is expensive and reusable | Reuse bounded external resources |
Use Builder when the caller knows the values and construction is messy.
Use Prototype when an existing object is already close to what you need.
Use Object Pool when repeated creation and destruction is expensive and the object can be safely reused.
In interviews, pick Builder for complex immutable request, config, or domain objects. Pick Prototype when cloning from templates is cheaper or clearer than rebuilding. Pick Object Pool for expensive external resources like DB connections, threads, or network channels.
For a connection pool prompt, design a bounded pool with blocking acquire, timeout, health check on release, and replacement of unhealthy objects.
Warning:
build(), so the object can still be invalid.@Builder as if annotation usage decides the design. Lombok removes boilerplate. Invariants still need explicit design.Cloneable blindly. In interviews, wrap clone behavior behind a clear copy() method and state whether it is deep or shallow.null when the pool is empty. A production pool should block with timeout or fail explicitly.No related case studies for this subpattern.