Factory and Builder Patterns: Flexible Object Construction

Factory and Builder patterns both separate object construction from usage, but they solve different problems. Factory methods/abstract factories encapsulate the choice of which class to instantiate. B

Introduction#

Factory and Builder patterns both separate object construction from usage, but they solve different problems. Factory methods/abstract factories encapsulate the choice of which class to instantiate. Builder patterns manage complex construction of objects with many optional parts. Both reduce coupling between the code that needs an object and the code that creates it.

Factory Method#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from abc import ABC, abstractmethod

# Factory method: subclasses decide which class to instantiate

class Notifier(ABC):
    @abstractmethod
    def send(self, recipient: str, message: str) -> None:
        pass

class EmailNotifier(Notifier):
    def __init__(self, smtp_host: str, from_address: str):
        self._smtp_host = smtp_host
        self._from_address = from_address

    def send(self, recipient: str, message: str) -> None:
        print(f"Email from {self._from_address} to {recipient}: {message}")

class SlackNotifier(Notifier):
    def __init__(self, webhook_url: str, channel: str):
        self._webhook_url = webhook_url
        self._channel = channel

    def send(self, recipient: str, message: str) -> None:
        print(f"Slack to {self._channel}: {message}")

class SMSNotifier(Notifier):
    def __init__(self, api_key: str):
        self._api_key = api_key

    def send(self, recipient: str, message: str) -> None:
        print(f"SMS to {recipient}: {message}")

# Factory function (simplest form)
def create_notifier(config: dict) -> Notifier:
    kind = config["type"]
    if kind == "email":
        return EmailNotifier(config["smtp_host"], config["from_address"])
    elif kind == "slack":
        return SlackNotifier(config["webhook_url"], config["channel"])
    elif kind == "sms":
        return SMSNotifier(config["api_key"])
    raise ValueError(f"Unknown notifier type: {kind}")

# Registry-based factory (extensible without modifying the factory)
class NotifierRegistry:
    _registry: dict[str, type[Notifier]] = {}

    @classmethod
    def register(cls, name: str, notifier_class: type[Notifier]) -> None:
        cls._registry[name] = notifier_class

    @classmethod
    def create(cls, config: dict) -> Notifier:
        kind = config["type"]
        notifier_class = cls._registry.get(kind)
        if not notifier_class:
            raise ValueError(f"Unknown notifier: {kind}")
        return notifier_class(**{k: v for k, v in config.items() if k != "type"})

NotifierRegistry.register("email", EmailNotifier)
NotifierRegistry.register("slack", SlackNotifier)
NotifierRegistry.register("sms", SMSNotifier)

# Now adding a new notifier type requires no change to the registry code
class WebhookNotifier(Notifier):
    def __init__(self, url: str):
        self._url = url

    def send(self, recipient: str, message: str) -> None:
        print(f"Webhook to {self._url}: {message}")

NotifierRegistry.register("webhook", WebhookNotifier)

Abstract Factory#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# Abstract factory: produce families of related objects

class Button(ABC):
    @abstractmethod
    def render(self) -> str:
        pass

class Dialog(ABC):
    @abstractmethod
    def render(self) -> str:
        pass

class Input(ABC):
    @abstractmethod
    def render(self) -> str:
        pass

# Concrete families
class MaterialButton(Button):
    def render(self) -> str:
        return "<button class='mat-button'>Click</button>"

class MaterialDialog(Dialog):
    def render(self) -> str:
        return "<div class='mat-dialog'></div>"

class MaterialInput(Input):
    def render(self) -> str:
        return "<input class='mat-input' />"

class BootstrapButton(Button):
    def render(self) -> str:
        return "<button class='btn btn-primary'>Click</button>"

class BootstrapDialog(Dialog):
    def render(self) -> str:
        return "<div class='modal'></div>"

class BootstrapInput(Input):
    def render(self) -> str:
        return "<input class='form-control' />"

# Abstract factory interface
class UIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button:
        pass

    @abstractmethod
    def create_dialog(self) -> Dialog:
        pass

    @abstractmethod
    def create_input(self) -> Input:
        pass

# Concrete factories
class MaterialUIFactory(UIFactory):
    def create_button(self) -> Button: return MaterialButton()
    def create_dialog(self) -> Dialog: return MaterialDialog()
    def create_input(self) -> Input: return MaterialInput()

class BootstrapFactory(UIFactory):
    def create_button(self) -> Button: return BootstrapButton()
    def create_dialog(self) -> Dialog: return BootstrapDialog()
    def create_input(self) -> Input: return BootstrapInput()

# Code that uses UI components is decoupled from their implementation
def render_login_form(factory: UIFactory) -> str:
    email = factory.create_input()
    password = factory.create_input()
    submit = factory.create_button()
    return f"{email.render()}{password.render()}{submit.render()}"

# Switch entire UI theme by changing the factory
form = render_login_form(MaterialUIFactory())
form = render_login_form(BootstrapFactory())

Builder Pattern#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# Builder: construct complex objects step by step

from dataclasses import dataclass, field
from typing import Optional

