Design Patterns

Creational Design Patterns

Singleton, Factory (simple factory and Factory Method), Builder, Abstract Factory and Prototype: the patterns that control how objects get created. Why a lazy Singleton breaks under two threads and which fixes are safe; why a simple factory is not Factory Method; how a Builder ends telescoping constructors and swapped arguments; how an Abstract Factory keeps families of objects consistent; and how a shallow Prototype leaks changes into its template. Every example runs in Java and Python, with verified output.

Suggest an edit

Creational Design Patterns — Controlling How Objects Are Made

new is the most concrete line of code there is. Every new EmailSender() or new RazorpayGateway() fixes, at that spot, exactly which class is used, how it is configured, and when it is created. Scatter those lines through a system and every change to what gets created becomes a change to the code that uses it.

Creational patterns move those decisions into one place. Each one answers a different question: how many instances may exist (Singleton), which class to create (Factory), how to assemble an object with many optional parts (Builder), which family of related objects to create (Abstract Factory), and whether to copy an existing object instead of building one (Prototype).

💡 The core idea.

  • Singleton guarantees one instance, but a lazy version is only correct if two threads can't both create it; it is also global state, which makes testing harder.
  • Factory puts the choice of concrete class in one place. A simple factory is one method with a conditional; Factory Method lets subclasses decide, so a new product is a new subclass rather than an edit.
  • Builder assembles an object step by step with named calls, so optional parts don't need telescoping constructors.
  • Abstract Factory creates whole families of objects that must match, such as a region's payment gateway and its invoice.
  • Prototype makes new objects by copying a configured one, and is only safe if the copy is deep enough.

This builds on Introduction to Design Patterns and SOLID Principles. Prototype relies on the copying rules from Relationships & Object Behaviour. Every output below was produced by running the code on Java 21 and Python 3.11, except where a block is marked as not runnable.

You'll be able to: write a Singleton that stays single under concurrent access, and say when not to use one; tell a simple factory from Factory Method, and say which one keeps existing code closed to edits; replace telescoping constructors with a Builder that validates and copies its inputs; use an Abstract Factory to stop mismatched families of objects; implement Prototype with a copy that is deep enough for the template's fields.

📘 How to read the Intuition boxes. Each one is built in three moves:

  1. The mechanism — what decision about creation the pattern moves, and where to.
  2. A concrete bite — a specific, runnable program where creating objects the naive way gives a wrong result.
  3. The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.

Table of contents

  1. Singleton pattern
  2. Factory pattern
  3. Builder pattern
  4. Abstract Factory pattern
  5. Prototype pattern
  6. Mental-model summary
  7. Gotcha checklist
  8. Check yourself
  9. Sources

1. Singleton pattern

📘 Definition. "Ensure a class only has one instance, and provide a global point of access to it" [1].

In simpler terms. Sometimes you want exactly one shared object for the whole life of the program. Singleton restricts object creation, so that every part of the application gets the same object.

The problem it solves. Usually, creating several objects of a class does no harm. But for some things, such as a logger, the application's configuration, or a pool of database connections, several instances waste resources or behave inconsistently.

Real-world analogy: the print spooler. In an office, everyone sends documents to one shared printer. If each computer talked to the printer directly, on its own terms, the jobs would get jumbled or overlap, and the printer might crash.

💡 Insight. Instead, there's a print spooler: a background service that manages all print jobs.

Whoever starts the print, the job goes through the one spooler, which queues the jobs and handles them in order.

Why is it a creational pattern? Because it controls how objects are created. Instead of letting callers use new, it hands back an existing instance.

Identifying the need. Imagine a logging service that writes to a file. If every part of the application creates its own logger, you can get overwritten log lines, many open handles to the same file, and synchronisation problems. With one logger instance, every part of the program writes to the file through the same object, in a controlled way.

How it works. A Singleton usually has three parts:

  • A private constructor, so no other class can call new.
  • A static field that holds the single instance.
  • A public static method that returns that instance.

However many times the method is called, it returns the same object. There are two main ways to decide when that object is created, each with its own trade-offs in memory, start-up time and thread safety.

1. Eager loading (early initialisation). The instance is created as soon as the class is loaded, whether or not it is ever used. Think of a fire extinguisher: it is always there, even if there is never a fire.

2. Lazy loading (on-demand initialisation). The instance is created only when it is first needed, the first time getInstance() is called. Think of a coffee machine that brews only when you press the button, and uses no energy until then.

Output (Java):

main started
EagerSingleton constructed
eager: same instance? true
LazySingleton constructed
lazy: same instance? true

