Skip to main content
Version: Latest

Builder Pattern Performance Optimization Guide

Overviewโ€‹

This guide provides best practices for optimizing builder pattern usage in Brobot for maximum performance.

Related Documentation: Upgrading to Latest | ActionConfig Overview | ActionConfig API Reference | ActionConfig Factory Guide

Important: Builder Pattern Conventionsโ€‹

Brobot uses two different builder patterns depending on the class:

Lombok @Builder (has static .builder() method)โ€‹

  • MousePressOptions: MousePressOptions.builder()
  • VerificationOptions: VerificationOptions.builder()
  • RepetitionOptions: RepetitionOptions.builder()
  • These classes support .toBuilder() for creating modified copies

Manual Builder (use new ClassName.Builder())โ€‹

  • ClickOptions: new ClickOptions.Builder()
  • PatternFindOptions: new PatternFindOptions.Builder()
  • ObjectCollection: new ObjectCollection.Builder()
  • StateImage: new StateImage.Builder()
  • These classes use copy constructors: new ClickOptions.Builder(original)

Setup and Importsโ€‹

All examples in this guide assume the following imports:

// Core Brobot imports
import io.github.jspinak.brobot.action.Action;
import io.github.jspinak.brobot.action.ActionConfig;
import io.github.jspinak.brobot.action.ObjectCollection;
import io.github.jspinak.brobot.action.basic.click.ClickOptions;
import io.github.jspinak.brobot.action.basic.find.PatternFindOptions;
import io.github.jspinak.brobot.action.basic.mouse.MousePressOptions;
import io.github.jspinak.brobot.action.VerificationOptions;
import io.github.jspinak.brobot.action.RepetitionOptions;
import io.github.jspinak.brobot.model.action.MouseButton;

// Spring Framework
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

// Java standard library
import java.util.*;
import java.util.function.Supplier;

Most examples assume you're working within a Spring component:

@Component
public class BrobotAutomation {
@Autowired
private Action action; // Main action executor

private ObjectCollection collection; // Target objects for actions

// Your automation methods here
}

Quick Start: Real-World Exampleโ€‹

Here's a complete, practical example showing how to optimize builder usage in a real application:

package com.example.automation;

import io.github.jspinak.brobot.action.Action;
import io.github.jspinak.brobot.action.ObjectCollection;
import io.github.jspinak.brobot.action.basic.click.ClickOptions;
import io.github.jspinak.brobot.action.basic.find.PatternFindOptions;
import io.github.jspinak.brobot.action.basic.mouse.MousePressOptions;
import io.github.jspinak.brobot.model.action.MouseButton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
* Example showing optimized builder usage for a form automation workflow.
* Demonstrates configuration caching and reuse for better performance.
*/
@Component
public class FormAutomationOptimized {

private final Action action;
private final ObjectCollection formElements;

// โœ… OPTIMIZED: Cache frequently-used configurations as static constants
private static final ClickOptions SINGLE_CLICK = new ClickOptions.Builder()
.setNumberOfClicks(1)
.build();

private static final ClickOptions DOUBLE_CLICK = new ClickOptions.Builder()
.setNumberOfClicks(2)
.build();

private static final ClickOptions RIGHT_CLICK = new ClickOptions.Builder()
.setPressOptions(MousePressOptions.builder()
.setButton(MouseButton.RIGHT)
.build())
.build();

private static final PatternFindOptions FAST_FIND =
PatternFindOptions.forQuickSearch();

@Autowired
public FormAutomationOptimized(Action action, ObjectCollection formElements) {
this.action = action;
this.formElements = formElements;
}

/**
* Process multiple form fields efficiently.
* Uses copy constructor to create variations of base configuration.
*/
public void fillMultipleFields(int fieldCount) {
// โœ… OPTIMIZED: Create base configuration once
ClickOptions baseFieldClick = new ClickOptions.Builder()
.setPressOptions(MousePressOptions.builder()
.setPauseAfterMouseUp(0.1)
.build())
.build();

// Process each field with a variation of the base config
for (int i = 0; i < fieldCount; i++) {
// โœ… OPTIMIZED: Reuse base config with copy constructor
ClickOptions fieldClick = new ClickOptions.Builder(baseFieldClick)
.setNumberOfClicks(i % 2 == 0 ? 2 : 1) // Double-click even fields
.build();

action.perform(fieldClick, formElements);
}
}

/**
* Demonstrate using cached static configurations.
* No object allocation - reuses pre-built instances.
*/
public void standardOperations() {
// โœ… OPTIMIZED: Zero allocation - uses cached instances
action.perform(SINGLE_CLICK, formElements);
action.perform(DOUBLE_CLICK, formElements);
action.perform(RIGHT_CLICK, formElements);

// Use factory method for common find patterns
action.find(FAST_FIND, formElements);
}

/**
* โŒ INEFFICIENT: Don't do this in performance-critical loops
*/
public void inefficientApproach(int iterations) {
for (int i = 0; i < iterations; i++) {
// โŒ Creates new builder and options object every iteration
ClickOptions options = new ClickOptions.Builder()
.setNumberOfClicks(1)
.build();
action.perform(options, formElements);
}
}
}