@dataclass
class HttpRequest:
    url: str
    method: str = "GET"
    headers: dict[str, str] = field(default_factory=dict)
    params: dict[str, str] = field(default_factory=dict)
    body: Optional[bytes] = None
    timeout: float = 30.0
    follow_redirects: bool = True
    auth: Optional[tuple[str, str]] = None

class HttpRequestBuilder:
    def __init__(self, url: str):
        self._url = url
        self._method = "GET"
        self._headers: dict[str, str] = {}
        self._params: dict[str, str] = {}
        self._body: Optional[bytes] = None
        self._timeout = 30.0
        self._follow_redirects = True
        self._auth: Optional[tuple[str, str]] = None

    def method(self, method: str) -> "HttpRequestBuilder":
        self._method = method.upper()
        return self  # fluent interface: return self for chaining

    def header(self, key: str, value: str) -> "HttpRequestBuilder":
        self._headers[key] = value
        return self

    def bearer_auth(self, token: str) -> "HttpRequestBuilder":
        self._headers["Authorization"] = f"Bearer {token}"
        return self

    def basic_auth(self, username: str, password: str) -> "HttpRequestBuilder":
        self._auth = (username, password)
        return self

    def param(self, key: str, value: str) -> "HttpRequestBuilder":
        self._params[key] = value
        return self

    def json_body(self, data: dict) -> "HttpRequestBuilder":
        import json
        self._body = json.dumps(data).encode()
        self._headers["Content-Type"] = "application/json"
        self._method = "POST" if self._method == "GET" else self._method
        return self

    def timeout(self, seconds: float) -> "HttpRequestBuilder":
        self._timeout = seconds
        return self

    def no_redirects(self) -> "HttpRequestBuilder":
        self._follow_redirects = False
        return self

    def build(self) -> HttpRequest:
        return HttpRequest(
            url=self._url,
            method=self._method,
            headers=self._headers.copy(),
            params=self._params.copy(),
            body=self._body,
            timeout=self._timeout,
            follow_redirects=self._follow_redirects,
            auth=self._auth,
        )

# Fluent builder usage
request = (
    HttpRequestBuilder("https://api.example.com/orders")
    .method("POST")
    .bearer_auth(token)
    .header("X-Request-ID", "abc-123")
    .json_body({"customer_id": "c1", "items": [...]})
    .timeout(10.0)
    .build()
)

Builder for SQL Queries#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class SelectBuilder:
    """Type-safe SQL SELECT builder."""

    def __init__(self, table: str):
        self._table = table
        self._columns: list[str] = ["*"]
        self._conditions: list[str] = []
        self._params: list = []
        self._order_by: list[str] = []
        self._limit: Optional[int] = None
        self._offset: Optional[int] = None
        self._joins: list[str] = []

    def columns(self, *cols: str) -> "SelectBuilder":
        self._columns = list(cols)
        return self

    def where(self, condition: str, *params) -> "SelectBuilder":
        self._conditions.append(condition)
        self._params.extend(params)
        return self

    def join(self, table: str, on: str) -> "SelectBuilder":
        self._joins.append(f"JOIN {table} ON {on}")
        return self

    def order_by(self, column: str, direction: str = "ASC") -> "SelectBuilder":
        self._order_by.append(f"{column} {direction}")
        return self

    def limit(self, n: int) -> "SelectBuilder":
        self._limit = n
        return self

    def offset(self, n: int) -> "SelectBuilder":
        self._offset = n
        return self

    def build(self) -> tuple[str, list]:
        parts = [f"SELECT {', '.join(self._columns)} FROM {self._table}"]
        parts.extend(self._joins)
        if self._conditions:
            parts.append(f"WHERE {' AND '.join(self._conditions)}")
        if self._order_by:
            parts.append(f"ORDER BY {', '.join(self._order_by)}")
        if self._limit is not None:
            parts.append(f"LIMIT {self._limit}")
        if self._offset is not None:
            parts.append(f"OFFSET {self._offset}")
        return " ".join(parts), self._params

# Usage
query, params = (
    SelectBuilder("orders")
    .columns("orders.id", "orders.total", "customers.name")
    .join("customers", "orders.customer_id = customers.id")
    .where("orders.status = %s", "completed")
    .where("orders.created_at > %s", "2025-01-01")
    .order_by("orders.created_at", "DESC")
    .limit(20)
    .offset(40)
    .build()
)

print(query)
# SELECT orders.id, orders.total, customers.name FROM orders
# JOIN customers ON orders.customer_id = customers.id
# WHERE orders.status = %s AND orders.created_at > %s
# ORDER BY orders.created_at DESC LIMIT 20 OFFSET 40

Conclusion#

Factory patterns decouple the caller from the specific class being instantiated — use them when the exact type to create depends on runtime configuration or context. Registry-based factories are extensible without modification. Builder patterns manage complex construction with many optional parts — use them when a constructor would have many parameters, particularly optional ones. The fluent builder interface (returning self) makes call chains readable and enforces a clear construction sequence. Both patterns reduce the reach of if/else chains and magic string lookups into a single, well-defined place.

Contents