Output (Python):

EagerSingleton constructed
main started
eager: same instance? true
LazySingleton constructed
lazy: same instance? true

Analysis. Each class handed back one object however many times it was asked. The order of the first two lines differs, and that difference is instructive. Java initialises a class, and so runs EagerSingleton's static field initialiser, only when the class is first used [3], which here is after main started. Python runs EagerSingleton() as soon as the module reaches that line, before the rest of the program. Both lazy versions construct on the first get_instance() call.

Eager loading's properties: the object exists as soon as the class is initialised, it is always available, and it is thread-safe because the JVM initialises a class only once. It is very simple and needs no extra handling for threads, but it creates the instance even if it is never used, which matters for heavy objects. Lazy loading's properties: the instance starts as null, is created on the first call, and is returned from then on. It saves the cost of an instance nobody uses, but it is not thread-safe as written.

Intuition. Mechanism. Lazy initialisation is a check-then-act sequence: test whether the instance exists, then create it. If two threads run the check before either runs the act, both see null, and both create an instance.

Concrete bite. Most of the time the two threads don't line up, so the bug hides. This program forces the unlucky timing on every run, by making each thread wait at a barrier after its null check, until the other has also passed it:

Output:

instances created: 2

The barrier only makes the timing reliable; without it the same thing happens occasionally, under load, in production. A bug like this is hard to reproduce, severe (it defeats the whole point of the pattern), and costly when the Singleton guards something like a connection pool or a configuration that must not be loaded twice.

There are several ways to make a lazy Singleton thread-safe:

  • Synchronized method. Mark getInstance() as synchronized, so only one thread at a time can run it. It is simple and obviously correct, but every call takes the lock, even long after the instance exists, which can slow down code that calls it often from many threads.
  • Double-checked locking. Check for null without the lock; only if it is null, take the lock and check again before creating. The lock is taken only around the first creation. The field must be volatile. Without it, the Java memory model allows another thread to see the reference to the new object before the object's constructor has finished, and so use a half-built Singleton; volatile guarantees that the write of a fully constructed object happens before any thread reads it [3]. This works from Java 5 onwards, when the memory model was revised. It is efficient, but slightly more complex.
  • Initialisation-on-demand holder ("Bill Pugh"). Put the instance in a static nested Holder class. The JVM initialises Holder only the first time getInstance() touches it, and class initialisation is guaranteed to happen exactly once, even with many threads [3]. It is lazy and thread-safe with no synchronized or volatile, though the nested class is less obvious to beginners.
  • Eager loading, as above, avoids the problem by creating the instance up front, at the cost of creating it even when it isn't used.
  • A single-element enum. Bloch recommends it: "a single-element enum type is often the best way to implement a singleton" [2]. The JVM guarantees one instance, and it also survives serialisation and reflection attacks that can create second instances of the other forms.

Here all four thread-safe Java forms, and the two idiomatic Python ones, are hammered by eight threads at once:

Output (Java):

synchronized method: instances created = 1
double-checked locking: instances created = 1
holder class: instances created = 1
enum: instances created = 1

Output (Python):

double-checked locking: instances created = 1
module-level object: instances created = 1

Python doesn't need five answers to this, because the Java variants are really about one problem: JVM class loading and thread visibility. In Python, a module-level object is the idiomatic Singleton, and a lock covers the cases where you need a class.

💡 Earned rule. If you truly need one instance in Java, use an enum or the holder class: both are lazy enough, thread-safe, and have no locking code to get wrong. In Python, use a module-level object. Never write an unsynchronised lazy getInstance() in code that might run on more than one thread.

The bigger cost is the pattern itself: a Singleton is global state that every caller reaches by name. Prefer creating one instance at start-up and passing it in (dependency injection), which gives you "one instance" without the global access point, and lets tests pass a fake.

Pros of the Singleton pattern.

  • Simple to use: a clear, tidy way to manage a single shared instance, especially in a thread-safe form.
  • Guarantees one instance: only one object of the class can exist, which suits shared resources.
  • One global access point: centralised access to an application-wide resource, such as configuration.
  • Supports lazy loading: many implementations create the instance only when it is first used, saving memory and start-up time.

Cons of the Singleton pattern.

  • Confused with Factory when it takes parameters: if creating the instance needs arguments, which call's arguments win? That blurs the line with the Factory pattern and confuses the design.
  • Hard to unit test: it holds global state that tests share and can't easily replace with a fake.
  • Callers are tightly coupled to it: every class that calls getInstance() depends on that concrete class, which makes the code harder to change.
  • Race conditions: in multithreaded code, creation must be guarded, which complicates the implementation, as shown above.
  • Two jobs in one class: a Singleton controls its own instance count and does its real work, which goes against the Single Responsibility Principle.