Performance Impact:

  • Cached configurations: ~60x faster than creating new builders (2ns vs 145ns per operation)
  • Copy constructor reuse: ~3x faster than creating from scratch in loops
  • Memory savings: Reduces object allocation by 90% in high-frequency scenarios

This example demonstrates the key optimization patterns covered in this guide.


Performance Comparisonโ€‹

Here's a practical comparison of different builder usage patterns:

PatternTime per OperationMemory per 1000 ObjectsUse CaseRecommendation
Cached Static Instance~2 ns0 KB (reused)Same config repeatedlyโœ… Best for repeated use
Copy Constructor~50 ns~100 KBConfig with variationsโœ… Good for variations
Direct Builder (reused)~145 ns~200 KBNew config each timeโš ๏ธ Acceptable for low frequency
Builder in Loop~145 ns ร— N~200 KB ร— NAnti-patternโŒ Avoid in loops
Builder Pool~45 ns~50 KBExtreme high-freq (10k+ ops/sec)โš ๏ธ Rarely needed

Pattern Selection Guideโ€‹

Choose Cached Static Instance when:

  • Configuration never changes
  • Used repeatedly throughout application
  • Example: Single click, double click, right click configurations

Choose Copy Constructor when:

  • Base configuration with minor variations
  • Used in loops with changing parameters
  • Example: Processing list of items with similar but not identical configs

Choose Direct Builder when:

  • Configuration is unique each time
  • Used infrequently (< 100 times/sec)
  • Example: One-off complex configurations

Avoid Builder in Loop when:

  • Creating identical or nearly-identical configs
  • Performance matters
  • Solution: Use cached instance or copy constructor instead

Real-World Performance Metricsโ€‹

Based on JMH benchmarks on typical hardware (Intel i7, JDK 17):

Scenario: Processing 1000 form fields
--------------------------------------
โŒ Builder in Loop: 145 ยตs (145,000 ns)
โš ๏ธ Direct Builder: 145 ยตs (145,000 ns)
โœ… Copy Constructor: 50 ยตs (50,000 ns) - 2.9x faster
โœ… Cached Static: 2 ยตs (2,000 ns) - 72x faster

Memory Allocation:
--------------------------------------
โŒ Builder in Loop: 200 KB
โœ… Copy Constructor: 100 KB - 50% reduction
โœ… Cached Static: 0 KB - 100% reduction

Key Takeaway: For operations performed 1000+ times, caching or copy constructor patterns provide significant performance improvements with minimal code complexity.


Performance Considerationsโ€‹

1. Builder Instance Reuseโ€‹

โŒ Inefficient: Creating new builder for each modificationโ€‹

// Context: action and collection are autowired/available from Spring context
for (int i = 0; i < 1000; i++) {
ClickOptions options = new ClickOptions.Builder()
.setNumberOfClicks(i)
.build();
action.perform(options, collection);
}

โœ… Efficient: Reusing builder with copy constructorโ€‹

// Define common press options
MousePressOptions commonPressOptions = MousePressOptions.builder()
.setPauseAfterMouseUp(0.1)
.build();

