Loading episodes…
0:00 0:00

Clone, Don't Recreate: A Visual Guide to the Prototype Pattern in Java

00:00
BACK TO HOME

Clone, Don't Recreate: A Visual Guide to the Prototype Pattern in Java

10xTeam December 11, 2025 12 min read

Have you ever found yourself stuck in a loop, writing nearly identical code to create slightly different versions of the same object? This common scenario, especially in systems that handle things like invoices, user profiles, or game entities, leads to bloated, brittle, and hard-to-maintain code.

Imagine a system that generates monthly invoices. You might have different templates: one for subscriptions, one for one-time consulting, and another for hosting services.

The Problem: Repetitive Construction Logic

Initially, you might handle this with a series of if-else statements or a switch block. For each customer, you check their invoice type and then manually construct a new Invoice object, setting properties like the header, footer, and tax rate.

// The "Before" Scenario: Manual, Repetitive Object Creation
for (Customer customer : customers) {
    Invoice invoice;
    if (customer.getType().equals("SUBSCRIPTION")) {
        invoice = new Invoice();
        invoice.setHeader("Default Company Header");
        invoice.setFooter("Default Company Footer");
        invoice.setTaxRate(0.15);
        invoice.setItems(List.of(new InvoiceItem("Monthly Subscription", 100.0)));
        // ... set customer-specific details
    } else if (customer.getType().equals("CUSTOM")) {
        invoice = new Invoice();
        invoice.setHeader("Default Company Header"); // Repetitive
        invoice.setFooter("Default Company Footer"); // Repetitive
        invoice.setTaxRate(0.18); // Slightly different, but still part of a template
        invoice.setItems(List.of(new InvoiceItem("Custom Development", 2000.0)));
        // ... set customer-specific details
    } else if (customer.getType().equals("HOSTING")) {
        invoice = new Invoice();
        invoice.setHeader("Default Company Header"); // Repetitive
        invoice.setFooter("Default Company Footer"); // Repetitive
        invoice.setTaxRate(0.15);
        invoice.setItems(List.of(new InvoiceItem("Hosting Service", 150.0)));
        // ... set customer-specific details
    }
    // ... more types
}

This approach has two major flaws:

  1. Code Duplication: The logic for setting the header, footer, and other shared attributes is repeated for every invoice type.
  2. Maintenance Nightmare: If the default header needs to be updated, you have to hunt down and change it in every single if block across your entire application. This is a violation of the DRY (Don’t Repeat Yourself) principle and is highly error-prone.

This is precisely the problem the Prototype design pattern solves.

What is the Prototype Pattern?

The Prototype pattern is a creational design pattern that lets you create new objects by copying an existing object, known as the “prototype,” rather than creating the object from scratch.

Formal Definition: “Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.”

Think of it like this: instead of rebuilding a complex robot from individual parts every time you need a new one, you build one master robot and then use a high-tech 3D scanner and replicator to clone it instantly.

Here’s a conceptual breakdown of the workflow:

graph TD;
    subgraph Manual Creation
        direction LR
        A[Start] --> B{Assemble Parts};
        B --> C{Configure Logic};
        C --> D[Final Object];
    end

    subgraph Prototype Pattern
        direction LR
        X[Get Prototype] --> Y{Clone()};
        Y --> Z[New Object Copy];
    end

    Client --> Manual_Creation;
    Client --> Prototype_Pattern;

    style Manual_Creation fill:#f9f,stroke:#333,stroke-width:2px
    style Prototype_Pattern fill:#9cf,stroke:#333,stroke-width:2px

The pattern delegates the cloning process to the actual objects being cloned. Let’s see how to implement this in Java.

Step 1: Define the Prototype Interface

First, we create a simple, generic interface. Any class that can be cloned will implement this interface.

// src/main/java/com/example/pattern/Prototype.java
package com.example.pattern;

/**
 * A generic interface for a cloneable object.
 * @param <T> The type of the object to be cloned.
 */
public interface Prototype<T> {
    T clone();
}

This interface defines a single clone() method that will return a new instance of its own type.

Step 2: Implement the Prototype in Concrete Classes

