Builder Pattern
The Builder Pattern is a core design pattern used throughout Graphite to provide a fluent, type-safe, and consistent way to construct complex objects such as Assistants, Tools, Nodes, Workflows, and Topics. This pattern enables clean, readable object creation with method chaining while ensuring proper validation and configuration.
Overview
Graphite implements the Builder Pattern using a composition-based approach where builders are separate classes that construct target objects using kwargs. This design provides:
- Fluent Interface: Method chaining for readable configuration
- Type Safety: Compile-time type checking with proper return types
- Separation of Concerns: Builders are independent of the target classes
- Validation: Centralized validation logic in the
build()
method - Consistency: Uniform construction pattern across all components
Core Architecture
BaseBuilder (Generic)
The foundation of all builders in Graphite is the BaseBuilder
class:
from typing import Any, Generic, TypeVar
from pydantic import BaseModel
T = TypeVar("T", bound=BaseModel)
class BaseBuilder(Generic[T]):
"""Generic builder that can build *any* Pydantic model."""
kwargs: dict[str, Any] = {}
_cls: type[T]
def __init__(self, cls: type[T]) -> None:
self._cls = cls
self.kwargs = {}
def build(self) -> T:
"""Return the fully configured product."""
return self._cls(**self.kwargs)
Key Design Principles:
- kwargs-based: Builders accumulate configuration in a
kwargs
dictionary - Generic: Single base class handles all Pydantic model types
- Immutable Target: Objects are constructed once with all parameters
- Type Safety: Generic type parameter ensures type safety
Implementation Guide
Creating a New Builder
When implementing the builder pattern for a new component, follow these steps:
1. Define Your Component Class
First, create your Pydantic model without any builder methods:
from typing import Optional
from pydantic import BaseModel, Field
class DatabaseConnection(BaseModel):
"""Database connection configuration."""
host: str
port: int = Field(default=5432)
database: str
username: str
password: Optional[str] = Field(default=None)
ssl_enabled: bool = Field(default=True)
timeout: int = Field(default=30)
2. Create the Builder Class
Create a separate builder class that extends the appropriate base builder, and do not override the build()
methods if no advanced type checks. For post initialization checks or operations, use pydantic model_post_init()
instead.
from typing import Self, TypeVar, Optional
T_DB = TypeVar("T_DB", bound=DatabaseConnection)
class DatabaseConnectionBuilder(BaseBuilder[T_DB]):
"""Builder for DatabaseConnection instances."""
def host(self, host: str) -> Self:
self.kwargs["host"] = host
return self
def port(self, port: int) -> Self:
self.kwargs["port"] = port
return self
def database(self, database: str) -> Self:
self.kwargs["database"] = database
return self
def username(self, username: str) -> Self:
self.kwargs["username"] = username
return self
def password(self, password: str) -> Self:
self.kwargs["password"] = password
return self
def ssl_enabled(self, enabled: bool) -> Self:
self.kwargs["ssl_enabled"] = enabled
return self
def timeout(self, timeout: int) -> Self:
self.kwargs["timeout"] = timeout
return self
3. Add Builder class to its object class
from typing import Optional
from pydantic import BaseModel, Field
class DatabaseConnection(BaseModel):
"""Database connection configuration."""
host: str
port: int = Field(default=5432)
database: str
username: str
password: Optional[str] = Field(default=None)
ssl_enabled: bool = Field(default=True)
timeout: int = Field(default=30)
@classmethod
def builder(cls) -> "DatabaseConnectionBuilder":
"""Return a builder for DatabaseConnectionBuilder."""
return DatabaseConnectionBuilder(cls)
4. Usage Examples
Here's how to use the builder:
# Basic usage
db_config = (DatabaseConnection.builder()
.host("localhost")
.database("myapp")
.username("user")
.password("secret")
.build())
# With optional parameters
db_config = (DatabaseConnection.builder()
.host("prod-db.example.com")
.port(3306)
.database("production")
.username("app_user")
.password("secure_password")
.ssl_enabled(True)
.timeout(60)
.build())
# Error handling
try:
db_config = (DatabaseConnection.builder()
.host("localhost")
# Missing required database and username
.build())
except ValueError as e:
print(f"Configuration error: {e}")
Advanced Builder Patterns
Builder with Complex Validation
For components with complex validation rules:
class EmailServerBuilder(BaseBuilder[EmailServer]):
"""Builder for EmailServer with complex validation."""
def build(self) -> EmailServer:
"""Build with comprehensive validation."""
# Required field validation
required_fields = ["host", "port", "sender_email"]
for field in required_fields:
if field not in self.kwargs:
raise ValueError(f"{field} is required")
# Conditional validation
if self.kwargs.get("use_tls", False) and self.kwargs.get("port") == 25:
raise ValueError("TLS cannot be used with port 25")
# Cross-field validation
if self.kwargs.get("auth_required", False):
if not self.kwargs.get("username") or not self.kwargs.get("password"):
raise ValueError("username and password required when auth_required=True")
# Format validation
email = self.kwargs.get("sender_email", "")
if "@" not in email:
raise ValueError("sender_email must be a valid email address")
return super().build()
Best Practices
1. Separation of Concerns
Do: Keep builders separate from the target classes
# Good - Builder is separate
class MyComponent(BaseModel):
name: str
value: int
class MyComponentBuilder(BaseBuilder[MyComponent]):
def name(self, name: str) -> Self:
self.kwargs["name"] = name
return self
Don't: Mix builder methods into the target class
# Bad - Builder methods in target class
class MyComponent(BaseModel):
name: str
value: int
def with_name(self, name: str) -> Self: # Don't do this
self.name = name
return self
2. Validation in build()
Perform validation in the build()
method if have to, pydantic will validate the required fields.
def build(self) -> MyComponent:
"""Build with validation."""
# Validate required fields
if "name" not in self.kwargs:
raise ValueError("name is required")
# Validate business rules
if self.kwargs.get("value", 0) < 0:
raise ValueError("value must be non-negative")
return super().build()
3. Type Safety
Use proper type annotations and generics:
T_MC = TypeVar("T_MC", bound=MyComponent)
class MyComponentBuilder(BaseBuilder[T_MC]):
def name(self, name: str) -> Self: # Returns Self for chaining
self.kwargs["name"] = name
return self
4. Error Messages
Provide clear, actionable error messages:
def build(self) -> MyComponent:
if "host" not in self.kwargs:
raise ValueError("host is required. Use .host('hostname') to set it.")
if self.kwargs.get("port", 0) <= 0:
raise ValueError("port must be positive. Use .port(8080) to set a valid port.")
Integration with Existing Components
When working with Graphite's existing components, use their provided builders:
# Assistant construction
assistant = (MyAssistant.builder()
.name("Customer Support")
.type("support")
.oi_span_type(OpenInferenceSpanKindValues.AGENT)
.event_store(InMemoryEventStore())
.build())
# Workflow construction
workflow = (EventDrivenWorkflow.builder()
.name("Processing Pipeline")
.node(preprocessing_node)
.node(llm_node)
.node(postprocessing_node)
.build())
# Topic construction
topic = (Topic.builder()
.name("user_input")
.condition(lambda msgs: len(msgs) > 0)
.build())
Summary
The Builder Pattern in Graphite provides a consistent, type-safe way to construct complex objects through:
- Separation: Builders are independent classes, not mixed into target objects
- Parameter-based Construction: All configuration accumulates in a parameters dictionary
- Generic Base: Single
BaseBuilder
class handles all model types - Validation: Centralized validation logic in the
build()
method - Type Safety: Proper generics and type annotations throughout
This pattern enables readable, maintainable object construction while ensuring proper validation and configuration management across the entire framework.