Software Design Patterns: Creational

·19 min read·Graham Mace
Software Design Patterns: Creational

This post is part of a series covering the Gang of Four design patterns, with all source code available in the accompanying design-patterns GitHub repository:

maceage/design-patterns

Creational design patterns are concerned with how objects are created. Rather than scattering new expressions throughout your codebase, these patterns abstract the instantiation process away from the code that uses the resulting objects, giving you control over what gets created, when it gets created, and how much of it is created at any one time.

In this post we’ll cover the creational patterns from the Pluralsight design patterns course:

One small caveat: although the Lazy Load pattern appeared alongside the GoF patterns in the course, it isn’t actually one of the original Gang of Four patterns – it comes from Martin Fowler’s Patterns of Enterprise Application Architecture. It earns its place here all the same, as it’s fundamentally about controlling when object creation happens.


Builder pattern

The Builder pattern is a creational design pattern that allows you to construct complex objects step by step, while keeping the construction process separate from the final product. It’s particularly useful when multiple objects share an identical shape but differ in their configuration, as the same construction “template” can be reused for each variant.

In this example, CarBuilder is an abstract base class with multiple abstract methods for building a Car:

public abstract class CarBuilder
{
    protected Car _car;

    public Car GetCar()
    {
        return _car;
    }

    public void CreateNewCar()
    {
        _car = new Car();
    }

    public abstract void PrepareDriveType();
    public abstract void ApplyBodyType();
    public abstract void ApplyBadges();
    public abstract void AddFeatures();
}

The CarMaker class - sometimes called the director - then uses the CarBuilder it is given via the constructor to instantiate a car in a templated fashion:

public class CarMaker
{
    private readonly CarBuilder _builder;

    public CarMaker(CarBuilder builder)
    {
        _builder = builder;
    }

    public void BuildCar()
    {
        _builder.CreateNewCar();
        _builder.PrepareDriveType();
        _builder.ApplyBodyType();
        _builder.ApplyBadges();
        _builder.AddFeatures();
    }

    public Car GetCar()
    {
        return _builder.GetCar();
    }
}

Each concrete builder class implements these methods to create a specific type of Car - an AudiCarBuilder, a BmwCarBuilder and a VolkswagenCarBuilder:

// AudiCarBuilder
public class AudiCarBuilder : CarBuilder
{
    public override void PrepareDriveType()
    {
        _car.DriveType = DriveType.FourByFour;
        _car.IsPetrol = true;
    }

    public override void ApplyBodyType()
    {
        _car.CarType = CarType.Suv;
    }

    public override void ApplyBadges()
    {
        _car.ManufacturerType = Manufacturer.Audi;
        _car.ModelName = "Q3";
    }

    public override void AddFeatures()
    {
        _car.HasSunroof = true;
        _car.HasAirConditioning = true;

        _car.Features = new List<string>
        {
            "Rear Parking Sensors",
            "Front Parking Sensors",
            "Adaptive Headlights",
            "Cruise Control"
        };
    }
}

// BmwCarBuilder
public class BmwCarBuilder : CarBuilder
{
    public override void PrepareDriveType()
    {
        _car.DriveType = DriveType.RearWheelDrive;
        _car.IsPetrol = true;
    }

    public override void ApplyBodyType()
    {
        _car.CarType = CarType.Coupe;
    }

    public override void ApplyBadges()
    {
        _car.ManufacturerType = Manufacturer.Bmw;
        _car.ModelName = "3 Series";
    }

    public override void AddFeatures()
    {
        _car.HasSunroof = true;
        _car.HasAirConditioning = true;

        _car.Features = new List<string>
        {
            "Rear Parking Sensors",
            "Front Parking Sensors",
            "Adaptive Headlights",
            "Cruise Control"
        };
    }
}

// VolkswagenCarBuilder
public class VolkswagenCarBuilder : CarBuilder
{
    public override void PrepareDriveType()
    {
        _car.DriveType = DriveType.FrontWheelDrive;
        _car.IsPetrol = false;
    }