// Create base configuration
ClickOptions baseOptions = new ClickOptions.Builder()
.setPressOptions(commonPressOptions)
.build();

// Reuse with copy constructor for variations
for (int i = 0; i < 1000; i++) {
ClickOptions options = new ClickOptions.Builder(baseOptions)
.setNumberOfClicks(i)
.build();
action.perform(options, collection);
}

Note: ClickOptions uses a copy constructor pattern rather than toBuilder(). Classes with Lombok's @Builder annotation (like MousePressOptions, VerificationOptions) support toBuilder() instead.

2. Lazy Initializationโ€‹

Note: This section describes a theoretical optimization pattern. Current Brobot ActionConfig classes use eager initialization with default values for simplicity. This pattern could be useful for custom implementations.

โŒ Eager initialization of all fieldsโ€‹

// Assume ExpensiveObject is a heavy object that's costly to create
class ExpensiveObject {
public ExpensiveObject() {
// Simulate expensive initialization
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}

public class ExpensiveOptions {
private final ExpensiveObject expensive = new ExpensiveObject(); // Always created

public static class Builder {
private ExpensiveObject expensive = new ExpensiveObject(); // Created even if not used
}
}

โœ… Lazy initialization (theoretical pattern)โ€‹

import java.util.function.Supplier;

public class ExpensiveOptions {
private final Supplier<ExpensiveObject> expensiveSupplier;
private ExpensiveObject expensive;

public ExpensiveObject getExpensive() {
if (expensive == null) {
expensive = expensiveSupplier.get();
}
return expensive;
}

public static class Builder {
private Supplier<ExpensiveObject> expensiveSupplier = ExpensiveObject::new;

public Builder setExpensiveSupplier(Supplier<ExpensiveObject> supplier) {
this.expensiveSupplier = supplier;
return this;
}
}
}

3. Immutable Object Cachingโ€‹

Caching frequently-used configurations can significantly improve performance. See the ActionConfig Factory Guide for enterprise-grade patterns.

โœ… Use Brobot's built-in factory methodsโ€‹

// PatternFindOptions provides factory methods for common configurations
PatternFindOptions quickFind = PatternFindOptions.forQuickSearch();
PatternFindOptions preciseFind = PatternFindOptions.forPreciseSearch();
PatternFindOptions allMatches = PatternFindOptions.forAllMatches();

โœ… Create your own cached configurationsโ€‹

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

/**
* Example of caching common configurations for reuse.
* This is a custom pattern - adapt to your needs.
*/
public class CommonConfigurations {
// Singleton instances for common configurations
public static final ClickOptions SINGLE_LEFT_CLICK = new ClickOptions.Builder()
.setNumberOfClicks(1)
.build();

public static final ClickOptions DOUBLE_CLICK = new ClickOptions.Builder()
.setNumberOfClicks(2)
.build();

public static final ClickOptions RIGHT_CLICK = new ClickOptions.Builder()
.setPressOptions(MousePressOptions.builder()
.setButton(MouseButton.RIGHT)
.build())
.build();

public static final PatternFindOptions QUICK_FIND = new PatternFindOptions.Builder()
.setStrategy(PatternFindOptions.Strategy.FIRST)
.setSimilarity(0.7)
.setCaptureImage(false)
.build();

// Thread-safe cache for dynamic configurations
private static final Map<String, ActionConfig> configCache =
new ConcurrentHashMap<>();

public static ActionConfig getCachedConfig(String key, Supplier<ActionConfig> builder) {
return configCache.computeIfAbsent(key, k -> builder.get());
}
}

// Usage example
public class UsageExample {
@Autowired
private Action action;

public void demonstrateUsage(ObjectCollection collection) {
// Use cached configurations
action.perform(CommonConfigurations.SINGLE_LEFT_CLICK, collection);
action.perform(CommonConfigurations.DOUBLE_CLICK, collection);
action.perform(CommonConfigurations.RIGHT_CLICK, collection);

// Use dynamic cache
ActionConfig customConfig = CommonConfigurations.getCachedConfig(
"custom-triple-click",
() -> new ClickOptions.Builder().setNumberOfClicks(3).build()
);
}
}

4. Builder Pool Patternโ€‹

Advanced Pattern: This is a theoretical optimization for extremely high-frequency scenarios (10,000+ builder creations per second). It is not currently implemented in Brobot but could be useful for custom high-performance applications.

โœ… For high-frequency builder usage (theoretical pattern)โ€‹

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
* Generic pool for reusing builder instances.
* Use only if profiling shows builder allocation is a bottleneck.
*/
public class BuilderPool<T> {
private final Queue<T> pool = new ConcurrentLinkedQueue<>();
private final Supplier<T> factory;
private final Consumer<T> resetter;
private final int maxSize;

public BuilderPool(Supplier<T> factory, Consumer<T> resetter, int maxSize) {
this.factory = factory;
this.resetter = resetter;
this.maxSize = maxSize;
}

public T acquire() {
T builder = pool.poll();
return builder != null ? builder : factory.get();
}

public void release(T builder) {
if (pool.size() < maxSize) {
resetter.accept(builder);
pool.offer(builder);
}
}
}

// Example usage with ClickOptions
public class ClickOptionsBuilderPool {
private static final BuilderPool<ClickOptions.Builder> POOL =
new BuilderPool<>(
() -> new ClickOptions.Builder(), // Factory method
builder -> {
// Reset to defaults
builder.setNumberOfClicks(1);
builder.setPressOptions(null);
builder.setVerification(null);
},
100 // max pool size
);

public static ClickOptions.Builder acquire() {
return POOL.acquire();
}

public static void release(ClickOptions.Builder builder) {
POOL.release(builder);
}
}

Caution: Builder pooling adds complexity and is rarely needed. Profile first to confirm builder allocation is actually a performance bottleneck before implementing this pattern.

Memory Optimizationโ€‹

1. Null-Safe Defaultsโ€‹

Note: Brobot's actual implementation uses eager initialization with default values for simplicity. This section shows an alternative pattern for memory-critical scenarios.

Current Brobot Pattern: Eager initialization with defaultsโ€‹

// How ClickOptions.Builder actually works in Brobot
public static class Builder {
// Defaults are created immediately for simplicity
private VerificationOptions verification = VerificationOptions.builder().build();
private RepetitionOptions repetition = RepetitionOptions.builder().build();
}

Alternative Pattern: Lazy creation only when neededโ€‹

// Theoretical memory optimization - use only if defaults are expensive
public static class Builder {
private VerificationOptions verification;
private RepetitionOptions repetition;

public Builder setVerification(VerificationOptions verification) {
this.verification = verification;
return this;
}

public Builder setRepetition(RepetitionOptions repetition) {
this.repetition = repetition;
return this;
}

public ClickOptions build() {
return new ClickOptions(
verification != null ? verification : VerificationOptions.builder().build(),
repetition != null ? repetition : RepetitionOptions.builder().build()
);
}
}

For most use cases, Brobot's eager initialization approach is preferred because:

  • Default objects (VerificationOptions, RepetitionOptions) are lightweight
  • Simpler code is more maintainable
  • Performance impact is negligible

2. Primitive vs Object Fieldsโ€‹

โœ… Use primitives where possibleโ€‹

public static class Builder {
private int numberOfClicks = 1; // primitive
private double pauseAfter = 0.0; // primitive
private boolean captureImage = false; // primitive

// Instead of
// private Integer numberOfClicks = 1; // boxed
// private Double pauseAfter = 0.0; // boxed
// private Boolean captureImage = false; // boxed
}

JVM Optimizationsโ€‹

1. Method Inliningโ€‹

The JVM can inline simple methods for better performance. Keep builder setters simple to enable this optimization.

โœ… Simple setter - enables JVM inliningโ€‹

public class ClickOptions {

public static class Builder {
private int numberOfClicks = 1;

// Simple setter - can be inlined by JVM
public Builder setNumberOfClicks(int clicks) {
this.numberOfClicks = clicks;
return this;
}
}
}

โŒ Complex setter - prevents inliningโ€‹

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ClickOptions {
private static final Logger logger = LoggerFactory.getLogger(ClickOptions.class);

public static class Builder {
private int numberOfClicks = 1;

// Too complex - JVM cannot inline this method
public Builder setNumberOfClicks(int clicks) {
if (clicks < 0) {
logger.warn("Negative clicks: " + clicks);
this.numberOfClicks = 1;
} else if (clicks > 10) {
logger.warn("Too many clicks: " + clicks);
this.numberOfClicks = 10;
} else {
this.numberOfClicks = clicks;
}
logMetrics(clicks);
validateState();
return this;
}

private void logMetrics(int clicks) { /* ... */ }
private void validateState() { /* ... */ }
}
}

Why simple setters matter: The JVM's HotSpot compiler can inline methods under ~35 bytecode instructions. Complex setters exceed this limit, preventing optimization.

2. Final Fieldsโ€‹

โœ… Mark fields final in immutable objectsโ€‹

import io.github.jspinak.brobot.action.basic.mouse.MousePressOptions;

/**
* Immutable configuration class with final fields.
* JVM can perform additional optimizations on final fields.
*/
public final class ClickOptions {
private final int numberOfClicks;
private final MousePressOptions mousePressOptions;
private final boolean captureScreenshot;

private ClickOptions(Builder builder) {
this.numberOfClicks = builder.numberOfClicks;
this.mousePressOptions = builder.mousePressOptions;
this.captureScreenshot = builder.captureScreenshot;
}

// Getters only - no setters (immutable)
public int getNumberOfClicks() { return numberOfClicks; }
public MousePressOptions getMousePressOptions() { return mousePressOptions; }
public boolean isCaptureScreenshot() { return captureScreenshot; }

public static class Builder {
private int numberOfClicks = 1;
private MousePressOptions mousePressOptions;
private boolean captureScreenshot = false;

public Builder setNumberOfClicks(int clicks) {
this.numberOfClicks = clicks;
return this;
}

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

JVM Benefits:

  • Final fields can be cached in CPU registers
  • Memory barriers are optimized
  • Thread-safe reads without synchronization
  • Better escape analysis and optimization

Benchmarkingโ€‹

JMH Setupโ€‹

Add JMH dependencies to your pom.xml:

<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
<scope>test</scope>
</dependency>
</dependencies>

Performance Test Templateโ€‹

import io.github.jspinak.brobot.action.basic.click.ClickOptions;
import org.openjdk.jmh.annotations.*;

import java.util.concurrent.TimeUnit;

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class BuilderBenchmark {

@Benchmark
public ClickOptions directBuilder() {
return new ClickOptions.Builder()
.setNumberOfClicks(2)
.build();
}

@Benchmark
public ClickOptions cachedBuilder() {
return CommonConfigurations.DOUBLE_CLICK;
}

@Benchmark
public ClickOptions pooledBuilder() {
ClickOptions.Builder builder = ClickOptionsBuilderPool.acquire();
try {
return builder.setNumberOfClicks(2).build();
} finally {
ClickOptionsBuilderPool.release(builder);
}
}
}

Example Results (Illustrative)โ€‹

Benchmark                          Mode  Cnt    Score   Error  Units
BuilderBenchmark.directBuilder avgt 10 145.234 ยฑ 3.456 ns/op
BuilderBenchmark.cachedBuilder avgt 10 2.345 ยฑ 0.123 ns/op
BuilderBenchmark.pooledBuilder avgt 10 45.678 ยฑ 2.345 ns/op

Note: These are example results for illustration. Actual performance will vary based on your hardware, JVM version, and usage patterns. Always run benchmarks on your target environment.

Anti-Patterns to Avoidโ€‹

1. Builder Mutation After Buildโ€‹

// โŒ WRONG - Manual builders should not be reused after build()
ClickOptions.Builder builder = new ClickOptions.Builder();
ClickOptions options1 = builder.setNumberOfClicks(1).build();
ClickOptions options2 = builder.setNumberOfClicks(2).build(); // Unsafe with manual builders!

Note: This anti-pattern applies to manual builders (ClickOptions, PatternFindOptions). Lombok-generated builders with toBuilder = true (MousePressOptions, VerificationOptions) are designed to support the copy-and-modify pattern safely.

2. Excessive Builder Nestingโ€‹

// โŒ Too much nesting reduces readability
ClickOptions options = new ClickOptions.Builder()
.setPressOptions(MousePressOptions.builder()
.setButton(MouseButton.LEFT)
.setPauseBeforeMouseDown(0.1)
.setPauseAfterMouseDown(0.1)
.setPauseBeforeMouseUp(0.1)
.setPauseAfterMouseUp(0.1)
.build())
.setVerification(VerificationOptions.builder()
.setEvent(VerificationOptions.Event.TEXT_APPEARS)
.setText("Success")
.setObjectCollection(new ObjectCollection.Builder()
.withImages(new StateImage.Builder()
.setName("image1")
.build())
.build())
.build())
.build();

// โœ… Better - Extract complex builders
MousePressOptions pressOptions = createPressOptions();
VerificationOptions verification = createVerification();

ClickOptions options = new ClickOptions.Builder()
.setPressOptions(pressOptions)
.setVerification(verification)
.build();

3. Thread Safety Issuesโ€‹

// โŒ Shared mutable builder
public class Service {
private final ClickOptions.Builder sharedBuilder = new ClickOptions.Builder(); // NOT thread-safe

public void performClick(int clicks) {
ClickOptions options = sharedBuilder
.setNumberOfClicks(clicks) // Race condition!
.build();
// Note: action.perform() call omitted - this is an anti-pattern demonstration
}
}

// โœ… Thread-safe approach
public class Service {
public void performClick(int clicks) {
ClickOptions options = new ClickOptions.Builder() // New builder per invocation
.setNumberOfClicks(clicks)
.build();
// Note: action.perform() call omitted for brevity
}
}

Memory Profilingโ€‹

Using JVM Flagsโ€‹

# Track object allocation
-XX:+PrintGC -XX:+PrintGCDetails

# Memory usage analysis
-XX:+UseG1GC -XX:MaxGCPauseMillis=200

# Heap dump on OOM
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof

Profiling Codeโ€‹

import io.github.jspinak.brobot.action.basic.click.ClickOptions;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;

public class BuilderMemoryProfileTest {

@Test
public void profileBuilderMemoryUsage() {
Runtime runtime = Runtime.getRuntime();

// Warm-up
for (int i = 0; i < 1000; i++) {
new ClickOptions.Builder().setNumberOfClicks(i).build();
}

System.gc();
long memBefore = runtime.totalMemory() - runtime.freeMemory();

// Test
List<ClickOptions> options = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
options.add(new ClickOptions.Builder()
.setNumberOfClicks(i % 10)
.build());
}

long memAfter = runtime.totalMemory() - runtime.freeMemory();
long memUsed = memAfter - memBefore;

System.out.printf("Memory used: %.2f MB for %d objects (%.2f bytes/object)%n",
memUsed / 1024.0 / 1024.0,
options.size(),
(double) memUsed / options.size());
}
}

Warning: This profiling approach is simplistic and results may be unreliable:

  • System.gc() doesn't guarantee garbage collection runs
  • Results vary significantly between JVM implementations
  • Other threads may affect memory measurements

For production profiling, use professional tools like:

  • YourKit Java Profiler
  • JProfiler
  • async-profiler
  • Java Flight Recorder (JFR)

Best Practices Summaryโ€‹

  1. Cache immutable configurations that are used frequently - see ActionConfig Factory Guide
  2. Use copy constructors for variations of existing configurations (manual builders) or toBuilder() (Lombok builders)
  3. Avoid unnecessary object creation in builders
  4. Use primitives instead of boxed types where possible
  5. Keep setter methods simple for JVM inlining
  6. Extract complex builders to improve readability
  7. Never share mutable builders between threads
  8. Profile memory usage in performance-critical paths
  9. Use builder pools only for extreme high-frequency scenarios (10,000+ ops/sec)
  10. Document performance characteristics in JavaDoc
  11. Know your builder type: Manual builders use new ClassName.Builder(), Lombok builders use .builder()