Conclusion. Singleton is useful for genuinely shared resources, but its global state hurts testing and maintenance. Consider creating one instance and injecting it instead, which Dependency Injection & Error Handling covers.

The class diagram below shows the holder-class Singleton: the private constructor, and the static nested Holder class that lazily provides the single instance.


2. Factory pattern

There are two related ideas under the name "factory", and it matters which one you mean.

📘 Definition. A simple factory is a method, often static, that takes some input and returns one of several concrete classes behind a common interface. It is a common idiom, but not one of the 23 GoF patterns. The GoF pattern is Factory Method: "Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses" [1].

In simpler terms. Rather than calling a constructor directly, the code that needs an object asks a factory for one, based on some input or condition, and works only with the interface it gets back.

When should you use it?

  • The client code needs to work with several types of object through one interface.
  • Which class to create must be decided at run time.
  • Creating the object is complex, or needs to be controlled in one place.

Real-world analogy: ordering pizza. In a pizza shop you say "I'd like a pizza", and the shop asks "Which type? Margherita, pepperoni or veggie?" The kitchen, the factory, makes the pizza you chose and hands it over. You, the client, don't care how it's made or which ingredients went into it.

💡 Insight. That is what a factory does in code: it creates an object based on some input, without exposing the instantiation logic to the client.

Basic structure. A factory has three parts:

  • Product: an interface or abstract class that every product implements.
  • Concrete products: the classes that implement it.
  • Factory: a method that returns one of the concrete products, based on its input.

Output:

Drawing Circle
Drawing Square

ShapeFactory returns different objects (Circle, Square) based on its input, and the caller only ever sees Shape. An unknown type throws an exception rather than returning null, so a typo fails at once instead of as a NullPointerException somewhere later.

Real-life example: logistics services

A logistics application sends parcels by different modes of transport: by road, by air, and so on. Here is a first version, with the creation logic mixed into the business logic:

Output:

Sending by air logic
Sending by road logic
all three parcels handed over

The issue. In LogisticsService, the Air or Road object is created directly, based on a string comparison, inside the send method. That causes several problems:

  • Tight coupling: LogisticsService depends directly on the Air and Road classes.
  • Hard to extend: adding a mode, such as Ship or Drone, means modifying send, which violates the Open/Closed Principle.
  • No separation of concerns: creating objects and using them are mixed together.
  • Duplication: the create-then-send code is repeated in each branch.
  • Hard to test and maintain: you can't test the service with a fake transport.

A simple factory. Moving the creation into a factory separates the two concerns:

Output:

Sending by air logic
Sending by road logic
refused: Unknown logistics mode: Ship

Creation now lives in LogisticsFactory, and LogisticsService deals only with sending. That brings real benefits: the service is decoupled from the concrete classes, creation is in one place instead of repeated, and each part can be tested on its own. But be precise about the Open/Closed Principle: adding Ship now leaves LogisticsService untouched, yet it still means editing LogisticsFactory. The service is closed for modification; the factory is not.

Factory Method. The GoF pattern removes that last edit. The class that uses the product declares an abstract factory method, and each subclass overrides it to create its own product. Adding a mode is then a new subclass, and no existing class changes:

Output:

Sending by air logic
Sending by road logic
Sending by sea logic