    public override void ApplyBodyType()
    {
        _car.CarType = CarType.Sedan;
    }

    public override void ApplyBadges()
    {
        _car.ManufacturerType = Manufacturer.Volkswagen;
        _car.ModelName = "Passat";
    }

    public override void AddFeatures()
    {
        _car.HasSunroof = false;
        _car.HasAirConditioning = true;

        _car.Features = new List<string>
        {
            "Rear Parking Sensors",
            "Cruise Control"
        };
    }
}

To build a particular car, a CarBuilder implementation is instantiated and passed to the CarMaker constructor, the BuildCar() method instantiates the car, and the car is retrieved via the GetCar() method:

var audiCarMaker = new CarMaker(new AudiCarBuilder());
audiCarMaker.BuildCar();
var audiCar = audiCarMaker.GetCar();

var bmwCarMaker = new CarMaker(new BmwCarBuilder());
bmwCarMaker.BuildCar();
var bmwCar = bmwCarMaker.GetCar();

var volkswagenCarMaker = new CarMaker(new VolkswagenCarBuilder());
volkswagenCarMaker.BuildCar();
var volkswagenCar = volkswagenCarMaker.GetCar();

Benefits of the Builder pattern:

  • Construction decoupled from representation – the same director logic can build many different variants of a product without knowing anything about them
  • Open for extension – new product variants are added simply by writing a new builder, without touching the CarMaker or any existing builders
  • Step-by-step construction – each build step stays small, understandable and independently testable
  • Clean client code – callers never need to know how a car’s drive type, body or badges are actually assembled
  • Repeated steps are easy to spot - identical steps across builders (such as the shared feature list in AudiCarBuilder and BmwCarBuilder) can be hoisted into the base class

Overall, the Builder pattern is a strong choice whenever you find yourself with telescoping constructors or object initialisation logic scattered across a codebase, letting you standardise construction while keeping variants cleanly separated.

Factory pattern

The Factory pattern encapsulates object creation behind a single abstraction, so client code can request an object without knowing anything about the concrete implementation it’s getting back. Rather than calling new BlueSmurf() directly, clients ask the factory by name and receive an ISmurf.

In this example, a SmurfFactory creates instances of Smurf objects - BlueSmurf, RedSmurf or NullSmurf - all of which implement a simple ISmurf interface:

public interface ISmurf
{
    void Talk();
    void Sleep();
}

The factory encapsulates the creation logic, allowing clients to create Smurf objects without knowing the specific implementation details:

public class SmurfFactory
{
    private Dictionary<string, Type> _smurfs;

    public SmurfFactory()
    {
        LoadTypes();
    }

    public ISmurf CreateInstance(string smurfName)
    {
        Type t = GetTypeToCreate(smurfName);

        if (t == null)
        {
            return new NullSmurf();
        }

        return Activator.CreateInstance(t) as ISmurf;
    }

    private Type GetTypeToCreate(string smurfName)
    {
        return (from smurf in _smurfs
                where smurf.Key.Contains(smurfName)
                select _smurfs[smurf.Key]).FirstOrDefault();
    }

    private void LoadTypes()
    {
        _smurfs = new Dictionary<string, Type>();

        Type[] typesInThisAssembly = Assembly.GetExecutingAssembly().GetTypes();

        foreach (Type type in typesInThisAssembly)
        {
            if (type.GetInterface(typeof(ISmurf).ToString()) != null)
            {
                _smurfs.Add(type.Name.ToLower(), type);
            }
        }
    }
}

There are a few things worth calling out here. First, LoadTypes() uses reflection to scan the executing assembly for every type implementing ISmurf, registering each one by its lowercase name. Second, GetTypeToCreate() performs a substring match, so any name that doesn’t match a registered Smurf simply returns null. Third, when no match is found, the factory returns a NullSmurf - the null object pattern in disguise - rather than throwing an exception or returning an actual null, so client code can always operate on the result safely.

