In the previous summary, we introduced design patterns as reusable solution for repeated software design problems.

Before using design patterns in our own system, we should first understand the common categories. Refactoring.Guru groups design patterns into three major types:

  • Creational patterns: focus on how objects are created.
  • Structural patterns: focus on how classes and objects are composed into larger structures.
  • Behavioral pattern: focus on how objects communicate and share responsibilities.

This classification is useful because each group answers a different design question.

Category Main question
Creational How should we create objects without coupling the code too tightly to concrete classes?
Structural How should we compose objects and modules while keeping the structure flexible?
Behavioral How should objects communicate, delegate work, and control behavior?

This article focuses on Creational patterns.

Why creational pattern matter

Object creation looks simple at first. We can just call a constructor:

1
let client = ProviderClient::new(config);

But in a real system, object creation often includes more decisions:

  • Which implementation should be used?
  • Which config should be passed in?
  • Which dependency should be injected?
  • Should the object be created once or many times?
  • Should object construction be hidden from the caller?

If every caller knows how to create every concrete object, the system becomes harder to change. Creational patterns help us move creation logic to a clearer place.

Creational patterns catalog

Refactoring.Guru lists five common creational patterns:

Pattern Main idea Common use case
Factory Method Let subclasses or specialized methods decide which object to create. The caller knows the product interface, but not the concrete implementation.
Abstract Factory Create a family of related objects together. The system needs related objects that should be compatible with each other.
Builder Create complex objects step by step. The object has many optional fields or construction steps.
Prototype Create new objects by copying existing ones. Object creation is expensive or depends on an existing configured instance.
Singleton Ensure only one instance exists globally. A shared instance is required, but lifecycle and testing risks must be controlled.

Factory Method

Factory Method separates object creation from object usage.

Instead of letting the caller create a concrete object directly, the caller depends on an interface or abstract type. The creation logic decides which concrete implementation should be returned.

For example:

1
2
create_provider("nba") -> NbaProvider
create_provider("mlb") -> MlbProvider

The caller does not need to know the concrete provider class. It only needs to know that the returned object can behave like a provider.

Factory Method pros

  • Reduces coupling to concrete classes.
  • Keeps creation logic in one place.
  • Makes it easier to add a new implementation.
  • Works well with config-driven behavior.

Factory Method cons

  • Adds one more abstraction layer.
  • Can be overkill when there is only one concrete implementation.
  • If the factory contains too many conditions, it can become hard to maintain.

Abstract Factory

Abstract Factory is useful when we need to create a group of related objects.

The important point is not just creating one object. The important point is making sure several created objects belong to the same family.

For example:

1
2
3
4
SportradarFactory
-> create_nba_provider()
-> create_mlb_provider()
-> create_status_mapper()

If all of these objects need to follow the same provider rules, an abstract factory can keep them consistent.

Abstract Factory pros

  • Keeps related objects compatible.
  • Groups provider-specific creation logic.
  • Makes switching between object families easier.

Abstract Factory cons

  • More complex than a simple factory.
  • Adding a new object type may require changing every factory.
  • It can be too heavy if the system only creates one or two objects.

Builder

Builder is useful when creating an object requires many steps or many optional fields.

For example:

1
2
3
4
5
6
ProviderConfigBuilder
-> with_endpoint(...)
-> with_api_key(...)
-> with_retry_policy(...)
-> with_timeout(...)
-> build()

The caller can build the object gradually without passing a long list of constructor parameters.

Builder pros

  • Avoids long constructors.
  • Makes optional configuration clearer.
  • Makes object creation easier to read.
  • Can validate the final object before returning it.

Builder cons

  • Adds extra code.
  • Can hide required fields if the builder is not designed carefully.
  • May be unnecessary for simple objects.

Prototype

Prototype creates new objects by copying an existing object.

This is useful when creating an object from scratch is expensive, or when the new object should start from an existing configured state.

For example:

1
2
3
base_provider_config.clone()
-> override sport = "nba"
-> override endpoint = "/nba/feed"

Instead of rebuilding all shared configuration every time, we copy a base object and change only the different parts.

Prototype pros

  • Reuses existing configured state.
  • Can reduce repeated setup logic.
  • Useful when object creation is expensive.

Prototype cons

  • Copy behavior must be clear.
  • Deep copy and shallow copy can cause bugs.
  • Shared mutable state can become dangerous.

Singleton

Singleton ensures there is only one instance of a class or service.

It is often used for global configuration, logger, or shared resource manager. However, it is also one of the most risky patterns because it can hide dependencies.

For example:

1
2
GlobalConfig::instance()
Logger::instance()

The code becomes convenient, but the dependency is no longer explicit.

Singleton pros

  • Easy to access.
  • Ensures only one instance exists.
  • Can be useful for truly global, stateless services.

Singleton cons

  • Makes testing harder.
  • Hides dependencies.
  • Makes lifecycle unclear.
  • Can introduce global mutable state.
  • Often increases coupling over time.

How to choose

Creational patterns should be chosen based on the creation problem we actually have.

Situation Better pattern
Need to choose one implementation from config Factory Method
Need to create a family of related objects Abstract Factory
Need to build an object with many optional fields Builder
Need to copy a configured object Prototype
Need exactly one shared instance Singleton, but be careful

The main idea is simple: object creation is also part of system design. If creation logic is scattered everywhere, the system becomes harder to change. If creation logic is centralized too aggressively, the factory itself may become too complex.

The next article will introduce Structural patterns, which focus on how objects and modules are composed.

Reference