DEV Community

Cover image for The Builder Pattern: Stop Writing 10-Parameter Constructors
Anas Hussain
Anas Hussain

Posted on

The Builder Pattern: Stop Writing 10-Parameter Constructors

1. Hook & Problem Statement

You're building a Laravel application. You need to send a report email with attachments, custom headers, CC and BCC recipients, and a custom template.

Your constructor looks like this:

$email = new ReportEmail(
    $recipient,
    $subject,
    $body,
    $attachments,
    $ccRecipients,
    $bccRecipients,
    $headers,
    $template,
    $priority,
    $replyTo,
    $fromAddress,
    $fromName,
    $scheduledAt
);
Enter fullscreen mode Exit fullscreen mode

12 parameters! You can't remember the order. You constantly pass null for optional parameters. When you add a new feature, you have to update every place that creates this object.

This is Constructor Hell. It's a symptom of a deeper problem: objects that have too many configuration options.

Enter the Builder Pattern.

The Builder Pattern is designed to handle the construction of complex objects step by step. It allows you to create objects with many optional parameters without sacrificing readability or maintainability.


2. Why This Pattern Exists

The Software Engineering Problem It Solves

The Builder Pattern solves the problem of constructing complex objects with many optional parameters. It separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

The Pain That Existed Before

Before the Builder Pattern (or in codebases that don't use it), developers faced:

  1. Telescoping Constructor Anti-Pattern: Constructors with 10+ parameters that are impossible to remember.

  2. Optional Parameters Overload: Passing null for every optional parameter.

  3. Invalid State: Objects could be created in an invalid state because parameters were missing.

  4. Immutability Challenges: Complex objects that needed to be immutable but had many configuration options.

  5. Duplicate Code: The same complex object creation code repeated across multiple places.

Why Large Applications Need It

As applications grow, objects become more complex:

  • Domain Objects: Entities with many attributes and behaviors.
  • DTOs: Data Transfer Objects with many fields.
  • Configuration Objects: Objects that hold many configuration options.
  • Request Objects: Objects that encapsulate complex request data.

The Builder Pattern provides:

  • Readability: Method chaining makes the code self-documenting.
  • Flexibility: You can add new configuration options without breaking existing code.
  • Immutability: Builders can create immutable objects.
  • Validation: Builders can validate the object state before building.

3. Real World Analogy

Ordering a Custom Pizza

You walk into a pizzeria. You want a custom pizza.

The Bad Way (Telescoping Constructor):
You shout your order in one breath:
"Large, thin crust, tomato sauce, mozzarella, pepperoni, mushrooms, olives, extra cheese, well-done, and can I have it delivered to my friend's house?"

The pizza maker is confused. They forget your order. You realize you don't even remember all the options.

The Builder Way:
The pizzeria has a Pizza Builder:

  • Step 1: Choose the size (Large).
  • Step 2: Choose the crust (Thin).
  • Step 3: Add sauce (Tomato).
  • Step 4: Add cheese (Mozzarella, Extra cheese).
  • Step 5: Add toppings (Pepperoni, Mushrooms, Olives).
  • Step 6: Choose cooking instructions (Well-done).
  • Step 7: Choose delivery address (Friend's house).

Each step is clear and focused. You can skip optional steps. The builder ensures you end up with a valid pizza.

Analogy Mapping:

  • Pizza: The complex object being built.
  • Pizza Builder: The Builder object.
  • Size, Crust, Sauce: Required configuration.
  • Toppings: Optional configuration.
  • Cooking Instructions: Optional configuration.
  • Delivery Address: Optional configuration.
  • Final Build(): The method that creates the pizza.

4. The Pain (Bad Design)

Let's look at a typical "Constructor Hell" scenario.

namespace App\Services\Email;

class ReportEmail
{
    private string $to;
    private string $subject;
    private string $body;
    private array $attachments;
    private array $cc;
    private array $bcc;
    private array $headers;
    private string $template;
    private string $priority;
    private string $replyTo;
    private string $fromAddress;
    private string $fromName;
    private ?DateTime $scheduledAt;

    public function __construct(
        string $to,
        string $subject,
        string $body,
        array $attachments = [],
        array $cc = [],
        array $bcc = [],
        array $headers = [],
        string $template = 'default',
        string $priority = 'normal',
        string $replyTo = '',
        string $fromAddress = '',
        string $fromName = '',
        ?DateTime $scheduledAt = null
    ) {
        $this->to = $to;
        $this->subject = $subject;
        $this->body = $body;
        $this->attachments = $attachments;
        $this->cc = $cc;
        $this->bcc = $bcc;
        $this->headers = $headers;
        $this->template = $template;
        $this->priority = $priority;
        $this->replyTo = $replyTo;
        $this->fromAddress = $fromAddress;
        $this->fromName = $fromName;
        $this->scheduledAt = $scheduledAt;
    }

    // Getters and methods...
}

// Usage: Constructor Hell
class ReportService
{
    public function sendReport(Report $report, User $user): void
    {
        $email = new ReportEmail(
            $user->email,
            "Monthly Report - {$report->month}",
            $this->generateReportContent($report),
            [
                ['path' => storage_path("reports/{$report->id}.pdf"), 'name' => 'report.pdf'],
                ['path' => storage_path("reports/{$report->id}.xlsx"), 'name' => 'data.xlsx'],
            ],
            ['manager@company.com', 'team@company.com'],
            [],
            ['X-Report-ID' => $report->id, 'X-Priority' => 'high'],
            'report_template',
            'high',
            'no-reply@company.com',
            config('mail.from.address'),
            config('mail.from.name'),
            null
        );

        Mail::send($email);
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Terrible

  1. Telescoping Constructor: 13 parameters! You can't remember the order.

  2. Passing null and Empty Arrays: Many parameters are optional but still need to be passed.

  3. No Type Safety: You can't enforce which parameters are required vs optional.

  4. Difficult to Read: The constructor call is a wall of text.

  5. Difficult to Maintain: Adding a new parameter requires updating every usage.

  6. No Validation: The object might be in an invalid state.

  7. Duplicate Code: The same complex creation logic is repeated.

Why Developers Write Code Like This

  • They start with a simple constructor.
  • Features are added over time.
  • They don't recognize the Builder Pattern opportunity.
  • They think "just one more parameter won't hurt."
  • They're not designing for maintainability.

5. Solution Overview

The Builder Pattern is a creational design pattern that separates the construction of a complex object from its representation. It allows the same construction process to create different representations.

Core Idea

Instead of using a telescoping constructor, you use a builder to construct the object step by step. The builder provides methods for setting each parameter, and a build() method that creates the final object.

Main Participants

  1. Product: The complex object being built (e.g., ReportEmail).

  2. Builder Interface: Defines the steps for building the product.

  3. Concrete Builder: Implements the builder interface with specific methods.

  4. Director (Optional): Orchestrates the building process for common configurations.

  5. Client: Creates the builder, sets parameters, and calls build().

How Objects Collaborate

Client → Builder → setParameterA() → setParameterB() → build() → Product
Enter fullscreen mode Exit fullscreen mode

The client creates a builder, sets parameters using fluent methods, and then calls build() to get the product.

Mental Model

Think of a recipe in a cookbook.

  • Builder: The cookbook with step-by-step instructions.
  • Product: The finished dish.
  • Methods: Each step in the recipe (add flour, add eggs, bake).
  • Client: The cook following the recipe.

You don't need to know how the dish is made. You just follow the steps. The builder handles the details.

Benefits

  • Readability: Method chaining makes the code self-documenting.
  • Flexibility: You can add new parameters without breaking existing code.
  • Validation: The builder can validate the object state before building.
  • Immutability: The builder can create immutable objects.
  • Reusability: The same builder can be used to create different configurations.

Trade-offs

  • More Classes: You need a builder class and a product class.
  • Complexity: More moving parts.
  • Boilerplate: You need to write builder methods for each parameter.

6. UML Diagram

Laravel Builder Pattern Mermaid Diagram

Laravel Builder Pattern Mermaid Diagram

Diagram Explanation

  1. ReportEmail is the product (complex object).
  2. ReportEmailBuilder handles the step-by-step construction.
  3. The builder has fluent methods for setting parameters.
  4. The build() method creates the product.
  5. The validate() method ensures the object is in a valid state.

7. Vanilla PHP Example

Let's refactor the report email using the Builder Pattern.

Before Refactoring (Constructor Hell)

(The terrible code shown above)

After Refactoring

Step 1: Define the Product (Simplified)
class ReportEmail
{
    private string $to;
    private string $subject;
    private string $body;
    private array $attachments;
    private array $cc;
    private array $bcc;
    private array $headers;
    private string $template;
    private string $priority;
    private string $replyTo;
    private string $fromAddress;
    private string $fromName;
    private ?DateTime $scheduledAt;

    // Private constructor - only the builder can create instances
    private function __construct(ReportEmailBuilder $builder)
    {
        $this->to = $builder->getTo();
        $this->subject = $builder->getSubject();
        $this->body = $builder->getBody();
        $this->attachments = $builder->getAttachments();
        $this->cc = $builder->getCc();
        $this->bcc = $builder->getBcc();
        $this->headers = $builder->getHeaders();
        $this->template = $builder->getTemplate();
        $this->priority = $builder->getPriority();
        $this->replyTo = $builder->getReplyTo();
        $this->fromAddress = $builder->getFromAddress();
        $this->fromName = $builder->getFromName();
        $this->scheduledAt = $builder->getScheduledAt();
    }

    // Factory method to create the builder
    public static function builder(): ReportEmailBuilder
    {
        return new ReportEmailBuilder();
    }

    public function send(): void
    {
        // Send the email
        Mail::send($this->template, ['body' => $this->body], function ($message) {
            $message->to($this->to)
                    ->subject($this->subject)
                    ->from($this->fromAddress, $this->fromName);

            if ($this->replyTo) {
                $message->replyTo($this->replyTo);
            }

            foreach ($this->cc as $cc) {
                $message->cc($cc);
            }

            foreach ($this->bcc as $bcc) {
                $message->bcc($bcc);
            }

            foreach ($this->attachments as $attachment) {
                $message->attach($attachment['path'], ['as' => $attachment['name']]);
            }

            foreach ($this->headers as $key => $value) {
                $message->setHeader($key, $value);
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 2: Define the Builder
class ReportEmailBuilder
{
    private string $to;
    private string $subject;
    private string $body;
    private array $attachments = [];
    private array $cc = [];
    private array $bcc = [];
    private array $headers = [];
    private string $template = 'default';
    private string $priority = 'normal';
    private string $replyTo = '';
    private string $fromAddress;
    private string $fromName;
    private ?DateTime $scheduledAt = null;

    public function __construct()
    {
        $this->fromAddress = config('mail.from.address');
        $this->fromName = config('mail.from.name');
    }

    // Required parameters
    public function to(string $to): self
    {
        $this->to = $to;
        return $this;
    }

    public function subject(string $subject): self
    {
        $this->subject = $subject;
        return $this;
    }

    public function body(string $body): self
    {
        $this->body = $body;
        return $this;
    }

    // Optional parameters
    public function attachment(string $path, string $name = null): self
    {
        $this->attachments[] = [
            'path' => $path,
            'name' => $name ?? basename($path),
        ];
        return $this;
    }

    public function attachments(array $attachments): self
    {
        foreach ($attachments as $attachment) {
            if (is_string($attachment)) {
                $this->attachment($attachment);
            } else {
                $this->attachment($attachment['path'], $attachment['name'] ?? null);
            }
        }
        return $this;
    }

    public function cc(string|array $cc): self
    {
        $this->cc = array_merge($this->cc, (array) $cc);
        return $this;
    }

    public function bcc(string|array $bcc): self
    {
        $this->bcc = array_merge($this->bcc, (array) $bcc);
        return $this;
    }

    public function header(string $key, string $value): self
    {
        $this->headers[$key] = $value;
        return $this;
    }

    public function headers(array $headers): self
    {
        $this->headers = array_merge($this->headers, $headers);
        return $this;
    }

    public function template(string $template): self
    {
        $this->template = $template;
        return $this;
    }

    public function priority(string $priority): self
    {
        $this->priority = $priority;
        return $this;
    }

    public function replyTo(string $replyTo): self
    {
        $this->replyTo = $replyTo;
        return $this;
    }

    public function from(string $address, string $name = ''): self
    {
        $this->fromAddress = $address;
        $this->fromName = $name;
        return $this;
    }

    public function scheduledAt(DateTime $dateTime): self
    {
        $this->scheduledAt = $dateTime;
        return $this;
    }

    // Build method
    public function build(): ReportEmail
    {
        $this->validate();
        return new ReportEmail($this);
    }

    // Validation
    private function validate(): void
    {
        if (empty($this->to)) {
            throw new \InvalidArgumentException('Recipient email is required');
        }

        if (empty($this->subject)) {
            throw new \InvalidArgumentException('Subject is required');
        }

        if (empty($this->body)) {
            throw new \InvalidArgumentException('Body is required');
        }

        if (!filter_var($this->to, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('Invalid recipient email');
        }

        if ($this->fromAddress && !filter_var($this->fromAddress, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('Invalid from address');
        }
    }

    // Getters for the product
    public function getTo(): string { return $this->to; }
    public function getSubject(): string { return $this->subject; }
    public function getBody(): string { return $this->body; }
    public function getAttachments(): array { return $this->attachments; }
    public function getCc(): array { return $this->cc; }
    public function getBcc(): array { return $this->bcc; }
    public function getHeaders(): array { return $this->headers; }
    public function getTemplate(): string { return $this->template; }
    public function getPriority(): string { return $this->priority; }
    public function getReplyTo(): string { return $this->replyTo; }
    public function getFromAddress(): string { return $this->fromAddress; }
    public function getFromName(): string { return $this->fromName; }
    public function getScheduledAt(): ?DateTime { return $this->scheduledAt; }
}
Enter fullscreen mode Exit fullscreen mode
Step 3: Usage
class ReportService
{
    public function sendReport(Report $report, User $user): void
    {
        $email = ReportEmail::builder()
            ->to($user->email)
            ->subject("Monthly Report - {$report->month}")
            ->body($this->generateReportContent($report))
            ->attachment(storage_path("reports/{$report->id}.pdf"), 'report.pdf')
            ->attachment(storage_path("reports/{$report->id}.xlsx"), 'data.xlsx')
            ->cc(['manager@company.com', 'team@company.com'])
            ->header('X-Report-ID', $report->id)
            ->header('X-Priority', 'high')
            ->template('report_template')
            ->priority('high')
            ->replyTo('no-reply@company.com')
            ->from(config('mail.from.address'), config('mail.from.name'))
            ->build();

        $email->send();
    }
}
Enter fullscreen mode Exit fullscreen mode

What We Improved

  1. Readability: The builder chain reads like a sentence.
  2. Flexibility: You can skip optional parameters.
  3. Validation: The builder validates the state before building.
  4. Maintainability: Adding a new parameter only requires adding a builder method.
  5. Immutability: The product is immutable (private constructor).
  6. Self-Documenting: The builder methods describe what they do.

8. Laravel Internal Example

Laravel uses the Builder Pattern extensively. Let's look at some key examples.

Query Builder

Laravel's Query Builder is a perfect example of the Builder Pattern.

// Laravel's Query Builder
$users = DB::table('users')
    ->select('name', 'email')
    ->where('active', true)
    ->where('age', '>', 18)
    ->orderBy('name')
    ->limit(10)
    ->get();
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • Fluent Interface: Method chaining makes the query read like English.
  • Step-by-Step Construction: Each method adds to the query.
  • Flexibility: You can add conditions conditionally.
  • Immutability: Each method returns a new query instance.

Eloquent Query Builder

Eloquent also uses the Builder Pattern.

// Eloquent Query Builder
$users = User::where('active', true)
    ->whereHas('posts', function ($query) {
        $query->where('published', true);
    })
    ->with('profile')
    ->orderBy('created_at', 'desc')
    ->paginate(15);
Enter fullscreen mode Exit fullscreen mode

HTTP Request Builder

Laravel's HTTP client uses the Builder Pattern.

// HTTP Client Builder
$response = Http::withHeaders([
    'X-Custom-Header' => 'value',
    'Authorization' => 'Bearer token',
])
->withOptions([
    'timeout' => 30,
    'verify' => false,
])
->withCookies(['session' => 'value'], 'example.com')
->asJson()
->post('https://api.example.com/data', [
    'name' => 'John Doe',
    'email' => 'john@example.com',
]);
Enter fullscreen mode Exit fullscreen mode

Mail Builder

Laravel's Mail system uses a builder-like approach.

// Mail Builder
Mail::to($user)
    ->cc($manager)
    ->bcc($team)
    ->subject('Welcome!')
    ->view('emails.welcome')
    ->attach(storage_path('files/welcome.pdf'))
    ->send();
Enter fullscreen mode Exit fullscreen mode

Validator Builder

Laravel's validation uses a builder for rules.

// Validator Builder
$validator = Validator::make($data, [
    'name' => 'required|string|max:255',
    'email' => 'required|email|unique:users',
    'age' => 'integer|min:18',
]);
Enter fullscreen mode Exit fullscreen mode

Database Schema Builder

Laravel's Schema Builder is another great example.

// Schema Builder
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->rememberToken();
    $table->timestamps();
});
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • Fluent Interface: Each method adds to the schema.
  • Self-Documenting: The method names describe the column types.
  • Flexibility: You can add columns conditionally.
  • Validation: The builder ensures the schema is valid.

9. Real Laravel Application Example

Let's build a Report Builder for generating complex business reports with multiple sections, filters, formats, and delivery options.

Scenario

Your application needs to generate reports with:

  • Multiple data sources
  • Filters (date range, categories, statuses)
  • Formatting options (chart type, column visibility, sorting)
  • Delivery options (email, download, webhook)
  • Scheduling options (immediate, scheduled, recurring)

Implementation

Step 1: Define the Product
// app/DTO/Report.php
namespace App\DTO;

class Report
{
    private string $title;
    private array $sections;
    private array $filters;
    private array $formatting;
    private array $dataSources;
    private array $deliveryOptions;
    private ?string $schedule = null;
    private ?string $timezone = null;
    private ?string $emailSubject = null;
    private ?string $emailBody = null;
    private array $recipients = [];

    // Private constructor
    private function __construct(ReportBuilder $builder)
    {
        $this->title = $builder->getTitle();
        $this->sections = $builder->getSections();
        $this->filters = $builder->getFilters();
        $this->formatting = $builder->getFormatting();
        $this->dataSources = $builder->getDataSources();
        $this->deliveryOptions = $builder->getDeliveryOptions();
        $this->schedule = $builder->getSchedule();
        $this->timezone = $builder->getTimezone();
        $this->emailSubject = $builder->getEmailSubject();
        $this->emailBody = $builder->getEmailBody();
        $this->recipients = $builder->getRecipients();
    }

    public static function builder(): ReportBuilder
    {
        return new ReportBuilder();
    }

    public function generate(): string
    {
        // Generate the report
        $content = '';
        foreach ($this->sections as $section) {
            $content .= $this->generateSection($section);
        }
        return $content;
    }

    private function generateSection(array $section): string
    {
        // Generate each section
        return '';
    }

    // Getters
    public function getTitle(): string { return $this->title; }
    public function getSections(): array { return $this->sections; }
    public function getFilters(): array { return $this->filters; }
    public function getFormatting(): array { return $this->formatting; }
    public function getDataSources(): array { return $this->dataSources; }
    public function getDeliveryOptions(): array { return $this->deliveryOptions; }
    public function getSchedule(): ?string { return $this->schedule; }
    public function getTimezone(): ?string { return $this->timezone; }
    public function getEmailSubject(): ?string { return $this->emailSubject; }
    public function getEmailBody(): ?string { return $this->emailBody; }
    public function getRecipients(): array { return $this->recipients; }
}
Enter fullscreen mode Exit fullscreen mode
Step 2: Define the Builder
// app/Builders/ReportBuilder.php
namespace App\Builders;

use App\DTO\Report;

class ReportBuilder
{
    private string $title;
    private array $sections = [];
    private array $filters = [];
    private array $formatting = [];
    private array $dataSources = [];
    private array $deliveryOptions = [];
    private ?string $schedule = null;
    private ?string $timezone = null;
    private ?string $emailSubject = null;
    private ?string $emailBody = null;
    private array $recipients = [];

    // Required
    public function title(string $title): self
    {
        $this->title = $title;
        return $this;
    }

    // Sections
    public function section(string $name, array $data, string $type = 'table'): self
    {
        $this->sections[] = [
            'name' => $name,
            'data' => $data,
            'type' => $type,
        ];
        return $this;
    }

    public function chartSection(string $name, array $data, string $chartType = 'bar'): self
    {
        $this->sections[] = [
            'name' => $name,
            'data' => $data,
            'type' => 'chart',
            'chart_type' => $chartType,
        ];
        return $this;
    }

    public function summarySection(string $name, array $metrics): self
    {
        $this->sections[] = [
            'name' => $name,
            'data' => $metrics,
            'type' => 'summary',
        ];
        return $this;
    }

    // Data Sources
    public function dataSource(string $name, string $connection, string $query): self
    {
        $this->dataSources[] = [
            'name' => $name,
            'connection' => $connection,
            'query' => $query,
        ];
        return $this;
    }

    public function dataSources(array $sources): self
    {
        foreach ($sources as $source) {
            $this->dataSource(
                $source['name'],
                $source['connection'],
                $source['query']
            );
        }
        return $this;
    }

    // Filters
    public function filter(string $key, $value, string $operator = '='): self
    {
        $this->filters[] = [
            'key' => $key,
            'value' => $value,
            'operator' => $operator,
        ];
        return $this;
    }

    public function dateRange(string $key, string $start, string $end): self
    {
        $this->filters[] = [
            'key' => $key,
            'type' => 'date_range',
            'start' => $start,
            'end' => $end,
        ];
        return $this;
    }

    public function filters(array $filters): self
    {
        foreach ($filters as $filter) {
            if (isset($filter['type']) && $filter['type'] === 'date_range') {
                $this->dateRange($filter['key'], $filter['start'], $filter['end']);
            } else {
                $this->filter($filter['key'], $filter['value'], $filter['operator'] ?? '=');
            }
        }
        return $this;
    }

    // Formatting
    public function formatting(array $options): self
    {
        $this->formatting = array_merge($this->formatting, $options);
        return $this;
    }

    public function sortBy(string $column, string $direction = 'asc'): self
    {
        $this->formatting['sort_by'] = $column;
        $this->formatting['sort_direction'] = $direction;
        return $this;
    }

    public function columns(array $columns): self
    {
        $this->formatting['columns'] = $columns;
        return $this;
    }

    public function withTotals(): self
    {
        $this->formatting['show_totals'] = true;
        return $this;
    }

    public function withPercentages(): self
    {
        $this->formatting['show_percentages'] = true;
        return $this;
    }

    // Delivery Options
    public function delivery(string $method, array $options = []): self
    {
        $this->deliveryOptions[] = [
            'method' => $method,
            'options' => $options,
        ];
        return $this;
    }

    public function emailDelivery(array $recipients, string $subject = null, string $body = null): self
    {
        $this->recipients = array_merge($this->recipients, $recipients);
        $this->emailSubject = $subject;
        $this->emailBody = $body;

        return $this->delivery('email', [
            'recipients' => $recipients,
            'subject' => $subject,
            'body' => $body,
        ]);
    }

    public function downloadDelivery(string $format = 'pdf'): self
    {
        return $this->delivery('download', ['format' => $format]);
    }

    public function webhookDelivery(string $url, array $headers = []): self
    {
        return $this->delivery('webhook', [
            'url' => $url,
            'headers' => $headers,
        ]);
    }

    // Scheduling
    public function schedule(string $cron, string $timezone = 'UTC'): self
    {
        $this->schedule = $cron;
        $this->timezone = $timezone;
        return $this;
    }

    public function immediate(): self
    {
        $this->schedule = 'immediate';
        return $this;
    }

    // Build method
    public function build(): Report
    {
        $this->validate();
        return new Report($this);
    }

    // Validation
    private function validate(): void
    {
        if (empty($this->title)) {
            throw new \InvalidArgumentException('Report title is required');
        }

        if (empty($this->sections)) {
            throw new \InvalidArgumentException('Report must have at least one section');
        }

        if (empty($this->dataSources)) {
            throw new \InvalidArgumentException('Report must have at least one data source');
        }
    }

    // Getters for the product
    public function getTitle(): string { return $this->title; }
    public function getSections(): array { return $this->sections; }
    public function getFilters(): array { return $this->filters; }
    public function getFormatting(): array { return $this->formatting; }
    public function getDataSources(): array { return $this->dataSources; }
    public function getDeliveryOptions(): array { return $this->deliveryOptions; }
    public function getSchedule(): ?string { return $this->schedule; }
    public function getTimezone(): ?string { return $this->timezone; }
    public function getEmailSubject(): ?string { return $this->emailSubject; }
    public function getEmailBody(): ?string { return $this->emailBody; }
    public function getRecipients(): array { return $this->recipients; }
}
Enter fullscreen mode Exit fullscreen mode
Step 3: Usage
// app/Http/Controllers/ReportController.php
namespace App\Http\Controllers;

use App\DTO\Report;
use App\Builders\ReportBuilder;
use Illuminate\Http\Request;

class ReportController extends Controller
{
    public function generate(Request $request)
    {
        $report = Report::builder()
            ->title('Monthly Sales Report')

            // Add sections
            ->section('Overview', [
                ['total_sales' => '$125,430', 'orders' => 1245, 'avg_order' => '$100.75'],
            ], 'summary')

            ->chartSection('Sales Trends', [
                ['month' => 'Jan', 'sales' => 12000],
                ['month' => 'Feb', 'sales' => 15000],
                ['month' => 'Mar', 'sales' => 18000],
            ], 'line')

            ->section('Top Products', [
                ['product' => 'Widget A', 'sales' => 450, 'revenue' => 22500],
                ['product' => 'Widget B', 'sales' => 320, 'revenue' => 16000],
                ['product' => 'Widget C', 'sales' => 280, 'revenue' => 14000],
            ], 'table')

            // Add data sources
            ->dataSource('orders', 'mysql', 'SELECT * FROM orders WHERE status = "completed"')
            ->dataSource('customers', 'mysql', 'SELECT * FROM customers WHERE active = 1')

            // Add filters
            ->dateRange('created_at', '2024-01-01', '2024-12-31')
            ->filter('status', 'completed')
            ->filter('payment_method', ['stripe', 'paypal'], 'in')

            // Formatting
            ->columns(['product', 'sales', 'revenue'])
            ->sortBy('sales', 'desc')
            ->withTotals()
            ->withPercentages()

            // Delivery
            ->emailDelivery(
                ['ceo@company.com', 'finance@company.com'],
                'Monthly Sales Report',
                'Please find the monthly sales report attached.'
            )
            ->downloadDelivery('pdf')
            ->webhookDelivery('https://webhook.company.com/reports', ['X-API-Key' => 'secret'])

            // Schedule
            ->schedule('0 9 1 * *', 'America/New_York')

            ->build();

        // Generate and deliver the report
        $content = $report->generate();

        // Send via email, download, webhook based on delivery options
        $this->deliverReport($report, $content);

        return response()->json(['success' => true]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Design Works

  1. Readability: The builder chain clearly shows what the report contains.
  2. Flexibility: You can add sections, filters, and delivery options as needed.
  3. Validation: The builder validates required fields.
  4. Extensibility: Add new features by adding builder methods.
  5. Immutability: The report is immutable after construction.

10. SOLID Principles Mapping

S - Single Responsibility Principle (SRP)

  • The builder is responsible for construction.
  • The product is responsible for its behavior.
  • The client is responsible for configuring the builder.

O - Open/Closed Principle (OCP)

Add new configuration options by adding builder methods, without modifying existing code.

// Adding a new method doesn't affect existing code
public function withWatermark(string $text): self
{
    $this->formatting['watermark'] = $text;
    return $this;
}
Enter fullscreen mode Exit fullscreen mode

L - Liskov Substitution Principle (LSP)

The builder creates products that are substitutable for the product interface.

D - Dependency Inversion Principle (DIP)

The client depends on the builder abstraction, not on concrete product construction.

I - Interface Segregation Principle (ISP)

The builder has focused methods for each configuration option, not a bloated interface.


11. Trade-offs

Benefits

  1. Readability: The code reads like English.
  2. Flexibility: You can skip optional parameters.
  3. Validation: The builder can validate the object state.
  4. Immutability: The builder can create immutable objects.
  5. Maintainability: Adding new parameters is easy.
  6. Reusability: The same builder can create different configurations.

Costs

  1. More Classes: You need a builder class and a product class.
  2. Complexity: More moving parts.
  3. Boilerplate: You need to write builder methods for each parameter.
  4. Learning Curve: Developers need to understand the pattern.

When Is Complexity Justified?

Use the Builder Pattern when:

  • Objects have 4+ parameters, especially with many optional ones.
  • The construction process has multiple steps.
  • You need to create immutable objects.
  • The object configuration is complex.
  • You want to validate the object state before creation.

Avoid the Builder Pattern when:

  • Objects have 1-3 simple parameters.
  • The construction process is trivial.
  • The overhead of the pattern isn't justified.

12. When NOT To Use It

3 Green Flags (USE BUILDER)

  1. Telescoping Constructor: Your constructor has 4+ parameters.

  2. Multiple Optional Parameters: Many parameters are optional.

  3. Complex Construction: The object requires multiple steps to build.

3 Red Flags (AVOID BUILDER)

  1. Simple Objects: 1-3 simple parameters.
// GOOD: Simple constructor
$user = new User($name, $email);

// BAD: Builder for simple object
$user = User::builder()->name('John')->email('john@example.com')->build();
Enter fullscreen mode Exit fullscreen mode
  1. Single Configuration: The object has one fixed configuration.

  2. Performance Critical: The overhead of the builder matters.


13. Common Mistakes

1. Over-Engineering Simple Objects

// BAD: Builder for a simple object
class UserBuilder
{
    private string $name;
    private string $email;

    public function name(string $name): self { $this->name = $name; return $this; }
    public function email(string $email): self { $this->email = $email; return $this; }
    public function build(): User { return new User($this->name, $this->email); }
}

// GOOD: Simple constructor
$user = new User('John Doe', 'john@example.com');
Enter fullscreen mode Exit fullscreen mode

2. Mutable Builder

// BAD: Mutable builder
class Builder
{
    private array $data;

    public function set(string $key, $value): self
    {
        $this->data[$key] = $value;
        return $this;
    }

    public function build(): Product
    {
        // Reuses the same data
        return new Product($this->data);
    }
}

// Problem: Builder state can be modified after use
Enter fullscreen mode Exit fullscreen mode

Fix: Make the builder immutable or reset state after build().

3. Missing Validation

// BAD: No validation
class Builder
{
    public function build(): Product
    {
        return new Product($this->data); // Could be invalid!
    }
}

// GOOD: Validation before building
public function build(): Product
{
    $this->validate();
    return new Product($this->data);
}
Enter fullscreen mode Exit fullscreen mode

4. Builder Interface Pollution

// BAD: Builder with too many methods
interface Builder
{
    public function setA($value): self;
    public function setB($value): self;
    public function setC($value): self;
    // 20 more methods...
    public function build(): Product;
}
Enter fullscreen mode Exit fullscreen mode

Fix: Keep the builder focused on the product being built.

5. Not Handling Required Parameters

// BAD: Required parameters are optional in the builder
class Builder
{
    public function build(): Product
    {
        // Missing required parameters cause runtime errors
        return new Product($this->data);
    }
}

// GOOD: Required parameters are enforced in the constructor
class Builder
{
    public function __construct(string $required)
    {
        $this->required = $required;
    }
}
Enter fullscreen mode Exit fullscreen mode

14. Frequently Asked Interview Questions

Beginner/Intermediate

  1. Q: What is the Builder Pattern?
    A: A creational design pattern that separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

  2. Q: When would you use the Builder Pattern?
    A: When objects have 4+ parameters, especially with many optional ones, and when the construction process is complex.

  3. Q: How does the Builder Pattern differ from the Factory Pattern?
    A: Factory Method creates objects in one step, while Builder creates objects step by step. Builder is better for complex objects with many parameters.

  4. Q: What are the benefits of the Builder Pattern?
    A: Readability, flexibility, validation, immutability, and maintainability.

  5. Q: How does Laravel use the Builder Pattern?
    A: In Query Builder, Eloquent, HTTP Client, Mail, Schema Builder, and Validation.

Senior/Architect

  1. Q: Explain the difference between a Builder and a Factory.
    A: A factory creates objects in one go. A builder constructs objects step by step and is used when the object has many configuration options.

  2. Q: How do you handle required vs optional parameters in a builder?
    A: Required parameters can be passed in the builder's constructor. Optional parameters have fluent methods.

  3. Q: What's the relationship between the Builder Pattern and immutability?
    A: Builders are ideal for creating immutable objects because they separate construction from representation.

  4. Q: How do you test a class that uses a builder?
    A: You can test the builder's validation and the product's behavior separately. Use dependency injection for the builder.

  5. Q: How do you handle builder reuse?
    A: You can create multiple products from the same builder by resetting the builder state or using different builder instances.


15. Interactive Practice Challenge

The Requirement

You're building a Notification Builder for a SaaS application. Notifications can be complex with many options.

The Code (POOR DESIGN)

namespace App\Services;

class Notification
{
    private string $type;
    private string $title;
    private string $body;
    private string $recipient;
    private ?string $actionUrl = null;
    private ?string $actionText = null;
    private array $actions = [];
    private string $priority = 'normal';
    private array $attachments = [];
    private ?string $imageUrl = null;
    private ?string $sender = null;
    private array $metadata = [];
    private bool $urgent = false;
    private ?string $scheduleAt = null;
    private string $channel = 'email';

    public function __construct(
        string $type,
        string $title,
        string $body,
        string $recipient,
        ?string $actionUrl = null,
        ?string $actionText = null,
        array $actions = [],
        string $priority = 'normal',
        array $attachments = [],
        ?string $imageUrl = null,
        ?string $sender = null,
        array $metadata = [],
        bool $urgent = false,
        ?string $scheduleAt = null,
        string $channel = 'email'
    ) {
        $this->type = $type;
        $this->title = $title;
        $this->body = $body;
        $this->recipient = $recipient;
        $this->actionUrl = $actionUrl;
        $this->actionText = $actionText;
        $this->actions = $actions;
        $this->priority = $priority;
        $this->attachments = $attachments;
        $this->imageUrl = $imageUrl;
        $this->sender = $sender;
        $this->metadata = $metadata;
        $this->urgent = $urgent;
        $this->scheduleAt = $scheduleAt;
        $this->channel = $channel;
    }
}

class NotificationService
{
    public function sendNotification(array $data): void
    {
        $notification = new Notification(
            $data['type'],
            $data['title'],
            $data['body'],
            $data['recipient'],
            $data['action_url'] ?? null,
            $data['action_text'] ?? null,
            $data['actions'] ?? [],
            $data['priority'] ?? 'normal',
            $data['attachments'] ?? [],
            $data['image_url'] ?? null,
            $data['sender'] ?? null,
            $data['metadata'] ?? [],
            $data['urgent'] ?? false,
            $data['schedule_at'] ?? null,
            $data['channel'] ?? 'email'
        );

        // Send notification...
    }
}
Enter fullscreen mode Exit fullscreen mode

The Challenges

The constructor has 15 parameters! The code is unmaintainable.

New requirements are coming:

  1. "We need to support push notifications with custom sounds."
  2. "We need to support in-app notifications with badges."
  3. "We need to support email notifications with custom templates."
  4. "We need to add notification categories for filtering."
  5. "We need to support notification threading/replies."

Your Task

Refactor this system using the Builder Pattern. Specifically:

  1. Create a NotificationBuilder class with fluent methods for each parameter.

  2. Create a Notification class with a private constructor that takes the builder.

  3. Implement validation in the builder for required fields.

  4. Add support for new features (push sounds, badges, templates, categories) using builder methods.

  5. Refactor the NotificationService to use the builder.

  6. Implement a fluent API that reads like English.

Questions to Consider

  • How do you handle required vs optional parameters?
  • How do you handle different notification types (email, push, in-app)?
  • How do you handle validation for each type?
  • How do you make the builder extensible for new features?
  • How do you test the builder and the notification separately?

(We won't provide the solution—refactor this code and master the Builder Pattern!)


16. Final Mental Model

To keep it simple, memorize these three sentences:

  • One-sentence definition: The Builder Pattern constructs complex objects step by step, separating construction from representation.

  • One-sentence intuition: Instead of a giant constructor, use a builder with fluent methods to set each parameter, then call build().

  • One-sentence decision rule: If your constructor has more than 4 parameters, especially with many optional ones, use a builder.


17. Related Concepts

SOLID Principles

  • Single Responsibility: The builder handles construction, the product handles behavior.
  • Open/Closed: Add new builder methods without changing existing code.
  • Liskov Substitution: Products are substitutable.
  • Dependency Inversion: Clients depend on the builder abstraction.

Design Patterns

  • Factory Method: Creates objects in one step.
  • Abstract Factory: Creates families of objects.
  • Prototype: Creates objects by cloning.
  • Singleton: Ensures only one instance.
  • Fluent Interface: The builder is a fluent interface.

Laravel Internals

  • Query Builder: DB::table()->select()->where()->get()
  • Eloquent: User::where()->with()->orderBy()->get()
  • HTTP Client: Http::withHeaders()->withOptions()->post()
  • Mail: Mail::to()->cc()->bcc()->send()
  • Schema Builder: Schema::create()->table()->id()->timestamps()

Enterprise Patterns

  • Builder: The pattern itself.
  • DTO: Often built with builders.
  • Value Object: Immutable objects built with builders.
  • Domain Object: Complex domain entities built with builders.

Final Thoughts

The new keyword is powerful, but with great power comes great responsibility. Using new everywhere creates tight coupling, scattered logic, and makes your code hard to test and extend.

The Builder Pattern gives you a better way. It centralizes object creation, decouples your code, and makes it easy to add new types.

Every time you find yourself writing new in your controller, ask yourself: "Should this be moved to a factory?"

Remember: Stop creating objects with new. Start using factories. Your future self will thank you.


Github: Builder Pattern Practice Labs

Top comments (0)