The SmurfFactory is instantiated in Program.cs and then used to create Smurf objects via the CreateInstance method, specifying the type of Smurf to be created as a string parameter value:

SmurfFactory factory = new SmurfFactory();

ISmurf blueSmurf = factory.CreateInstance("blue");  // Returns BlueSmurf
ISmurf redSmurf = factory.CreateInstance("red");    // Returns RedSmurf
ISmurf nullSmurf = factory.CreateInstance("papa");  // Returns NullSmurf

Benefits of the Factory pattern:

  • Low coupling - client code depends only on an interface (ISmurf), never on concrete implementations
  • Single point of change – all creation logic lives in one class, so construction changes ripple to exactly one place
  • Automatic registration - reflection-based scanning means new Smurf types are picked up without modifying the factory
  • Safe fallback behaviour – unknown types return a NullSmurf rather than an actual null, sparing callers null checks and null reference exceptions
  • Centralised lifetime management – object lifetime and caching decisions become factory responsibilities rather than client concerns

All told, the Factory pattern is the go-to tool when you want callers to think in terms of “give me something that does X” rather than “construct this specific class”, and it’s the natural home for any creation logic that involves lookup, fallback or reflection.

Lazy Load pattern

A relatively straightforward pattern, Lazy Load entails waiting until the last responsible moment before performing an expensive operation such as retrieving data or creating complex objects. This example has four demos of the pattern, each with a slight variation on the implementation.

Strictly speaking, Lazy Load is from Martin Fowler’s Patterns of Enterprise Application Architecture rather than the Gang of Four book, but it sits naturally alongside the creational patterns because it controls when an object comes into existence.

Ghosts

The Ghost variant represents an object that exists in memory with its identity, but whose full state hasn’t been loaded yet. The object starts life in a Ghost status and loads its data on demand – typically when a property is first accessed – using a TestOrderWrapper class to verify the behaviour:

public class TestOrderWrapper : Order
{
    public bool WasLoadCalled;
    public int GetDataRowCount;

    public TestOrderWrapper(int id)
        : base(id)
    {
    }

    public override void Load()
    {
        WasLoadCalled = true;
        base.Load();
    }

    protected override ArrayList GetDataRow()
    {
        GetDataRowCount++;
        return base.GetDataRow();
    }
}

Which extends the Order class:

public class Order : DomainObject
{
    private string _shipMethod;
    private Customer _customer;
    private Lazy<List<OrderItem>> _items;

    public Order(int id)
        : base(id)
    {
    }

    public string ShipMethod
    {
        get
        {
            Load();
            return _shipMethod;
        }

        set
        {
            Load();
            _shipMethod = value;
        }
    }

    public Customer Customer
    {
        get
        {
            Load();
            return _customer;
        }

        set
        {
            Load();
            _customer = value;
        }
    }

    public IEnumerable<OrderItem> Items
    {
        get
        {
            Load();
            return _items.Value.AsReadOnly();
        }
    }

    protected override void DoLoadLine(ArrayList dataRow)
    {
        ShipMethod = (string)dataRow[0];
        Customer = new Customer((int)dataRow[1]);
        _items = new Lazy<List<OrderItem>>(() => new OrderItemRepository().ListForOrder(Id).ToList());
    }

    protected override ArrayList GetDataRow()
    {
        var row = new ArrayList();

        row.Add("FEDEX");
        row.Add(123);

        return row;
    }
}

Which itself inherits from the DomainObject class:

public abstract class DomainObject
{
    private LoadStatus _status;

    protected DomainObject(int id)
    {
        Id = id;
    }

    public int Id { get; set; }

    public bool IsGhost
    {
        get { return _status == LoadStatus.Ghost; }
    }

    public bool IsLoaded
    {
        get { return _status == LoadStatus.Loaded; }
    }

    public void MarkLoading()
    {
        _status = LoadStatus.Loading;
    }