Analysis. All three versions sent the same parcels, but they differ in where a new mode lands. In the first, Ship silently did nothing. In the simple factory, it was refused with a clear error, and supporting it means one edit to the factory. With Factory Method, Ship was added as two new classes, and LogisticsService, AirLogisticsService and RoadLogisticsService were not touched. The send() method in the abstract creator is the business logic that stays closed; createLogistics() is the one step each subclass opens up. (Mermaid can't italicise a class name, so LogisticsService is labelled «abstract»; the italic createLogistics() is the abstract operation.)

Intuition. Mechanism. Both kinds of factory move new out of the business logic. A simple factory gathers every new into one method, which is easy to find and change, but must be edited for each new product. Factory Method distributes them across subclasses, so each new product comes with its own creator class, and existing classes stay closed. That is the trade: one place to edit, or no place to edit but a class per product.

Concrete bite. In the first program, send("Ship") printed nothing, and the program went on to report "all three parcels handed over". The if/else if chain had no final else, so an unsupported mode fell through silently, and a parcel was lost without an error. Moving creation into a factory gives you one natural place to refuse unknown input loudly.

💡 Earned rule. Move new out of business logic as soon as the choice of class depends on input. Start with a simple factory (or a map from name to constructor) that throws on unknown input. Switch to Factory Method when new products keep arriving and each one should be added without editing shared code, or when each creator subclass already exists for other reasons.

The cost of a simple factory is one class and a conditional that grows. The cost of Factory Method is a creator subclass for every product, which is a lot of classes if all they do is call new.

Pros of the factory patterns.

  • Loose coupling: client code is decoupled from the concrete classes it uses, and works with interfaces instead.
  • Extensibility (Open/Closed Principle): new products can be added without modifying the client code. With Factory Method, the creators that already exist don't change either.
  • Centralised creation (Single Responsibility Principle): creating objects becomes a separate job, and business logic stays focused on what to do with them.
  • Flexibility: which object to create can be decided at run time, from input, configuration or logic.
  • Reuse: the creation logic lives in one place, instead of being repeated wherever an object is needed.

Cons of the factory patterns.

  • More complexity: extra layers (factory classes, interfaces, creator subclasses) can be overkill in a small program.
  • More code: factories and interfaces can look unnecessary in simple cases where one new would do.

3. Builder pattern

📘 Definition. "Separate the construction of a complex object from its representation so that the same construction process can create different representations" [1]. In the GoF version, a director drives one of several builders through the same steps to produce different results. The form most Java code uses today, shown below, is Bloch's: a nested builder class with one method per optional part, and a final build() [2].

In simpler terms. Ordering a custom burger, you choose the bun, the patty, the toppings and the sauces, and whether you want it grilled or toasted. The chef builds it step by step from your choices. The Builder pattern lets you construct a complex object the same way, by specifying its parts one at a time.

Real-life analogy: a custom pizza order. Ordering a pizza online, you choose the crust, the size, the toppings, the cheese and the sauce, step by step, and the shop builds the pizza from those choices. Different customers use the same process and get entirely different pizzas.

Understanding the problem. A BurgerMeal in an application has mandatory parts, a bun and a patty, and optional ones: sides, toppings and cheese. A single constructor for all of them looks like this:

Output:

BurgerMeal{bun=wheat, patty=veg, sides=null, toppings=null, cheese=false}

This works, but it causes several problems:

  • Hard to read and maintain: callers must remember the order and type of every parameter, and it gets worse with each optional part added.
  • Needless nulls: a caller who wants no toppings or sides still has to pass null explicitly.
  • Risk of NullPointerException: any code inside the class that forgets to check the optional fields for null can crash.
  • Too many constructor overloads: covering the combinations (with cheese, without sides, toppings only) needs many constructors, which doesn't scale.
  • All in one go: the whole object must be specified in a single call, which doesn't match the step-by-step way people actually order.

The telescoping constructor anti-pattern. A common attempt at a fix is a chain of overloaded constructors, each with one more optional parameter than the last:

Output:

BurgerMeal{bun=wheat, patty=veg, cheese=false, side=null, drink=null}
BurgerMeal{bun=wheat, patty=veg, cheese=true, side=null, drink=null}
BurgerMeal{bun=wheat, patty=veg, cheese=true, side=fries, drink=null}
BurgerMeal{bun=multigrain, patty=chicken, cheese=true, side=fries, drink=coke}
BurgerMeal{bun=wheat, patty=veg, cheese=true, side=coke, drink=null}

This cascade of constructors is hard to read and write, error-prone because of the parameter order, hard to maintain as fields are added, and inflexible, because callers must give parameters in one fixed order. It is most common in Java, which has no optional or default parameters (C++ and Python do), so the only way to offer combinations is overloading. That is exactly the problem the Builder pattern solves: it gives the caller full control over which parts to set, and keeps construction readable and safe.

The solution. The builder separates construction from the finished object. Each optional part has its own named method, and the object is created, complete and immutable, by build():

Output (Java):

BurgerMeal{bunType=wheat, patty=veg, hasCheese=false, toppings=[], side=null, drink=null}
BurgerMeal{bunType=wheat, patty=veg, hasCheese=true, toppings=[], side=null, drink=null}
BurgerMeal{bunType=multigrain, patty=chicken, hasCheese=true, toppings=[lettuce, onion, jalapeno], side=fries, drink=coke}
BurgerMeal{bunType=wheat, patty=veg, hasCheese=true, toppings=[], side=null, drink=coke}

Output (Python):

BurgerMeal{bunType=wheat, patty=veg, hasCheese=false, toppings=[], side=null, drink=null}
BurgerMeal{bunType=wheat, patty=veg, hasCheese=true, toppings=[], side=null, drink=null}
BurgerMeal{bunType=multigrain, patty=chicken, hasCheese=true, toppings=[lettuce, onion, jalapeno], side=fries, drink=coke}
BurgerMeal{bunType=wheat, patty=veg, hasCheese=true, toppings=[], side=null, drink=coke}
coke None

Understanding the code.

  • Private constructor: BurgerMeal's constructor is private, so a meal can only be created through the builder.
  • Nested static BurgerBuilder class: the builder holds the same fields as the meal while it is being configured; the meal itself is immutable once built.
  • Fluent API: each withXyz method returns the builder itself, so calls can be chained readably.
  • Selective configuration: only the required parts (bunType, patty) go in the builder's constructor. Everything else is optional, with a default.
  • The final step, build(): it checks the whole order once, and returns the finished BurgerMeal. The meal copies the toppings list, so changing the caller's list afterwards can't change a meal that has already been built.

In Python, keyword arguments and @dataclass defaults remove most of the problem the Builder pattern solves in Java. Use a builder in Python when construction needs validation or several steps that defaults can't express.

Analysis. Every withXyz call names the part it sets, so the coke went into drink and nowhere else, and parts that weren't set kept safe defaults (an empty toppings list instead of null). In the telescoping version, the last call compiled and ran, and put the coke in the side field, because the four-argument constructor's fourth parameter is the side, and both are Strings.

Intuition. Mechanism. A constructor identifies arguments by position; a builder identifies them by method name. When several parameters share a type, position is the only thing distinguishing them, and the compiler can't tell a swapped pair from a correct one. A builder also gives you one place, build(), to validate the combination of parts before the object exists, and to copy anything mutable.

Concrete bite. new BurgerMeal("wheat", "veg", true, "coke") printed side=coke, drink=null. Nothing failed: the customer simply got a coke in place of fries and no drink. With the builder, .withDrink("coke") can only mean the drink.

💡 Earned rule. In Java, use a builder when a class has more than three or four constructor parameters, several of them optional, or several of the same type. Put the required parts in the builder's constructor, validate in build(), and copy mutable inputs there. In Python, use keyword arguments (or a dataclass) first, and a builder only when construction needs steps or validation.

The cost is a second class that mirrors the first, which must be kept in step whenever a field is added. For a class with two fields, a constructor is simpler.

Why this is better.

Aspect Constructor approach Builder pattern
Readability poor: nulls and long argument lists good: each part is named
Flexibility low: everything in one call high: set only what you need
Maintainability hard to scale as fields are added easy to add options
Safety swapped or null arguments compile fine parts are named, and build() validates

When to use it.

  • An object has many fields, especially many optional ones, which makes constructors messy and error-prone.
  • You want the finished object to be immutable: the builder collects the parts step by step, and the object is fixed once built.
  • You want readable object creation, for example for domain models or configuration objects, where the fluent style makes the intent clear.

When to avoid it.

  • The class has only one or two fields: a constructor is simpler and shorter.
  • You don't need customisation or immutability: for a small, mutable object built in one place, a builder adds needless complexity.

Pros of the Builder pattern.

  • No telescoping constructors: one builder replaces the cascade of overloads.
  • Immutability: the finished object can be immutable, which makes it safer, including when shared between threads.
  • Readable creation: the fluent API makes construction expressive and easy to follow.
  • Good for complex configuration: objects with many optional or conditional parts stay organised.

Cons of the Builder pattern.

  • More set-up: you have to write a separate builder class, which adds boilerplate.
  • Overkill for small classes: for one or two fields, a builder adds needless complexity.
  • A second class to maintain: the builder must be updated alongside the class it builds.

Real-world products using the Builder pattern

1. Lombok's @Builder annotation (Java). Lombok is a Java library that generates boilerplate code from annotations. Its @Builder annotation generates a builder class for you, so instead of writing the builder by hand you annotate the class:

⚠️ Watch out. These two snippets need Lombok on the classpath to compile, so they are not runnable here. The hand-written equivalent below them is the runnable version.

// requires Lombok: not runnable without the annotation processor
import lombok.Builder;
 
@Builder
class User {
    private String name;
    private int age;
    private String address;
}

Objects are then built with a fluent API:

// requires Lombok: uses the @Builder-generated User above
User user = User.builder()
            .name("John")
            .age(30)
            .address("NYC")
            .build();

Here is the same builder written by hand, so you can see what Lombok generates and run it without the annotation processor:

Output:

User{name=John, age=30, address=NYC}

2. A shopping-cart item. Think about the item in an online shopping cart. Adding it stores more than a product ID: a quantity, a size or colour, a delivery option, gift wrapping, a "save for later" flag, a discounted price or offer tag. Each customer sets these differently. A builder suits an object like this: it is configured step by step, and should be consistent and immutable once added to the cart.

The class diagram below shows the structure of the BurgerMeal builder.


4. Abstract Factory pattern

📘 Definition. "Provide an interface for creating families of related or dependent objects without specifying their concrete classes" [1].

In simpler terms. You use it when you have several factories, each producing a set of objects that are meant to work together.

When should you use it?

  • Several related objects must be created as one consistent set, such as a payment gateway and its matching invoice generator.
  • Which objects to create depends on a context, such as country, theme or platform.
  • Client code should not depend on the concrete product classes.
  • The products in a family must stay consistent, for example a US payment gateway always paired with a US-style invoice.

Real-life example: a checkout service

A checkout service takes a payment and generates an invoice. The first version creates both itself:

Output:

Processing INR payment via Razorpay: 1500.0
Generating GST Invoice for India.
---
Processing INR payment via PayU: 49.99
Generating GST Invoice for India.

Issues with this design.

  • Tight coupling: CheckoutService creates RazorpayGateway, PayUGateway and GSTInvoice itself, so it depends on each of them.
  • Violates the Open/Closed Principle: every new gateway or invoice type means modifying CheckoutService.
  • Not extensible: the hard-coded choices can't support other countries, or other combinations of payment method and invoice.

The improved design. The Abstract Factory pattern separates creating the gateway and the invoice from using them. A RegionFactory interface declares one creation method per product, and each region has a concrete factory that returns matching products:

Output:

Processing INR payment via Razorpay: 1500.0
Generating GST Invoice for India.
---
Processing USD payment via Stripe: 49.99
Generating Invoice as per US norms.
---
refused: Unsupported gateway for India: stripe

How this fixes the original issues.

  • Creation was mixed with business logic: it now lives in the factory classes, IndiaFactory and USFactory.
  • Concrete classes were hard-coded in the service: the service now uses only the PaymentGateway and Invoice interfaces.
  • New gateways or invoices meant modifying CheckoutService: a new region is now a new factory, with no changes to the service.
  • The code was hard to scale across regions: each region's products are plugged in through its own factory.

The benefits: the design scales by adding factories; CheckoutService doesn't care which gateway or invoice it uses; each factory can be tested on its own; and it follows the Open/Closed and Dependency Inversion principles.

Analysis. The US customer is now charged in dollars through Stripe and gets a US invoice. The factory makes the pair of products, so the service can't end up with an Indian invoice and a US gateway. An unsupported combination, Stripe in India, is refused when the service is created, before any money moves.

Intuition. Mechanism. An abstract factory groups the creation methods of products that must match into one object. Choosing the factory chooses the whole family at once. Code that receives a RegionFactory can create any product of the family, and can only get products that belong together.

Concrete bite. In the first program, the US customer's "stripe" fell into the else branch, so they were charged through PayU, an Indian gateway, in what the log calls INR, and then sent a GST invoice for India. Each choice was made separately, by its own line of code, so nothing checked that they matched.

💡 Earned rule. When two or more objects must always be chosen together (a region's gateway, invoice and tax rules; a UI theme's buttons and dialogs), create them through one factory per family, and pass the factory in. Then a mismatched set can't be built.

The cost shows when the family grows: adding a new kind of product (say a ReceiptPrinter) means adding a method to the interface and to every concrete factory. Use the pattern when families change more often than the list of products in them.

Pros of the Abstract Factory pattern.

  • Encapsulates creation: creating related objects is centralised and hidden, which keeps client code focused on behaviour.
  • Consistency across products: related objects, such as payment modules or UI components, are always used together correctly.
  • Scalability: new families or regions are new factory classes, with no change to existing logic.
  • Open/Closed Principle: open for new factories and products, closed for modification of the code that uses them.
  • Maintainability: less coupling between components and concrete classes makes each part easier to change, test and debug.
  • A layer of abstraction: platform- or environment-specific details are hidden from the client, which makes it more portable.

Cons of the Abstract Factory pattern.

  • More complexity: interfaces, factories and product families can be overkill for small projects.
  • Hard to add a new kind of product: adding a product to the family means updating every factory.
  • More boilerplate: several classes and interfaces even for basic cases.
  • The family is fixed per factory: a factory is usually chosen once, when the service is created, so switching families in the middle of an operation takes extra design.

The class diagram below shows how the parts of the Abstract Factory fit together.


5. Prototype pattern

📘 Definition. "Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype" [1].

In simpler terms. You already have a fully set-up object, such as a well-written email template or a configured game character. Instead of building a new one from scratch every time, which can be repetitive and expensive, you copy the existing one and make small changes.

Real-life analogy: the photocopier. To prepare ten offer letters, you don't type the same letter ten times. You write it once, photocopy it, and change the name on each copy.

💡 Insight. That is how the Prototype pattern works: start from a base object, and produce copies with small changes.

Understanding. In an email notification system, setting up each email can involve loading templates, configuration, user settings and formatting. Building every email from scratch repeats that work. With a pre-configured prototype email, you clone it for each user and change a few fields, such as the name or the content. That saves time, reduces mistakes, and keeps the code simpler.

Suitable use cases.

  • Creating the object is expensive or complex.
  • You need many similar objects with small differences.
  • You want to avoid repeating initialisation code.
  • You need to create objects at run time without depending on their concrete classes.

Real-life example: an email template system

The first version builds every email with new and then changes it:

Output:

Sending to [email protected]: [Welcome to the platform] Hi there! Thanks for joining us.
Sending to [email protected]: [Welcome to the platform] Hi there! Welcome to the premium plan.
Sending to [email protected]: [Welcome to the platform] Thanks for signing up. Let's get started!

Issues in this design.

  • Tight coupling to a concrete class: the client uses WelcomeEmail directly, with new WelcomeEmail() everywhere, and there is no abstraction for copying.
  • Repeated initialisation: every variation runs the constructor again, even though most of the data is the same. With a real template that loads files or settings, that work is repeated each time.
  • Repetition (DRY): the same new plus setContent sequence is repeated for every small variation.
  • No copying mechanism: there is no way to reuse a prepared template and change just a few parts.

The Prototype pattern applied. The template can clone itself, and a registry holds the configured prototypes and hands out copies:

Output:

Sending to [email protected]: [Welcome to the platform] Hi Alice, welcome to the premium plan!
Sending to [email protected]: [Welcome to the platform] Hi Bob, thanks for joining!
Sending to [email protected]: [Welcome to the platform] Hi there! Thanks for joining us.

The benefits of this design: clone() copies an object instead of recreating it; the registry, EmailTemplateRegistry, is one central place that holds the prototypes; client code doesn't depend on how WelcomeEmail is constructed; and an expensive set-up runs once, for the prototype, not for every email. Python's standard library has copying built in: copy.copy is the equivalent of Java's shallow Object.clone(), and copy.deepcopy is what you use instead of writing a deep clone by hand.

Analysis. Alice's and Bob's emails each changed only their own copy, and Carol, who got a fresh copy of the untouched prototype, received the default text. That works because WelcomeEmail's only fields are immutable strings, so a shallow copy shares nothing that can change.

Intuition. Mechanism. A prototype registry is a factory whose "recipe" is an object rather than a class. The quality of every object it hands out depends on one thing: how deep clone() copies. Anything the clone shares with the prototype is shared with every clone, past and future.

Concrete bite. Give the template a mutable field, a list of CC addresses, and keep the shallow clone():

Output:

next shallow clone cc: [[email protected], [email protected]]
next deep clone cc:    [[email protected]]

Adding Alice's manager to one email's CC list added them to the prototype, so every email created afterwards would copy Alice's manager in. The deep clone gave each copy its own list.

💡 Earned rule. Use Prototype when configuring an object is expensive or the set of configured variants is decided at run time. Make clone() copy every mutable field (or use copy.deepcopy), keep prototypes inside a registry that only hands out copies, and prefer immutable fields so there is less to copy.

The cost is that every new mutable field must be added to clone(), and forgetting one creates a bug that leaks between unrelated objects. If construction is cheap, a constructor or a factory is simpler.

Deep cloning vs shallow cloning. For the Prototype pattern, deep cloning is usually preferred: copying the object and the mutable objects it refers to, so that changes to a clone never affect the prototype or other clones. It is the safer choice when templates contain internal structures such as lists or nested configuration objects. Relationships & Object Behaviour covers shallow and deep copies, and copy constructors, in detail.

Pros of the Prototype pattern.

  • Cheaper creation when set-up is expensive: a costly initialisation runs once, for the prototype.
  • Fewer subclasses: variations can be configured prototypes instead of new subclasses.
  • Run-time configuration: prototypes can be added to the registry, and clones adjusted, while the program runs.
  • Good for copying structures: useful for duplicating UI component trees or screen states.

Cons of the Prototype pattern.

  • Deep cloning can be hard: a correct deep copy takes extra effort, and must be kept up to date as fields are added.
  • Circular references: objects that refer to each other are tricky to copy by hand (Python's copy.deepcopy handles them).
  • Subtle bugs: a clone that shares too much leaks changes between objects, as the CC example showed.

The class diagram below shows how the parts of the Prototype pattern fit together.


6. Mental-model summary

Principle Consequence
Creational patterns move new out of business logic Changing what gets created doesn't change the code that uses it
Singleton: one instance, global access A lazy version needs a holder class, an enum or locking; prefer injecting one instance
Simple factory: one method chooses the class Easy to find and change, but edited for every new product
Factory Method: subclasses choose the class New products are new subclasses; existing creators stay closed
Builder: named steps, then build() No telescoping constructors or swapped arguments; validate and copy in build()
Abstract Factory: one factory per family Products that must match always match; adding a product kind touches every factory
Prototype: copy a configured object Only as safe as clone() is deep

7. Gotcha checklist

Symptom Likely cause Fix
Two "singletons" exist under load unsynchronised lazy getInstance() holder class, enum, or double-checked locking with volatile
Tests interfere with each other shared Singleton state create one instance and inject it
An unknown type silently does nothing, or returns null a conditional with no final branch throw on unknown input in the factory
"Factory Method" that is a static if/else a simple factory mislabelled call it a simple factory, or use subclass creators
A value lands in the wrong field telescoping constructor with same-typed parameters a builder, or named arguments in Python
A built object changes after build() the builder stored the caller's mutable list copy mutable inputs in build() or the constructor
A US order with an Indian invoice each product chosen by separate code one abstract factory per family
Every new clone carries an old change shallow clone() with mutable fields deep-copy mutable fields in clone()

✅ Check yourself

One check per objective. Answer before you open anything.

A game creates enemies. Each level has a theme (forest, desert), and each theme has its own enemy, weapon and terrain classes that must match. Setting up an enemy loads expensive sprite data, and a level spawns hundreds of nearly identical enemies. Which creational patterns would you combine, and how?

Use an Abstract Factory per theme (ForestFactory, DesertFactory, each creating an enemy, a weapon and terrain), so a forest level can never get desert weapons. Inside each factory, use Prototype for enemies: load the sprite data once into a configured prototype enemy, and have the factory return clones, adjusting only position and health, so hundreds of enemies don't each reload sprites. Make the clone share the sprite data deliberately (it is immutable and large) and copy the mutable state (position, health). If enemies have many optional attributes (armour, speed, drops), a Builder can configure the prototype itself. Avoid a Singleton for the factory: pass the level's factory into the code that spawns enemies.


📚 Sources

  1. Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994), ch. 3 "Creational Patterns": Abstract Factory, Builder, Factory Method, Prototype, Singleton.
  2. Joshua Bloch, Effective Java, 3rd edition (Addison-Wesley, 2018), Item 2 "Consider a builder when faced with many constructor parameters", Item 3 "Enforce the singleton property with a private constructor or an enum type", Item 13 "Override clone judiciously".
  3. The Java Language Specification, Java SE 21, §12.4 "Initialization of Classes and Interfaces" (§12.4.1, §12.4.2) and §17.4 "Memory Model" — https://docs.oracle.com/javase/specs/jls/se21/html/jls-12.html#jls-12.4
  4. Python 3 documentation, copy and threading — https://docs.python.org/3/library/copy.html, https://docs.python.org/3/library/threading.html

🧪 Predict, then check.

  1. In the Python Singleton in section 1, add an __init__ to LazySingleton that sets self.value = "default". Run a = LazySingleton(); a.value = "changed"; LazySingleton() and predict what a.value is afterwards.
  2. In the race demo in section 1, add the synchronized keyword to getInstance() but keep the barrier. Predict what happens when you run it.
  3. In the Factory Method program in section 2, add a DroneLogisticsService. Predict how many existing lines you have to edit.
  4. In the Builder program in section 3, build a meal with .withToppings(list) where list is an ArrayList, then add "pickles" to list and print the meal. Predict whether the pickles appear.

Your Turn

Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.

Mark as readBook versionff97b7f