Skip to content
SP StackPractices
beginner By Mathias Paulenko

Builder Pattern for Complex Configuration Objects

Use the Builder pattern to construct complex configuration objects with optional parameters and sensible defaults without telescoping constructors

Topics: design

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Builder Pattern for Complex Configuration Objects

The Builder pattern separates the construction of a complex object from its representation. Instead of passing eight constructor arguments or creating an empty object and setting fields individually, the builder provides a readable, step-by-step API with defaults and validation.

When to Use This

  • An object has many optional parameters and sensible defaults
  • You want to prevent objects from being created in an invalid state
  • Constructor telescoping becomes unreadable with more than three optional arguments

Problem

Constructing a database connection config with optional pooling, SSL, and retry settings leads to either 12-argument constructors or partially-initialized mutable objects.

Solution

// config/DatabaseConfig.ts
interface DatabaseConfig {
  host: string;
  port: number;
  username: string;
  password: string;
  database: string;
  ssl?: boolean;
  poolSize?: number;
  maxRetries?: number;
  connectionTimeout?: number;
}

class DatabaseConfigBuilder {
  private config: Partial<DatabaseConfig> = {
    port: 5432,
    ssl: false,
    poolSize: 10,
    maxRetries: 3,
    connectionTimeout: 5000,
  };

  setHost(host: string): this {
    this.config.host = host;
    return this;
  }

  setPort(port: number): this {
    this.config.port = port;
    return this;
  }

  setCredentials(username: string, password: string): this {
    this.config.username = username;
    this.config.password = password;
    return this;
  }

  setDatabase(name: string): this {
    this.config.database = name;
    return this;
  }

  enableSSL(): this {
    this.config.ssl = true;
    return this;
  }

  setPoolSize(size: number): this {
    this.config.poolSize = size;
    return this;
  }

  setMaxRetries(retries: number): this {
    this.config.maxRetries = retries;
    return this;
  }

  build(): DatabaseConfig {
    if (!this.config.host || !this.config.username || !this.config.database) {
      throw new Error('Host, username, and database are required');
    }
    return this.config as DatabaseConfig;
  }
}

Usage

const config = new DatabaseConfigBuilder()
  .setHost('db.example.com')
  .setCredentials('app_user', process.env.DB_PASSWORD!)
  .setDatabase('analytics')
  .enableSSL()
  .setPoolSize(20)
  .build();

Variations

  • Immutable Builder: Return a new builder on each step instead of mutating state
  • Director: Encapsulate common configurations behind a director class
  • Step Builder: Enforce build order through separate interfaces for each step

Best Practices

  • Validate only at build() time, not on every setter
  • Return this for method chaining (fluent interface)
  • Freeze or seal the returned object to prevent post-construction mutation

Common Mistakes

  • Adding business logic to the builder instead of keeping it as pure construction
  • Forgetting to reset internal state when a builder is reused
  • Returning partially built objects without validation

FAQ

Q: When should I prefer a builder over an object literal? A: When validation is needed, defaults are complex, or the same construction logic is reused across multiple call sites.

Q: Is the Builder pattern still relevant with object spread syntax? A: Yes. Spreads are convenient for simple cases but do not enforce validation, defaults, or construction order.