    public void MarkLoaded()
    {
        _status = LoadStatus.Loaded;
    }

    public virtual void Load()
    {
        if (!IsGhost)
        {
            return;
        }

        var dataRow = GetDataRow();
        LoadLine(dataRow);
    }

    private void LoadLine(ArrayList dataRow)
    {
        if (IsGhost)
        {
            MarkLoading();
            DoLoadLine(dataRow);
            MarkLoaded();
        }
    }

    protected abstract void DoLoadLine(ArrayList dataRow);
    protected abstract ArrayList GetDataRow();
}

Internally, the ShipMethod and Customer properties are only loaded when they’re accessed, by first calling the Load() method in the DomainObject class. This method fetches the raw data row via GetDataRow(), and LoadLine() then marks the object as LoadStatus.Loading while DoLoadLine() populates its state, before finally marking the status as LoadStatus.Loaded. Once loaded, subsequent calls to Load() short-circuit immediately, so the data is only ever fetched once.

This is especially important for the Customer property, as this is a fully-fledged object in its own right, with its own data to load:

public class Customer : DomainObject
{
    public Customer(int id)
        : base(id)
    {
    }

    protected override void DoLoadLine(ArrayList dataRow)
    {
        throw new System.NotImplementedException();
    }

    protected override ArrayList GetDataRow()
    {
        throw new System.NotImplementedException();
    }
}

As such, if this were coming from a database, it would be important to ensure the Customer object is fully loaded before returning it to the caller.

Object-Relational Mappers such as NHibernate and Entity Framework use this pattern to ensure objects are fully loaded before returning them to the caller, while also not loading all related objects at once from the database. Note the Items property goes one step further still - the order items themselves are wrapped in a Lazy<T> so they don’t hit the database until the collection is actually enumerated.

Benefits of the Ghost variant:

  • Cheap instantiation – thousands of lightweight references can be held without triggering thousands of database round-trips
  • Fetch at most once - the status-guarded Load() method short-circuits after the first load, so data is never retrieved twice
  • Graph-friendly loading - prevents cascade-loading an entire object graph from the database just to display one entity
  • Enforced invariants – the base class guarantees the object is fully loaded before it’s handed to a caller, rather than relying on every consumer to check

In short, ghosts let you freely construct domain objects without paying for them until you actually need their state.

Lazy Init

This example has three classes - OrderGood, OrderBad and OrderLazy - the last of which demonstrates the Lazy Init pattern using the Lazy<T> class.

// OrderGood
public class OrderGood
{
    private Customer _customer;

    public Customer Customer
    {
        get
        {
            return _customer ?? (_customer = new Customer());
        }
    }

    public string PrintLabel()
    {
        return Customer.CompanyName + Environment.NewLine + Customer.Address;
    }
}

// OrderBad
public class OrderBad
{
    private Customer _customer;

    public Customer Customer
    {
        get
        {
            return _customer ?? (_customer = new Customer());
        }
    }

    public string PrintLabel()
    {
        string result = _customer.CompanyName;
        return result + Environment.NewLine + Customer.Address;
    }
}

// OrderLazy
public class OrderLazy
{
    private readonly Lazy<Customer> _customerInitializer;

    public OrderLazy()
    {
        _customerInitializer = new Lazy<Customer>(() => new Customer());
    }

    public Customer Customer
    {
        get
        {
            return _customerInitializer.Value;
        }
    }

    public string PrintLabel()
    {
        string result = Customer.CompanyName;
        return result + Environment.NewLine + _customerInitializer.Value.Address;
    }
}

The difference between OrderGood and OrderBad is subtle but crucial: OrderBad.PrintLabel() accesses the private _customer backing field directly, bypassing the lazy initialisation in the property getter. Because _customer is null on a freshly constructed order, the very first call throws a NullReferenceException. OrderGood avoids this by always going through the Customer property. It’s a good illustration of how fragile hand-rolled lazy initialisation can be.