Now, we’ll modify our Invoice and InvoiceItem classes to be cloneable.

Let’s assume our project structure looks like this:

src/main/java/com/example/
├── domain/
│   ├── Invoice.java
│   └── InvoiceItem.java
├── pattern/
│   ├── Prototype.java
│   └── InvoiceRegistry.java
└── Main.java

We’ll start with InvoiceItem. It’s a simple class, so its clone() method will just create a new instance with the same property values.

// src/main/java/com/example/domain/InvoiceItem.java
package com.example.domain;

import com.example.pattern.Prototype;

public class InvoiceItem implements Prototype<InvoiceItem> {
    private String description;
    private double price;

    // Constructors, Getters, and Setters...

    @Override
    public InvoiceItem clone() {
        InvoiceItem clone = new InvoiceItem();
        clone.setDescription(this.description);
        clone.setPrice(this.price);
        return clone;
    }
}

Next, the Invoice class. This one is more complex because it contains a list of other objects (InvoiceItem). This brings up a critical concept: shallow vs. deep copy.

[!NOTE] Shallow vs. Deep Copy

  • A shallow copy copies the top-level fields. If a field is a reference to an object (like our List<InvoiceItem>), only the reference is copied, not the object itself. Both the original and the clone would point to the same list in memory. Modifying the list in the clone would also modify it in the original!
  • A deep copy copies all fields and also makes copies of any dynamically allocated memory pointed to by the fields. In our case, we need to create a new list and fill it with clones of each InvoiceItem.

Here’s how we implement a deep copy in the Invoice class’s clone() method.

// src/main/java/com/example/domain/Invoice.java
package com.example.domain;

import com.example.pattern.Prototype;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class Invoice implements Prototype<Invoice> {
    private String customerName;
    private String header;
    private String footer;
    private double taxRate;
    private String invoiceNumber;
    private java.util.Date date;
    private List<InvoiceItem> items;

    // Constructors, Getters, and Setters...

    public Invoice() {
        this.items = new ArrayList<>();
    }

    @Override
    public Invoice clone() {
        Invoice clonedInvoice = new Invoice();
        
        // Copy primitive and immutable types
        clonedInvoice.setHeader(this.header);
        clonedInvoice.setFooter(this.footer);
        clonedInvoice.setTaxRate(this.taxRate);

        // These will be set by the client after cloning
        clonedInvoice.setCustomerName(null);
        clonedInvoice.setInvoiceNumber(null);
        clonedInvoice.setDate(null);

        // --- DEEP COPY ---
        // Create a new list and fill it with clones of the original items.
        List<InvoiceItem> clonedItems = this.items.stream()
                                                   .map(InvoiceItem::clone)
                                                   .collect(Collectors.toList());
        clonedInvoice.setItems(clonedItems);

        return clonedInvoice;
    }
}

Step 3: Create a Prototype Registry

The “registry” is a central place to store and retrieve our pre-configured prototype objects. It acts as a cache of templates. We can implement this as a simple class with static methods.

// src/main/java/com/example/pattern/InvoiceRegistry.java
package com.example.pattern;

import com.example.domain.Invoice;
import com.example.domain.InvoiceItem;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

public class InvoiceRegistry {

    private static final Map<String, Invoice> PROTOTYPES = new HashMap<>();

    // Static initializer block to pre-load the prototypes
    static {
        // --- Subscription Prototype ---
        Invoice subscriptionPrototype = new Invoice();
        subscriptionPrototype.setHeader("Default Company Header");
        subscriptionPrototype.setFooter("Default Company Footer");
        subscriptionPrototype.setTaxRate(0.15);
        subscriptionPrototype.setItems(List.of(new InvoiceItem("Monthly Subscription", 100.0)));
        PROTOTYPES.put("SUBSCRIPTION", subscriptionPrototype);

        // --- Hosting Prototype ---
        Invoice hostingPrototype = new Invoice();
        hostingPrototype.setHeader("Default Company Header");
        hostingPrototype.setFooter("Default Company Footer");
        hostingPrototype.setTaxRate(0.15);
        hostingPrototype.setItems(List.of(new InvoiceItem("Hosting Service", 150.0)));
        PROTOTYPES.put("HOSTING", hostingPrototype);

        // --- Custom Development Prototype ---
        Invoice customDevPrototype = new Invoice();
        customDevPrototype.setHeader("Default Company Header");
        customDevPrototype.setFooter("Default Company Footer");
        customDevPrototype.setTaxRate(0.18);
        customDevPrototype.setItems(List.of(new InvoiceItem("Custom Development", 2000.0)));
        PROTOTYPES.put("CUSTOM", customDevPrototype);
    }