OrderLazy achieves the same result more safely by wrapping the instantiation of the Customer class in a Lazy<T> object, which only loads the value when it’s first accessed. Unlike the ??-based null-coalescing trick, Lazy<T> is also thread-safe by default (using LazyThreadSafetyMode.ExecutionAndPublication), so two threads accessing Customer concurrently won’t race to create duplicate instances.

Benefits of Lazy Init:

  • Pay only on first use – expensive resources are never created for values that end up never being needed
  • Single home for initialisation – the logic lives in exactly one place rather than smeared across guard clauses
  • Thread-safety for free - Lazy<T> handles concurrent first access safely by default, unlike hand-rolled null-coalescing
  • Deterministic failures – exceptions thrown during initialisation are cached by Lazy<T> and rethrown on subsequent accesses rather than re-triggering the factory

The lesson of this variant is that lazy initialisation is easy to get subtly wrong, and Lazy<T> is the safer default when you need it.

Value Holder

Using an IValueLoader<T> interface, the Value Holder pattern allows for the encapsulation of a value that can be lazily initialised or computed on demand:

public interface IValueLoader<out T>
{
    T Load();
}

Using a ValueHolder<T> implementation, the IValueLoader<T> is passed via the constructor:

public class ValueHolder<T>
{
	private readonly IValueLoader<T> _loader;
	private T _value;

	public ValueHolder(IValueLoader<T> loader)
	{
		_loader = loader;
	}

	public T Value
	{
		get
		{
			if (_value == null)
			{
				_value = _loader.Load();
			}

			return _value;
		}
	}
}

When the .Value property is accessed, IValueLoader.Load() is invoked before returning the value to the caller.

In this example, an OrderItemLoader retrieves OrderItem instances when the Load() method is called:

public class OrderItemLoader : IValueLoader<List<OrderItem>>
{
    public OrderItemLoader(int orderId)
    {
        OrderId = orderId;
    }

    public int OrderId { get; private set; }

    public List<OrderItem> Load()
    {
        Console.WriteLine("Fetching OrderItems from Database");
        return new List<OrderItem>();
    }
}

The OrderValueHolder then uses this OrderItemLoader to lazily load the OrderItem instances when they’re requested:

public class OrderValueHolder
{
    private ValueHolder<List<OrderItem>> _items;

    public OrderValueHolder(int id)
    {
        Id = id;
    }

    public int Id { get; set; }

    public List<OrderItem> Items
    {
        get { return _items.Value; }
    }

    internal void SetItems(ValueHolder<List<OrderItem>> valueHolder)
    {
        _items = valueHolder;
    }
}

The values of which are set by the SetItems() method when the OrderValueHolder is created:

public OrderValueHolder CreateFromId(int id)
{
    var order = new OrderValueHolder(id);
    order.SetItems(new ValueHolder<List<OrderItem>>(new OrderItemLoader(id)));
    return order;
}

Benefits of the Value Holder variant:

  • Fully reusable - the generic ValueHolder<T> works for any type of value, not just domain entities
  • Injectable strategy - the loading logic behind IValueLoader<T> can be swapped or mocked independently of the holder
  • Transparent to consumers - callers simply read .Value with no knowledge that anything is deferred behind it
  • Cached after first access – repeated reads don’t repeat the expensive load operation

Essentially a DIY Lazy<T>, the Value Holder earns its keep when you need control over how, when and by whom the load is performed.

Virtual Proxy

In this example, the OrderProxy class extends the Order class, standing in for it and intercepting property access to defer creation of the Customer:

// Order class
public class Order
{
    public int Id { get; set; }

    public virtual Customer Customer { get; set; }