    public static Invoice getPrototype(String type) {
        try {
            return PROTOTYPES.get(type).clone();
        } catch (NullPointerException ex) {
            throw new IllegalArgumentException("Prototype with type '" + type + "' not found.");
        }
    }
}

[!TIP] Using a Map for the registry is more flexible than hardcoding static methods for each type. It allows you to add new prototypes at runtime without changing the registry’s code.

Step 4: Refactoring the Client Code

Now we can refactor our original client code. The difference is night and day.

Here’s the “before” and “after” using a diff format to highlight the change.

// The "After" Scenario: Clean, Scalable, and Maintainable
for (Customer customer : customers) {
-    Invoice invoice;
-    if (customer.getType().equals("SUBSCRIPTION")) {
-        invoice = new Invoice();
-        invoice.setHeader("Default Company Header");
-        invoice.setFooter("Default Company Footer");
-        invoice.setTaxRate(0.15);
-        invoice.setItems(List.of(new InvoiceItem("Monthly Subscription", 100.0)));
-    } else if (customer.getType().equals("CUSTOM")) {
-        // ... more repetitive code
-    }
+    // 1. Get a clone of the correct prototype from the registry
+    Invoice invoice = InvoiceRegistry.getPrototype(customer.getType());
+
+    // 2. Customize the clone with dynamic, customer-specific data
+    invoice.setCustomerName(customer.getName());
+    invoice.setInvoiceNumber(generateNextInvoiceNumber());
+    invoice.setDate(new java.util.Date());
+
+    // 3. The invoice is ready to be processed or sent!
+    System.out.println("Generated invoice for: " + invoice.getCustomerName());
}

The new code is incredibly clean. All the complex and repetitive construction logic is gone. The client simply asks the registry for a pre-built object, clones it, and then customizes the few properties that are unique to the current context.

If you need to change the header for all invoices, you only need to modify it in one place: the InvoiceRegistry.

Class Diagram Summary

Here is how the components fit together:

classDiagram
    direction LR
    class Client
    class Prototype~T~ {
        <<interface>>
        +T clone()
    }
    class Invoice {
        -String customerName
        -String header
        -List~InvoiceItem~ items
        +Invoice clone()
    }
    class InvoiceItem {
        -String description
        -double price
        +InvoiceItem clone()
    }
    class InvoiceRegistry {
        <<static>>
        -Map~String, Invoice~ PROTOTYPES
        +Invoice getPrototype(String type)
    }

    Client --> InvoiceRegistry : requests prototype
    Client --> Invoice : customizes clone
    InvoiceRegistry ..> Invoice : creates and clones
    
    Prototype <|.. Invoice
    Prototype <|.. InvoiceItem
    Invoice "1" *-- "many" InvoiceItem : contains

When to Use the Prototype Pattern

  • When object creation is expensive: If creating an object involves costly operations (like database calls, network requests, or heavy computation), cloning a pre-initialized object can be much faster.
  • When you need many similar objects: If your system needs to create numerous objects that only differ in their state, the Prototype pattern is a perfect fit.
  • To decouple the client from concrete classes: The client code works with the Prototype interface and doesn’t need to know the concrete class names, which are hidden away in the registry.

[!WARNING] Be mindful of the complexity of your objects. Implementing a correct deep copy can be tricky, especially for objects with complex internal graphs or circular references. In Java, java.lang.Cloneable and Object.clone() exist but are notoriously difficult to use correctly, which is why creating your own Prototype interface is often a safer and clearer approach.

By embracing the Prototype pattern, you can significantly reduce boilerplate, improve maintainability, and create a more flexible and scalable system.


Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?