    public string PrintLabel()
    {
        return Customer.CompanyName + Environment.NewLine + Customer.Address;
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

// OrderProxy class
public class OrderProxy : Order
{
    public override Customer Customer
    {
        get
        {
            return base.Customer ?? (base.Customer = new Customer());
        }

        set
        {
            base.Customer = value;
        }
    }

    public override bool Equals(object obj)
    {
        var other = obj as Order;

        if (other == null)
        {
            return false;
        }

        return other.Id == Id;
    }

    public override int GetHashCode()
    {
        return base.GetHashCode();
    }
}

This proxy class is then returned from the OrderFactory.CreateFromId() method in place of the Order class:

public class OrderFactory
{
    public Order CreateFromId(int id)
    {
        return new OrderProxy { Id = id };
    }
}

Because OrderProxy derives from Order and overrides the Customer property, callers are completely unaware they’re holding a proxy – they simply use it as an Order. On first access to Customer, the proxy either returns the existing customer or transparently creates and assigns a new one.

Benefits of the Virtual Proxy variant:

  • Invisible to callers – the proxy is substitutable for the real class anywhere an Order is expected
  • Deferred cost – the lazily-loaded member is only created on first access, spreading or avoiding upfront expense
  • Non-invasive - lazy behaviour is added purely through inheritance, with no changes to the Order class itself
  • One seam for everything – because the proxy is handed out by the factory, lazy behaviour can be introduced across an entire codebase from a single point

The Virtual Proxy is arguably the most transparent of the four Lazy Load variants – done well, the caller never learns that deferral is happening at all.

Prototype pattern

The Prototype pattern creates new objects by cloning an existing instance rather than constructing one from scratch. This is useful when creating a new object is expensive or complicated – perhaps it requires a database call or laborious configuration – but duplicating an existing, fully-configured instance is cheap. It also lets you produce new objects without needing to know their concrete types.

Implementing the ICloneable interface, this example uses a TheMatrix class which implements the Clone() method that returns a new instance of the class:

public class TheMatrix : ICloneable
{
    private bool _hasTakenRedPill;
    private bool _hasTakenBluePill;

    private int _numberOfTimesAgentSmithEncountered;
    private int _numberOfPhoneCalls;
    private int _numberOfBulletsFired;

    public void TakeRedPill()
    {
        _hasTakenRedPill = true;
        _hasTakenBluePill = false;
    }

    public void TakeBluePill()
    {
        _hasTakenBluePill = true;
        _hasTakenRedPill = false;
    }

    public void EncounterAgentSmith(int numberOfTimes)
    {
        _numberOfTimesAgentSmithEncountered += numberOfTimes;
    }

    public void MakePhoneCall()
    {
        _numberOfPhoneCalls++;
    }

    public void FireBullets(int numberOfBullets)
    {
        _numberOfBulletsFired += numberOfBullets;
    }

    public void PrintState()
    {
        Console.WriteLine("Has taken red pill? {0}", _hasTakenRedPill);
        Console.WriteLine("Has taken blue pill? {0}", _hasTakenBluePill);

        Console.WriteLine("Number of times Agent Smith encountered: {0}", _numberOfTimesAgentSmithEncountered);
        Console.WriteLine("Number of phone calls: {0}", _numberOfPhoneCalls);
        Console.WriteLine("Number of bullets fired: {0}", _numberOfBulletsFired);
    }

    public object Clone()
    {
        return MemberwiseClone();
    }
}

Internally, this is achieved by using the MemberwiseClone() method method to create a shallow copy of the object - meaning the cloned instance’s fields are copied, but any reference-type fields still point to the same underlying objects as the original.

It’s worth noting that Microsoft now recommends against implementing ICloneable precisely because the interface doesn’t communicate whether Clone() performs a shallow or deep copy, and callers can’t know which they’re getting without consulting documentation.

Benefits of the Prototype pattern:

  • Class-agnostic cloning - objects can be duplicated without depending on their concrete classes or knowing how they were originally constructed
  • Expensive work done once – costly construction (database reads, configuration, validation) is paid up front, then duplicated cheaply thereafter
  • State snapshots – cloning captures an object’s state at a moment in time, useful for undo scenarios and speculative branches of work
  • No parallel hierarchy - unlike Builder, a single class with a Clone() method suffices, with no family of builder classes required

On the whole, the Prototype pattern is the right tool when “copy an existing one” is simpler, faster or more expressive than “build a new one”.

Singleton pattern

Probably one of the most well-known patterns, the Singleton pattern ensures a class has only one instance and provides a global point of access to it. Typical uses include loggers, configuration holders and connection pools – anything where multiple instances would be wasteful or actively harmful.

In this example there are two classes - EagerSingleton and LazySingleton. The EagerSingleton instance is created when the class is loaded, while the LazySingleton instance is created when the GetInstance() method is called:

public class EagerSingleton
{
    private static readonly EagerSingleton _instance = new EagerSingleton();

    private EagerSingleton()
    {
    }

    public static EagerSingleton GetInstance()
    {
        return _instance;
    }

    public override string ToString()
    {
        return "Eager Singleton Instance";
    }
}

In the LazySingleton implementation, a _lockObject variable of type object is used to ensure thread-safety:

public class LazySingleton
{
    private static readonly object _lockObject = new object();

    private static LazySingleton _instance;

    private LazySingleton()
    {
    }

    public static LazySingleton GetInstance()
    {
        if (_instance == null)
        {
            lock (_lockObject)
            {
                if (_instance == null)
                {
                    _instance = new LazySingleton();
                }
            }
        }

        return _instance;
    }

    public override string ToString()
    {
        return "Lazy Singleton Instance";
    }
}

The GetInstance() method in LazySingleton uses double-checked locking: the first if avoids taking the lock on the (common) path where the instance already exists, while the second if inside the lock prevents two threads that both passed the outer check from creating competing instances.

The eager version, meanwhile, is inherently thread-safe with no locking at all – the CLR guarantees that static field initialisers run once per class, on first use of the class.

Benefits of the Singleton pattern:

  • One instance guaranteed – no matter how many threads or call sites request it, exactly one instance ever exists
  • Well-known access point – a single global route to the shared resource removes the need to thread it through every constructor in the application
  • Sensible trade-offs between variants – eager initialisation buys simplicity and intrinsic thread-safety, while lazy initialisation defers cost until first use
  • Encapsulated lifecycle – initialisation and lifetime of the shared resource are contained within one class rather than scattered across consumers

It’s worth being honest about the pattern’s reputation, though: singletons carry hidden costs – a global access point that hides dependencies, complicates unit testing, and couples callers to a concrete class. Dependency injection containers often offer the same guarantee (one shared instance) with fewer drawbacks. Used judiciously for genuinely global resources, though, the Singleton pattern remains a legitimate and effective tool.

Wrapping up

That brings us to the end of the creational patterns – the ones concerned with the how and when of object creation. Between them they cover a surprising amount of ground: Builder and Prototype give us different ways to assemble or duplicate objects without tangled constructors, Factory hides the concrete class behind an interface, Singleton puts a ceiling of one on instantiation, and the Lazy Load variants remind us that sometimes the most powerful thing you can do is simply not create something until it’s needed.

What strikes me looking back over them is how much of the benefit comes down to the same underlying idea: moving creation logic out of the calling code and behind an abstraction. Whether that’s a director class, a factory, a clone method or a proxy, the payoff is the same – the client stops caring about construction details and starts dealing only with what it actually needs.

A few caveats worth carrying forward, too. Singleton in particular deserves a sceptical eye, and ICloneable has aged poorly – patterns aren’t immune to changing tastes, and knowing when not to reach for one is as valuable as knowing how they work.

As with the rest of this series, all of the source code is available in the accompanying design-patterns repository - each pattern is a standalone console application you can clone, run and step through in the debugger, which is honestly the best way I’ve found to make these patterns stick.

In the next post we’ll move on to the structural patterns – Adapter, Bridge, Composite, Decorator, Facade, Flyweight and Proxy – which deal with how objects are composed into larger structures.