Mediumcreational16 min read

Prototype Design Pattern

1. What is Prototype Pattern?

Definition

The Prototype pattern is a GOF Creational Pattern. It creates a copy of an existing object through cloning, not from scratch level. According to GOF: "Specify the kind of objects to create using a prototypical instance, and create new objects by copying this prototype."

One line: Instead of creating objects from scratch (costly), clone existing cached objects to get new copies quickly.

Flyweight vs Prototype

AspectFlyweightPrototype
What it returnsThe SAME cached object (shared reference)A CLONE/COPY of the cached object (new instance)
Object ownershipShared among all clientsEach client gets its own independent copy
Modification safe?No (modifying affects everyone)Yes (each clone is independent)
Use whenObjects are immutable/read-onlyEach client needs a modifiable copy

2. The Problem

Problem

Creating new objects from scratch is a costly affair in Java. That needs more memory and CPU time. If we want certain bunch of objects to be created for multiple times, creating them every time from scratch level degrades the performance.

Problem: Every request creates objects from scratch EndUser 1 DB Server (ClientApp1) ---[1a]---> BookStore App ---[1b]---> Books info (db table) (buffer/cache) bookId bookName price EndUser 2 | List of [1c] 1001 C8J 9000 (ClientApp2) ---[2a]---> book objs <---------- 1002 BB 8000 | [2a] new objs from scratch! Every time hitting DB s/w and every time creating same bunch of objects is not really good practice. It will degrade the performance of the Application.

3. The Solution: Clone from Cache

Solution

Use Prototype Design Pattern which says: keep the objects that are created through costly process in cache memory and use cloning concept to create copy of objects from those cached objects.

Implementing Prototype Design pattern is nothing but creating bunch of objects through cloning process repeatedly by keeping the actual objects in the cache.

Solution: Clone from cached prototypes EndUser 1 buffer/cache DB Server (ClientApp1) ---[1a]---> [Book objs] <---[1b]--- Books info (db table) cached once! EndUser 2 | (ClientApp2) ---[2a]---> [2b] clone objs from cache (fast! no DB hit) First call: loads from DB, stores in cache Second call: clones from cache (no DB call)
Without cloning: With cloning: Student obj1 Student obj2 Student obj1 Student obj2 (Freshly (Another freshly (Freshly = (Student) obj1.clone() created created same created (object created object) object) object) through cloning process) Both created from scratch. obj2 is a copy of obj1. Costly: constructor + DB call. Cheap: memory copy only.

Four Components

ComponentRole
a) Prototype InterfaceDeclares the clone method (Cloneable interface in Java)
b) Concrete PrototypeImplements cloning. The actual object that gets cached and cloned.
c) Prototype Registry/FactoryMaintains a HashMap cache of prototype objects. Returns clones on request.
d) ClientGets cloned objects from the registry instead of creating new ones.

4. BookStore Example (from course notes)

Book.java (Concrete Prototype)

package com.nt.model;

public class Book implements Cloneable {
    private Integer bookId;
    private String bookName;
    private String bookType;
    private String bookAuthor;
    private double price;
    private int qty;

    public Book() {
        System.out.println("Book:: 0-param constructor (" + this.hashCode() + ")");
    }

    // Getters and setters
    public Integer getBookId() { return bookId; }
    public void setBookId(Integer bookId) { this.bookId = bookId; }
    public String getBookName() { return bookName; }
    public void setBookName(String bookName) { this.bookName = bookName; }
    public String getBookType() { return bookType; }
    public void setBookType(String bookType) { this.bookType = bookType; }
    public String getBookAuthor() { return bookAuthor; }
    public void setBookAuthor(String bookAuthor) { this.bookAuthor = bookAuthor; }
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
    public int getQty() { return qty; }
    public void setQty(int qty) { this.qty = qty; }

    @Override
    public String toString() {
        return "Book{" + bookId + ", " + bookName + ", " + bookType
            + ", " + bookAuthor + ", " + price + ", qty=" + qty + "}";
    }

    // Cloning support
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();  // shallow clone
    }
}

BookType.java (Abstract base with nested object)

package com.nt.model;

public abstract class BookType implements Cloneable {
    protected String type;
    protected String domain;
    protected String phone;

    public void setType(String type) { this.type = type; }
    public String getType() { return type; }

    @Override
    public String toString() {
        return "BookType{" + type + ", " + domain + ", " + phone + "}";
    }

    public abstract void readMe();

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

// ItBookType.java
package com.nt.model;

public class ItBookType extends BookType {
    @Override
    public void readMe() {
        System.out.println("IT Book: " + type);
    }
}

// NonItBookType.java
package com.nt.model;

public class NonItBookType extends BookType {
    @Override
    public void readMe() {
        System.out.println("Non-IT Book: " + type);
    }
}

BookStoreFactory.java (Prototype Registry with cache)

package com.nt.factory;

import com.nt.model.Book;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class BookStoreFactory {
    private static Map<Integer, Book> bookCache = new HashMap<>();

    // Load from DB (simulated) and cache
    public static void loadBooks() {
        System.out.println("Loading books from DB (costly operation)...");
        Book book1 = new Book();
        book1.setBookId(1001);
        book1.setBookName("Core Java");
        book1.setBookType("IT");
        book1.setBookAuthor("James Gosling");
        book1.setPrice(9000);
        book1.setQty(50);

        Book book2 = new Book();
        book2.setBookId(1002);
        book2.setBookName("Business Basics");
        book2.setBookType("Non-IT");
        book2.setBookAuthor("Robert Kiyosaki");
        book2.setPrice(8000);
        book2.setQty(30);

        bookCache.put(book1.getBookId(), book1);
        bookCache.put(book2.getBookId(), book2);
        System.out.println("Books cached: " + bookCache.size());
    }

    // Returns CLONED book (not the original cached object)
    public static Book getBook(Integer bookId) throws CloneNotSupportedException {
        Book cachedBook = bookCache.get(bookId);
        if (cachedBook == null) {
            throw new IllegalArgumentException("Book not found: " + bookId);
        }
        return (Book) cachedBook.clone();  // clone, not the original!
    }

    // Returns cloned list of all books
    public static List<Book> getAllBooks() throws CloneNotSupportedException {
        return bookCache.values().stream()
            .map(book -> {
                try { return (Book) book.clone(); }
                catch (CloneNotSupportedException e) { throw new RuntimeException(e); }
            })
            .collect(Collectors.toList());
    }
}

Client test

package com.nt.test;

import com.nt.model.Book;
import com.nt.factory.BookStoreFactory;

public class BookStoreTest {
    public static void main(String[] args) throws Exception {
        // First call: load from "DB" into cache
        BookStoreFactory.loadBooks();

        System.out.println("\n=== First client gets books (cloned from cache) ===");
        Book b1 = BookStoreFactory.getBook(1001);
        System.out.println("Client1 book: " + b1 + " hash:" + b1.hashCode());

        System.out.println("\n=== Second client gets same book (another clone) ===");
        Book b2 = BookStoreFactory.getBook(1001);
        System.out.println("Client2 book: " + b2 + " hash:" + b2.hashCode());

        // Modify clone - original is not affected
        System.out.println("\n=== Modify clone, original unchanged ===");
        b2.setPrice(7500);
        b2.setQty(25);
        System.out.println("Modified clone: " + b2);
        System.out.println("Original cache: " + BookStoreFactory.getBook(1001));

        // Verify different objects
        System.out.println("\n=== Same object? ===");
        System.out.println("b1 == b2? " + (b1 == b2));        // false (different objects)
        System.out.println("b1.equals(b2)? " + b1.getBookId().equals(b2.getBookId())); // true (same data)
    }
}
Loading books from DB (costly operation)... Book:: 0-param constructor (1550009438) Book:: 0-param constructor (895342119) Books cached: 2 === First client gets books (cloned from cache) === Client1 book: Book{1001, Core Java, IT, James Gosling, 9000.0, qty=50} hash:672329506 === Second client gets same book (another clone) === Client2 book: Book{1001, Core Java, IT, James Gosling, 9000.0, qty=50} hash:1300502809 === Modify clone, original unchanged === Modified clone: Book{1001, Core Java, IT, James Gosling, 7500.0, qty=25} Original cache: Book{1001, Core Java, IT, James Gosling, 9000.0, qty=50} === Same object? === b1 == b2? false b1.getBookId().equals(b2.getBookId())? true

5. Shallow Clone vs Deep Clone

Critical Concept

Java's default clone() method (super.clone()) performs a shallow clone. This is one of the most important distinctions in the Prototype pattern.

Shallow Clone (default)

Only the main object is cloned. Inner objects (references) are shared between original and clone. Changing an inner object via the clone affects the original.

Shallow Cloning (super.clone()): Original Book Cloned Book +--------------+ +--------------+ | bookId: 1001 | (primitive - | bookId: 1001 | (copied value) | price: 9000 | copied) | price: 9000 | | bookType ----|--+ | bookType ----|--+ +--------------+ | +--------------+ | v v BookType obj <--- SAME OBJECT! (shared reference) {type: "IT"} Changing bookType.setType("Science") via clone ALSO changes the original's bookType!
// Shallow clone - default behavior
@Override
public Object clone() throws CloneNotSupportedException {
    return super.clone();  // clones only primitives and references (not inner objects)
}

// Problem:
Book original = new Book();
original.setBookType(new ItBookType());  // inner object

Book clone = (Book) original.clone();
clone.getBookType().setType("Science");  // modifies SHARED inner object!

System.out.println(original.getBookType().getType());  // "Science" (BROKEN!)

Deep Clone

Both the main object AND all inner objects are cloned. Each clone has its own independent copy of everything. Modifying the clone does not affect the original.

Deep Cloning (manually clone inner objects): Original Book Cloned Book +--------------+ +--------------+ | bookId: 1001 | | bookId: 1001 | | price: 9000 | | price: 9000 | | bookType ----|--+ | bookType ----|--+ +--------------+ | +--------------+ | v v BookType obj1 BookType obj2 (SEPARATE object!) {type: "IT"} {type: "IT"} Changing clone's bookType does NOT affect original.
// Deep clone - manually clone inner objects
@Override
public Object clone() throws CloneNotSupportedException {
    Book cloned = (Book) super.clone();              // clone main object
    cloned.bookType = (BookType) bookType.clone();   // clone inner object too!
    return cloned;
}

// Now safe:
Book clone = (Book) original.clone();
clone.getBookType().setType("Science");  // modifies only clone's copy

System.out.println(original.getBookType().getType());  // "IT" (unchanged!)

Deep Clone using Serialization

For objects with many nested levels, manual deep cloning is tedious. An alternative is to serialize and then deserialize the object, which creates a completely independent copy.

// Deep clone via Serialization (all objects must implement Serializable)
public static <T extends Serializable> T deepClone(T obj) {
    try {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        oos.close();

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        T cloned = (T) ois.readObject();
        ois.close();

        return cloned;
    } catch (Exception e) {
        throw new RuntimeException("Deep clone failed", e);
    }
}
AspectShallow CloneDeep Clone
Mechanismsuper.clone()Manually clone inner objects or use serialization
PrimitivesCopied by valueCopied by value
Object referencesShared (same reference)Cloned (independent copy)
PerformanceFast (one memory copy)Slower (multiple copies)
SafetyUnsafe if inner objects are mutableSafe (fully independent)
Use whenObject has only primitives/immutablesObject has mutable inner objects
Note on Immutable Classes

String, Integer, Float, Double are immutable classes. Shallow cloning is safe for these because they cannot be modified after creation. The shared reference causes no harm.

6. Full-Length Industry Project: Document Template System

Business Scenario

National Bank of India generates thousands of documents daily: loan agreements, account statements, KYC forms. Each document type has a complex template with headers, footers, formatting, legal text, and bank branding. Creating a template from scratch (loading from DB, parsing XML, applying styles) takes 500ms per document. Using the Prototype pattern, we load each template once, cache it, and clone it for each customer. Cloning takes under 1ms. The clone is then customized with the customer's specific data.

High-Level Design (HLD)

+---------------------------------------------------+ | SERVICE LAYER | | DocumentService | | - generateLoanAgreement(customerId) | | - generateAccountStatement(customerId) | | - generateKYCForm(customerId) | | (gets template clone, fills customer data) | +----------------------------+----------------------+ | v +---------------------------------------------------+ | PROTOTYPE REGISTRY | | DocumentTemplateRegistry | | - cache: Map<String, DocumentTemplate> | | - getTemplate(type): DocumentTemplate (CLONE) | | - loadTemplates(): void (one-time DB call) | +---------------------------------------------------+ | v +---------------------------------------------------+ | PROTOTYPE OBJECTS | | DocumentTemplate (abstract, implements Cloneable) | | - templateId, type, header, footer, legalText | | - formatting: DocumentFormatting (nested object)| | | | | LoanAgreementTemplate | | AccountStatementTemplate | | KYCFormTemplate | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | DocumentTemplate (abstract, implements Cloneable) | +-------------------------------------------------------+ | # templateId: String | | # type: String | | # header: String (intrinsic, heavy) | | # footer: String (intrinsic, heavy) | | # legalText: String (intrinsic, heavy) | | # formatting: DocumentFormatting (nested mutable obj)| | # customerName: String (extrinsic, set after clone)| | # customerId: String (extrinsic) | | # generatedDate: String (extrinsic) | +-------------------------------------------------------+ | + clone(): DocumentTemplate [deep clone] | | + fillCustomerData(id, name): void | | + render(): String | | # abstract generateBody(): String | +-------------------------------------------------------+ +-------------------------------------------------------+ | DocumentFormatting (nested object, also Cloneable) | +-------------------------------------------------------+ | - fontFamily: String | | - fontSize: int | | - marginTop: int | | - marginBottom: int | | - watermark: String | +-------------------------------------------------------+ +-------------------------------------------------------+ | DocumentTemplateRegistry (Prototype Factory) | +-------------------------------------------------------+ | - cache: Map<String, DocumentTemplate> [static] | | - loaded: boolean | +-------------------------------------------------------+ | + loadTemplates(): void [one-time costly]| | + getTemplate(type): DocumentTemplate [returns CLONE] | | + getCacheStats(): String | +-------------------------------------------------------+

Project Structure

nbi-document-template/ ├── src/ │ └── main/java/com/nbi/document/ │ ├── model/ │ │ ├── DocumentTemplate.java (abstract prototype) │ │ ├── DocumentFormatting.java (nested cloneable) │ │ ├── LoanAgreementTemplate.java │ │ ├── AccountStatementTemplate.java │ │ └── KYCFormTemplate.java │ ├── registry/ │ │ └── DocumentTemplateRegistry.java (prototype registry) │ ├── service/ │ │ └── DocumentService.java │ └── Application.java ├── src/ │ └── test/java/com/nbi/document/ │ └── registry/ │ └── DocumentTemplateRegistryTest.java └── pom.xml

DocumentFormatting.java (nested Cloneable)

package com.nbi.document.model;

public class DocumentFormatting implements Cloneable {
    private String fontFamily;
    private int fontSize;
    private int marginTop;
    private int marginBottom;
    private String watermark;

    public DocumentFormatting(String fontFamily, int fontSize,
                               int marginTop, int marginBottom, String watermark) {
        this.fontFamily = fontFamily;
        this.fontSize = fontSize;
        this.marginTop = marginTop;
        this.marginBottom = marginBottom;
        this.watermark = watermark;
    }

    // Getters
    public String getFontFamily() { return fontFamily; }
    public int getFontSize() { return fontSize; }
    public String getWatermark() { return watermark; }

    // Setters for customization after cloning
    public void setWatermark(String watermark) { this.watermark = watermark; }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();  // safe: all fields are primitives or immutable Strings
    }

    @Override
    public String toString() {
        return fontFamily + " " + fontSize + "pt, margins(" + marginTop + ","
            + marginBottom + "), watermark: " + watermark;
    }
}

DocumentTemplate.java (abstract prototype)

package com.nbi.document.model;

import java.time.LocalDate;

public abstract class DocumentTemplate implements Cloneable {
    protected String templateId;
    protected String type;
    protected String header;
    protected String footer;
    protected String legalText;
    protected DocumentFormatting formatting;

    // Extrinsic data (filled after cloning, per customer)
    protected String customerName;
    protected String customerId;
    protected String generatedDate;

    protected DocumentTemplate(String templateId, String type) {
        this.templateId = templateId;
        this.type = type;
        // Simulate costly template loading (DB, XML parsing, style application)
        System.out.println("  [COSTLY] Loading template: " + type
            + " (simulating 500ms DB call)");
    }

    public void fillCustomerData(String customerId, String customerName) {
        this.customerId = customerId;
        this.customerName = customerName;
        this.generatedDate = LocalDate.now().toString();
    }

    public String render() {
        StringBuilder sb = new StringBuilder();
        sb.append("=== ").append(type.toUpperCase()).append(" ===\n");
        sb.append("Format: ").append(formatting).append("\n");
        sb.append(header).append("\n");
        sb.append("Customer: ").append(customerName)
            .append(" (").append(customerId).append(")\n");
        sb.append("Date: ").append(generatedDate).append("\n\n");
        sb.append(generateBody()).append("\n\n");
        sb.append(footer).append("\n");
        sb.append("Legal: ").append(legalText.substring(0, Math.min(80, legalText.length())))
            .append("...\n");
        return sb.toString();
    }

    protected abstract String generateBody();

    // DEEP CLONE: clone the main object AND the nested DocumentFormatting
    @Override
    public Object clone() throws CloneNotSupportedException {
        DocumentTemplate cloned = (DocumentTemplate) super.clone();
        cloned.formatting = (DocumentFormatting) this.formatting.clone();  // deep clone!
        // Reset extrinsic data (each clone starts fresh)
        cloned.customerName = null;
        cloned.customerId = null;
        cloned.generatedDate = null;
        return cloned;
    }

    public String getType() { return type; }
    public String getTemplateId() { return templateId; }
}

LoanAgreementTemplate.java

package com.nbi.document.model;

public class LoanAgreementTemplate extends DocumentTemplate {

    public LoanAgreementTemplate() {
        super("TMPL-LOAN-001", "Loan Agreement");
        this.header = "NATIONAL BANK OF INDIA - LOAN AGREEMENT";
        this.footer = "This is a computer-generated document. No signature required.";
        this.legalText = "This loan agreement is governed by the laws of India. "
            + "The borrower agrees to repay the principal and interest as per the schedule. "
            + "Default will attract penalty as per RBI guidelines.";
        this.formatting = new DocumentFormatting("Times New Roman", 12, 20, 20, "DRAFT");
    }

    @Override
    protected String generateBody() {
        return "LOAN DETAILS:\n"
            + "  Borrower: " + customerName + "\n"
            + "  Account: " + customerId + "\n"
            + "  This agreement certifies that the above borrower has been sanctioned\n"
            + "  a loan as per the terms discussed and agreed upon.";
    }
}

AccountStatementTemplate.java

package com.nbi.document.model;

public class AccountStatementTemplate extends DocumentTemplate {

    public AccountStatementTemplate() {
        super("TMPL-STMT-001", "Account Statement");
        this.header = "NATIONAL BANK OF INDIA - ACCOUNT STATEMENT";
        this.footer = "End of statement. Please verify all transactions.";
        this.legalText = "This statement is provided for informational purposes only. "
            + "Report any discrepancies within 30 days of issue. "
            + "The bank is not liable for errors not reported within this period.";
        this.formatting = new DocumentFormatting("Arial", 10, 15, 15, "CONFIDENTIAL");
    }

    @Override
    protected String generateBody() {
        return "STATEMENT FOR: " + customerName + " (" + customerId + ")\n"
            + "  Period: Last 30 days\n"
            + "  Opening Balance: Rs 250,000.00\n"
            + "  Total Credits: Rs 50,000.00\n"
            + "  Total Debits: Rs 25,000.00\n"
            + "  Closing Balance: Rs 275,000.00";
    }
}

KYCFormTemplate.java

package com.nbi.document.model;

public class KYCFormTemplate extends DocumentTemplate {

    public KYCFormTemplate() {
        super("TMPL-KYC-001", "KYC Form");
        this.header = "NATIONAL BANK OF INDIA - KNOW YOUR CUSTOMER (KYC) FORM";
        this.footer = "Please submit this form with valid ID proof at your nearest branch.";
        this.legalText = "As per RBI circular, KYC verification is mandatory for all accounts. "
            + "Failure to complete KYC will result in account restrictions.";
        this.formatting = new DocumentFormatting("Calibri", 11, 25, 25, "ORIGINAL");
    }

    @Override
    protected String generateBody() {
        return "KYC VERIFICATION FOR: " + customerName + "\n"
            + "  Customer ID: " + customerId + "\n"
            + "  ID Type: Aadhaar / PAN / Passport (please select)\n"
            + "  ID Number: _______________\n"
            + "  Address: _______________\n"
            + "  Phone: _______________";
    }
}

DocumentTemplateRegistry.java (Prototype Registry)

package com.nbi.document.registry;

import com.nbi.document.model.*;
import java.util.HashMap;
import java.util.Map;

public class DocumentTemplateRegistry {
    private static final Map<String, DocumentTemplate> cache = new HashMap<>();
    private static boolean loaded = false;
    private static int cloneCount = 0;

    public static void loadTemplates() {
        if (loaded) {
            System.out.println("Templates already loaded (skipping)");
            return;
        }
        System.out.println("=== Loading templates from DB (one-time costly operation) ===");
        long start = System.currentTimeMillis();

        cache.put("loan", new LoanAgreementTemplate());
        cache.put("statement", new AccountStatementTemplate());
        cache.put("kyc", new KYCFormTemplate());

        loaded = true;
        long elapsed = System.currentTimeMillis() - start;
        System.out.println("Templates loaded: " + cache.size() + " in " + elapsed + "ms\n");
    }

    public static DocumentTemplate getTemplate(String type) {
        if (!loaded) {
            loadTemplates();
        }

        DocumentTemplate prototype = cache.get(type.toLowerCase());
        if (prototype == null) {
            throw new IllegalArgumentException("Unknown template type: " + type
                + ". Available: " + cache.keySet());
        }

        try {
            cloneCount++;
            return (DocumentTemplate) prototype.clone();  // return CLONE, not original
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException("Clone failed for template: " + type, e);
        }
    }

    public static String getCacheStats() {
        return "Templates cached: " + cache.size() + " | Total clones: " + cloneCount;
    }
}

DocumentService.java

package com.nbi.document.service;

import com.nbi.document.model.DocumentTemplate;
import com.nbi.document.registry.DocumentTemplateRegistry;

public class DocumentService {

    public String generateDocument(String type, String customerId, String customerName) {
        DocumentTemplate template = DocumentTemplateRegistry.getTemplate(type);
        template.fillCustomerData(customerId, customerName);
        return template.render();
    }
}

Application.java

package com.nbi.document;

import com.nbi.document.registry.DocumentTemplateRegistry;
import com.nbi.document.service.DocumentService;

public class Application {
    public static void main(String[] args) {
        DocumentService service = new DocumentService();

        // Templates loaded on first call
        // Scenario 1: Generate loan agreement for Rajesh
        System.out.println("=== Scenario 1: Loan Agreement ===");
        System.out.println(service.generateDocument("loan", "SAV2001", "Rajesh Kumar"));

        // Scenario 2: Same template type for different customer (cloned, not recreated)
        System.out.println("=== Scenario 2: Loan Agreement for Priya ===");
        System.out.println(service.generateDocument("loan", "CUR3001", "Priya Singh"));

        // Scenario 3: Account Statement
        System.out.println("=== Scenario 3: Account Statement ===");
        System.out.println(service.generateDocument("statement", "SAV2001", "Rajesh Kumar"));

        // Scenario 4: KYC Form
        System.out.println("=== Scenario 4: KYC Form ===");
        System.out.println(service.generateDocument("kyc", "SAV2001", "Rajesh Kumar"));

        // Scenario 5: Generate 100 loan agreements (all cloned from cache)
        System.out.println("=== Scenario 5: Bulk Generation (100 documents) ===");
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            service.generateDocument("loan", "ACC" + i, "Customer " + i);
        }
        long elapsed = System.currentTimeMillis() - start;
        System.out.println("Generated 100 documents in " + elapsed + "ms (all cloned)");
        System.out.println(DocumentTemplateRegistry.getCacheStats());

        // Scenario 6: Deep clone verification
        System.out.println("\n=== Scenario 6: Deep Clone Verification ===");
        var t1 = DocumentTemplateRegistry.getTemplate("loan");
        var t2 = DocumentTemplateRegistry.getTemplate("loan");
        System.out.println("t1 == t2? " + (t1 == t2));                     // false
        System.out.println("t1.templateId == t2.templateId? "
            + (t1.getTemplateId() == t2.getTemplateId()));                   // true (String interned)
        System.out.println("Same formatting object? "
            + (t1.toString().contains(t2.toString())));                      // similar content, different objects
    }
}

Expected Output

=== Loading templates from DB (one-time costly operation) === [COSTLY] Loading template: Loan Agreement (simulating 500ms DB call) [COSTLY] Loading template: Account Statement (simulating 500ms DB call) [COSTLY] Loading template: KYC Form (simulating 500ms DB call) Templates loaded: 3 in 5ms === Scenario 1: Loan Agreement === === LOAN AGREEMENT === Format: Times New Roman 12pt, margins(20,20), watermark: DRAFT NATIONAL BANK OF INDIA - LOAN AGREEMENT Customer: Rajesh Kumar (SAV2001) Date: 2026-11-02 LOAN DETAILS: Borrower: Rajesh Kumar Account: SAV2001 This agreement certifies that the above borrower has been sanctioned a loan as per the terms discussed and agreed upon. This is a computer-generated document. No signature required. Legal: This loan agreement is governed by the laws of India. The borrower agrees to re... === Scenario 2: Loan Agreement for Priya === === LOAN AGREEMENT === Format: Times New Roman 12pt, margins(20,20), watermark: DRAFT NATIONAL BANK OF INDIA - LOAN AGREEMENT Customer: Priya Singh (CUR3001) Date: 2026-11-02 ... === Scenario 5: Bulk Generation (100 documents) === Generated 100 documents in 8ms (all cloned) Templates cached: 3 | Total clones: 106 === Scenario 6: Deep Clone Verification === t1 == t2? false

Key observations from the project

  1. 3 template objects serve 106 document generations. Templates loaded from "DB" once. All subsequent calls clone from cache.
  2. Deep clone protects the cache. DocumentFormatting is a mutable nested object. Deep cloning ensures modifying one document's formatting does not affect the template or other documents.
  3. Extrinsic data reset on clone. The clone() method resets customerName, customerId, generatedDate to null. Each clone starts clean and is filled with customer-specific data.
  4. 100 documents generated in 8ms. Without prototype, loading 100 templates from DB would take 100 x 500ms = 50 seconds. With prototype, it takes 8ms.
  5. Template types are extensible. Adding a new template (e.g., ChequeBookTemplate) requires one new class and one registry entry. No other code changes.

7. When to Use and When NOT to Use

When to use Prototype

ScenarioExampleWhy Prototype
Object creation is expensiveLoading from DB, parsing XML, network callClone from cache instead of re-creating
Many similar objects neededGame entities, document templates, form copiesCreate one prototype, clone for each use
Object configuration is complexMulti-step builder result needs copiesBuild once, clone many times
Runtime object creationObjects whose type is determined at runtimeRegistry returns clone of correct prototype

When NOT to use Prototype

ScenarioWhy NOTUse Instead
Object creation is cheapCloning overhead not worth itnew keyword
Objects have circular referencesDeep cloning circular references causes infinite recursionSerialization-based cloning or Builder
Few objects neededRegistry overhead for 2-3 objects is excessiveDirect creation

8. Prototype vs Other Creational Patterns

AspectPrototypeFlyweightFactorySingleton
ReturnsClone (new object)Same cached objectNew objectSame single object
Object countMany (one per clone)Few (shared)Many (one per call)One
Modifiable?Yes (each clone independent)No (shared, read-only)YesYes (but risky)
Creation costLow (memory copy)Zero (return reference)High (new each time)One-time

9. Quick Reference Card

ComponentRoleIn Document Project
Prototype InterfaceCloneableDocumentTemplate implements Cloneable
Concrete PrototypeThe cacheable, cloneable objectLoanAgreementTemplate, AccountStatementTemplate
Prototype RegistryCache + clone factoryDocumentTemplateRegistry
ClientGets clones, fills dataDocumentService
Creational GOF Pattern Medium

Prototype Design Pattern in Java

Complete Tutorial | 11 Sections | Full Document Template Project with HLD and LLD | Shallow vs Deep Cloning | BookStore Example | Prototype Registry | Multiple Scenarios | 12 Interview Questions

1. What is Prototype Pattern?

Definition

The Prototype pattern is a GOF Creational Pattern. It creates a copy of an existing object through cloning, not from scratch level. According to GOF: "Specify the kind of objects to create using a prototypical instance, and create new objects by copying this prototype."

One line: Instead of creating objects from scratch (costly), clone existing cached objects to get new copies quickly.

Flyweight vs Prototype

AspectFlyweightPrototype
What it returnsThe SAME cached object (shared reference)A CLONE/COPY of the cached object (new instance)
Object ownershipShared among all clientsEach client gets its own independent copy
Modification safe?No (modifying affects everyone)Yes (each clone is independent)
Use whenObjects are immutable/read-onlyEach client needs a modifiable copy

2. The Problem

Problem

Creating new objects from scratch is a costly affair in Java. That needs more memory and CPU time. If we want certain bunch of objects to be created for multiple times, creating them every time from scratch level degrades the performance.

Problem: Every request creates objects from scratch EndUser 1 DB Server (ClientApp1) ---[1a]---> BookStore App ---[1b]---> Books info (db table) (buffer/cache) bookId bookName price EndUser 2 | List of [1c] 1001 C8J 9000 (ClientApp2) ---[2a]---> book objs <---------- 1002 BB 8000 | [2a] new objs from scratch! Every time hitting DB s/w and every time creating same bunch of objects is not really good practice. It will degrade the performance of the Application.

3. The Solution: Clone from Cache

Solution

Use Prototype Design Pattern which says: keep the objects that are created through costly process in cache memory and use cloning concept to create copy of objects from those cached objects.

Implementing Prototype Design pattern is nothing but creating bunch of objects through cloning process repeatedly by keeping the actual objects in the cache.

Solution: Clone from cached prototypes EndUser 1 buffer/cache DB Server (ClientApp1) ---[1a]---> [Book objs] <---[1b]--- Books info (db table) cached once! EndUser 2 | (ClientApp2) ---[2a]---> [2b] clone objs from cache (fast! no DB hit) First call: loads from DB, stores in cache Second call: clones from cache (no DB call)
Without cloning: With cloning: Student obj1 Student obj2 Student obj1 Student obj2 (Freshly (Another freshly (Freshly = (Student) obj1.clone() created created same created (object created object) object) object) through cloning process) Both created from scratch. obj2 is a copy of obj1. Costly: constructor + DB call. Cheap: memory copy only.

Four Components

ComponentRole
a) Prototype InterfaceDeclares the clone method (Cloneable interface in Java)
b) Concrete PrototypeImplements cloning. The actual object that gets cached and cloned.
c) Prototype Registry/FactoryMaintains a HashMap cache of prototype objects. Returns clones on request.
d) ClientGets cloned objects from the registry instead of creating new ones.

4. BookStore Example (from course notes)

Book.java (Concrete Prototype)

package com.nt.model;

public class Book implements Cloneable {
    private Integer bookId;
    private String bookName;
    private String bookType;
    private String bookAuthor;
    private double price;
    private int qty;

    public Book() {
        System.out.println("Book:: 0-param constructor (" + this.hashCode() + ")");
    }

    // Getters and setters
    public Integer getBookId() { return bookId; }
    public void setBookId(Integer bookId) { this.bookId = bookId; }
    public String getBookName() { return bookName; }
    public void setBookName(String bookName) { this.bookName = bookName; }
    public String getBookType() { return bookType; }
    public void setBookType(String bookType) { this.bookType = bookType; }
    public String getBookAuthor() { return bookAuthor; }
    public void setBookAuthor(String bookAuthor) { this.bookAuthor = bookAuthor; }
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
    public int getQty() { return qty; }
    public void setQty(int qty) { this.qty = qty; }

    @Override
    public String toString() {
        return "Book{" + bookId + ", " + bookName + ", " + bookType
            + ", " + bookAuthor + ", " + price + ", qty=" + qty + "}";
    }

    // Cloning support
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();  // shallow clone
    }
}

BookType.java (Abstract base with nested object)

package com.nt.model;

public abstract class BookType implements Cloneable {
    protected String type;
    protected String domain;
    protected String phone;

    public void setType(String type) { this.type = type; }
    public String getType() { return type; }

    @Override
    public String toString() {
        return "BookType{" + type + ", " + domain + ", " + phone + "}";
    }

    public abstract void readMe();

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

// ItBookType.java
package com.nt.model;

public class ItBookType extends BookType {
    @Override
    public void readMe() {
        System.out.println("IT Book: " + type);
    }
}

// NonItBookType.java
package com.nt.model;

public class NonItBookType extends BookType {
    @Override
    public void readMe() {
        System.out.println("Non-IT Book: " + type);
    }
}

BookStoreFactory.java (Prototype Registry with cache)

package com.nt.factory;

import com.nt.model.Book;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class BookStoreFactory {
    private static Map<Integer, Book> bookCache = new HashMap<>();

    // Load from DB (simulated) and cache
    public static void loadBooks() {
        System.out.println("Loading books from DB (costly operation)...");
        Book book1 = new Book();
        book1.setBookId(1001);
        book1.setBookName("Core Java");
        book1.setBookType("IT");
        book1.setBookAuthor("James Gosling");
        book1.setPrice(9000);
        book1.setQty(50);

        Book book2 = new Book();
        book2.setBookId(1002);
        book2.setBookName("Business Basics");
        book2.setBookType("Non-IT");
        book2.setBookAuthor("Robert Kiyosaki");
        book2.setPrice(8000);
        book2.setQty(30);

        bookCache.put(book1.getBookId(), book1);
        bookCache.put(book2.getBookId(), book2);
        System.out.println("Books cached: " + bookCache.size());
    }

    // Returns CLONED book (not the original cached object)
    public static Book getBook(Integer bookId) throws CloneNotSupportedException {
        Book cachedBook = bookCache.get(bookId);
        if (cachedBook == null) {
            throw new IllegalArgumentException("Book not found: " + bookId);
        }
        return (Book) cachedBook.clone();  // clone, not the original!
    }

    // Returns cloned list of all books
    public static List<Book> getAllBooks() throws CloneNotSupportedException {
        return bookCache.values().stream()
            .map(book -> {
                try { return (Book) book.clone(); }
                catch (CloneNotSupportedException e) { throw new RuntimeException(e); }
            })
            .collect(Collectors.toList());
    }
}

Client test

package com.nt.test;

import com.nt.model.Book;
import com.nt.factory.BookStoreFactory;

public class BookStoreTest {
    public static void main(String[] args) throws Exception {
        // First call: load from "DB" into cache
        BookStoreFactory.loadBooks();

        System.out.println("\n=== First client gets books (cloned from cache) ===");
        Book b1 = BookStoreFactory.getBook(1001);
        System.out.println("Client1 book: " + b1 + " hash:" + b1.hashCode());

        System.out.println("\n=== Second client gets same book (another clone) ===");
        Book b2 = BookStoreFactory.getBook(1001);
        System.out.println("Client2 book: " + b2 + " hash:" + b2.hashCode());

        // Modify clone - original is not affected
        System.out.println("\n=== Modify clone, original unchanged ===");
        b2.setPrice(7500);
        b2.setQty(25);
        System.out.println("Modified clone: " + b2);
        System.out.println("Original cache: " + BookStoreFactory.getBook(1001));

        // Verify different objects
        System.out.println("\n=== Same object? ===");
        System.out.println("b1 == b2? " + (b1 == b2));        // false (different objects)
        System.out.println("b1.equals(b2)? " + b1.getBookId().equals(b2.getBookId())); // true (same data)
    }
}
Loading books from DB (costly operation)... Book:: 0-param constructor (1550009438) Book:: 0-param constructor (895342119) Books cached: 2 === First client gets books (cloned from cache) === Client1 book: Book{1001, Core Java, IT, James Gosling, 9000.0, qty=50} hash:672329506 === Second client gets same book (another clone) === Client2 book: Book{1001, Core Java, IT, James Gosling, 9000.0, qty=50} hash:1300502809 === Modify clone, original unchanged === Modified clone: Book{1001, Core Java, IT, James Gosling, 7500.0, qty=25} Original cache: Book{1001, Core Java, IT, James Gosling, 9000.0, qty=50} === Same object? === b1 == b2? false b1.getBookId().equals(b2.getBookId())? true

5. Shallow Clone vs Deep Clone

Critical Concept

Java's default clone() method (super.clone()) performs a shallow clone. This is one of the most important distinctions in the Prototype pattern.

Shallow Clone (default)

Only the main object is cloned. Inner objects (references) are shared between original and clone. Changing an inner object via the clone affects the original.

Shallow Cloning (super.clone()): Original Book Cloned Book +--------------+ +--------------+ | bookId: 1001 | (primitive - | bookId: 1001 | (copied value) | price: 9000 | copied) | price: 9000 | | bookType ----|--+ | bookType ----|--+ +--------------+ | +--------------+ | v v BookType obj <--- SAME OBJECT! (shared reference) {type: "IT"} Changing bookType.setType("Science") via clone ALSO changes the original's bookType!
// Shallow clone - default behavior
@Override
public Object clone() throws CloneNotSupportedException {
    return super.clone();  // clones only primitives and references (not inner objects)
}

// Problem:
Book original = new Book();
original.setBookType(new ItBookType());  // inner object

Book clone = (Book) original.clone();
clone.getBookType().setType("Science");  // modifies SHARED inner object!

System.out.println(original.getBookType().getType());  // "Science" (BROKEN!)

Deep Clone

Both the main object AND all inner objects are cloned. Each clone has its own independent copy of everything. Modifying the clone does not affect the original.

Deep Cloning (manually clone inner objects): Original Book Cloned Book +--------------+ +--------------+ | bookId: 1001 | | bookId: 1001 | | price: 9000 | | price: 9000 | | bookType ----|--+ | bookType ----|--+ +--------------+ | +--------------+ | v v BookType obj1 BookType obj2 (SEPARATE object!) {type: "IT"} {type: "IT"} Changing clone's bookType does NOT affect original.
// Deep clone - manually clone inner objects
@Override
public Object clone() throws CloneNotSupportedException {
    Book cloned = (Book) super.clone();              // clone main object
    cloned.bookType = (BookType) bookType.clone();   // clone inner object too!
    return cloned;
}

// Now safe:
Book clone = (Book) original.clone();
clone.getBookType().setType("Science");  // modifies only clone's copy

System.out.println(original.getBookType().getType());  // "IT" (unchanged!)

Deep Clone using Serialization

For objects with many nested levels, manual deep cloning is tedious. An alternative is to serialize and then deserialize the object, which creates a completely independent copy.

// Deep clone via Serialization (all objects must implement Serializable)
public static <T extends Serializable> T deepClone(T obj) {
    try {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        oos.close();

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        T cloned = (T) ois.readObject();
        ois.close();

        return cloned;
    } catch (Exception e) {
        throw new RuntimeException("Deep clone failed", e);
    }
}
AspectShallow CloneDeep Clone
Mechanismsuper.clone()Manually clone inner objects or use serialization
PrimitivesCopied by valueCopied by value
Object referencesShared (same reference)Cloned (independent copy)
PerformanceFast (one memory copy)Slower (multiple copies)
SafetyUnsafe if inner objects are mutableSafe (fully independent)
Use whenObject has only primitives/immutablesObject has mutable inner objects
Note on Immutable Classes

String, Integer, Float, Double are immutable classes. Shallow cloning is safe for these because they cannot be modified after creation. The shared reference causes no harm.

6. Full-Length Industry Project: Document Template System

Business Scenario

National Bank of India generates thousands of documents daily: loan agreements, account statements, KYC forms. Each document type has a complex template with headers, footers, formatting, legal text, and bank branding. Creating a template from scratch (loading from DB, parsing XML, applying styles) takes 500ms per document. Using the Prototype pattern, we load each template once, cache it, and clone it for each customer. Cloning takes under 1ms. The clone is then customized with the customer's specific data.

High-Level Design (HLD)

+---------------------------------------------------+ | SERVICE LAYER | | DocumentService | | - generateLoanAgreement(customerId) | | - generateAccountStatement(customerId) | | - generateKYCForm(customerId) | | (gets template clone, fills customer data) | +----------------------------+----------------------+ | v +---------------------------------------------------+ | PROTOTYPE REGISTRY | | DocumentTemplateRegistry | | - cache: Map<String, DocumentTemplate> | | - getTemplate(type): DocumentTemplate (CLONE) | | - loadTemplates(): void (one-time DB call) | +---------------------------------------------------+ | v +---------------------------------------------------+ | PROTOTYPE OBJECTS | | DocumentTemplate (abstract, implements Cloneable) | | - templateId, type, header, footer, legalText | | - formatting: DocumentFormatting (nested object)| | | | | LoanAgreementTemplate | | AccountStatementTemplate | | KYCFormTemplate | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | DocumentTemplate (abstract, implements Cloneable) | +-------------------------------------------------------+ | # templateId: String | | # type: String | | # header: String (intrinsic, heavy) | | # footer: String (intrinsic, heavy) | | # legalText: String (intrinsic, heavy) | | # formatting: DocumentFormatting (nested mutable obj)| | # customerName: String (extrinsic, set after clone)| | # customerId: String (extrinsic) | | # generatedDate: String (extrinsic) | +-------------------------------------------------------+ | + clone(): DocumentTemplate [deep clone] | | + fillCustomerData(id, name): void | | + render(): String | | # abstract generateBody(): String | +-------------------------------------------------------+ +-------------------------------------------------------+ | DocumentFormatting (nested object, also Cloneable) | +-------------------------------------------------------+ | - fontFamily: String | | - fontSize: int | | - marginTop: int | | - marginBottom: int | | - watermark: String | +-------------------------------------------------------+ +-------------------------------------------------------+ | DocumentTemplateRegistry (Prototype Factory) | +-------------------------------------------------------+ | - cache: Map<String, DocumentTemplate> [static] | | - loaded: boolean | +-------------------------------------------------------+ | + loadTemplates(): void [one-time costly]| | + getTemplate(type): DocumentTemplate [returns CLONE] | | + getCacheStats(): String | +-------------------------------------------------------+

Project Structure

nbi-document-template/ ├── src/ │ └── main/java/com/nbi/document/ │ ├── model/ │ │ ├── DocumentTemplate.java (abstract prototype) │ │ ├── DocumentFormatting.java (nested cloneable) │ │ ├── LoanAgreementTemplate.java │ │ ├── AccountStatementTemplate.java │ │ └── KYCFormTemplate.java │ ├── registry/ │ │ └── DocumentTemplateRegistry.java (prototype registry) │ ├── service/ │ │ └── DocumentService.java │ └── Application.java ├── src/ │ └── test/java/com/nbi/document/ │ └── registry/ │ └── DocumentTemplateRegistryTest.java └── pom.xml

DocumentFormatting.java (nested Cloneable)

package com.nbi.document.model;

public class DocumentFormatting implements Cloneable {
    private String fontFamily;
    private int fontSize;
    private int marginTop;
    private int marginBottom;
    private String watermark;

    public DocumentFormatting(String fontFamily, int fontSize,
                               int marginTop, int marginBottom, String watermark) {
        this.fontFamily = fontFamily;
        this.fontSize = fontSize;
        this.marginTop = marginTop;
        this.marginBottom = marginBottom;
        this.watermark = watermark;
    }

    // Getters
    public String getFontFamily() { return fontFamily; }
    public int getFontSize() { return fontSize; }
    public String getWatermark() { return watermark; }

    // Setters for customization after cloning
    public void setWatermark(String watermark) { this.watermark = watermark; }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();  // safe: all fields are primitives or immutable Strings
    }

    @Override
    public String toString() {
        return fontFamily + " " + fontSize + "pt, margins(" + marginTop + ","
            + marginBottom + "), watermark: " + watermark;
    }
}

DocumentTemplate.java (abstract prototype)

package com.nbi.document.model;

import java.time.LocalDate;

public abstract class DocumentTemplate implements Cloneable {
    protected String templateId;
    protected String type;
    protected String header;
    protected String footer;
    protected String legalText;
    protected DocumentFormatting formatting;

    // Extrinsic data (filled after cloning, per customer)
    protected String customerName;
    protected String customerId;
    protected String generatedDate;

    protected DocumentTemplate(String templateId, String type) {
        this.templateId = templateId;
        this.type = type;
        // Simulate costly template loading (DB, XML parsing, style application)
        System.out.println("  [COSTLY] Loading template: " + type
            + " (simulating 500ms DB call)");
    }

    public void fillCustomerData(String customerId, String customerName) {
        this.customerId = customerId;
        this.customerName = customerName;
        this.generatedDate = LocalDate.now().toString();
    }

    public String render() {
        StringBuilder sb = new StringBuilder();
        sb.append("=== ").append(type.toUpperCase()).append(" ===\n");
        sb.append("Format: ").append(formatting).append("\n");
        sb.append(header).append("\n");
        sb.append("Customer: ").append(customerName)
            .append(" (").append(customerId).append(")\n");
        sb.append("Date: ").append(generatedDate).append("\n\n");
        sb.append(generateBody()).append("\n\n");
        sb.append(footer).append("\n");
        sb.append("Legal: ").append(legalText.substring(0, Math.min(80, legalText.length())))
            .append("...\n");
        return sb.toString();
    }

    protected abstract String generateBody();

    // DEEP CLONE: clone the main object AND the nested DocumentFormatting
    @Override
    public Object clone() throws CloneNotSupportedException {
        DocumentTemplate cloned = (DocumentTemplate) super.clone();
        cloned.formatting = (DocumentFormatting) this.formatting.clone();  // deep clone!
        // Reset extrinsic data (each clone starts fresh)
        cloned.customerName = null;
        cloned.customerId = null;
        cloned.generatedDate = null;
        return cloned;
    }

    public String getType() { return type; }
    public String getTemplateId() { return templateId; }
}

LoanAgreementTemplate.java

package com.nbi.document.model;

public class LoanAgreementTemplate extends DocumentTemplate {

    public LoanAgreementTemplate() {
        super("TMPL-LOAN-001", "Loan Agreement");
        this.header = "NATIONAL BANK OF INDIA - LOAN AGREEMENT";
        this.footer = "This is a computer-generated document. No signature required.";
        this.legalText = "This loan agreement is governed by the laws of India. "
            + "The borrower agrees to repay the principal and interest as per the schedule. "
            + "Default will attract penalty as per RBI guidelines.";
        this.formatting = new DocumentFormatting("Times New Roman", 12, 20, 20, "DRAFT");
    }

    @Override
    protected String generateBody() {
        return "LOAN DETAILS:\n"
            + "  Borrower: " + customerName + "\n"
            + "  Account: " + customerId + "\n"
            + "  This agreement certifies that the above borrower has been sanctioned\n"
            + "  a loan as per the terms discussed and agreed upon.";
    }
}

AccountStatementTemplate.java

package com.nbi.document.model;

public class AccountStatementTemplate extends DocumentTemplate {

    public AccountStatementTemplate() {
        super("TMPL-STMT-001", "Account Statement");
        this.header = "NATIONAL BANK OF INDIA - ACCOUNT STATEMENT";
        this.footer = "End of statement. Please verify all transactions.";
        this.legalText = "This statement is provided for informational purposes only. "
            + "Report any discrepancies within 30 days of issue. "
            + "The bank is not liable for errors not reported within this period.";
        this.formatting = new DocumentFormatting("Arial", 10, 15, 15, "CONFIDENTIAL");
    }

    @Override
    protected String generateBody() {
        return "STATEMENT FOR: " + customerName + " (" + customerId + ")\n"
            + "  Period: Last 30 days\n"
            + "  Opening Balance: Rs 250,000.00\n"
            + "  Total Credits: Rs 50,000.00\n"
            + "  Total Debits: Rs 25,000.00\n"
            + "  Closing Balance: Rs 275,000.00";
    }
}

KYCFormTemplate.java

package com.nbi.document.model;

public class KYCFormTemplate extends DocumentTemplate {

    public KYCFormTemplate() {
        super("TMPL-KYC-001", "KYC Form");
        this.header = "NATIONAL BANK OF INDIA - KNOW YOUR CUSTOMER (KYC) FORM";
        this.footer = "Please submit this form with valid ID proof at your nearest branch.";
        this.legalText = "As per RBI circular, KYC verification is mandatory for all accounts. "
            + "Failure to complete KYC will result in account restrictions.";
        this.formatting = new DocumentFormatting("Calibri", 11, 25, 25, "ORIGINAL");
    }

    @Override
    protected String generateBody() {
        return "KYC VERIFICATION FOR: " + customerName + "\n"
            + "  Customer ID: " + customerId + "\n"
            + "  ID Type: Aadhaar / PAN / Passport (please select)\n"
            + "  ID Number: _______________\n"
            + "  Address: _______________\n"
            + "  Phone: _______________";
    }
}

DocumentTemplateRegistry.java (Prototype Registry)

package com.nbi.document.registry;

import com.nbi.document.model.*;
import java.util.HashMap;
import java.util.Map;

public class DocumentTemplateRegistry {
    private static final Map<String, DocumentTemplate> cache = new HashMap<>();
    private static boolean loaded = false;
    private static int cloneCount = 0;

    public static void loadTemplates() {
        if (loaded) {
            System.out.println("Templates already loaded (skipping)");
            return;
        }
        System.out.println("=== Loading templates from DB (one-time costly operation) ===");
        long start = System.currentTimeMillis();

        cache.put("loan", new LoanAgreementTemplate());
        cache.put("statement", new AccountStatementTemplate());
        cache.put("kyc", new KYCFormTemplate());

        loaded = true;
        long elapsed = System.currentTimeMillis() - start;
        System.out.println("Templates loaded: " + cache.size() + " in " + elapsed + "ms\n");
    }

    public static DocumentTemplate getTemplate(String type) {
        if (!loaded) {
            loadTemplates();
        }

        DocumentTemplate prototype = cache.get(type.toLowerCase());
        if (prototype == null) {
            throw new IllegalArgumentException("Unknown template type: " + type
                + ". Available: " + cache.keySet());
        }

        try {
            cloneCount++;
            return (DocumentTemplate) prototype.clone();  // return CLONE, not original
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException("Clone failed for template: " + type, e);
        }
    }

    public static String getCacheStats() {
        return "Templates cached: " + cache.size() + " | Total clones: " + cloneCount;
    }
}

DocumentService.java

package com.nbi.document.service;

import com.nbi.document.model.DocumentTemplate;
import com.nbi.document.registry.DocumentTemplateRegistry;

public class DocumentService {

    public String generateDocument(String type, String customerId, String customerName) {
        DocumentTemplate template = DocumentTemplateRegistry.getTemplate(type);
        template.fillCustomerData(customerId, customerName);
        return template.render();
    }
}

Application.java

package com.nbi.document;

import com.nbi.document.registry.DocumentTemplateRegistry;
import com.nbi.document.service.DocumentService;

public class Application {
    public static void main(String[] args) {
        DocumentService service = new DocumentService();

        // Templates loaded on first call
        // Scenario 1: Generate loan agreement for Rajesh
        System.out.println("=== Scenario 1: Loan Agreement ===");
        System.out.println(service.generateDocument("loan", "SAV2001", "Rajesh Kumar"));

        // Scenario 2: Same template type for different customer (cloned, not recreated)
        System.out.println("=== Scenario 2: Loan Agreement for Priya ===");
        System.out.println(service.generateDocument("loan", "CUR3001", "Priya Singh"));

        // Scenario 3: Account Statement
        System.out.println("=== Scenario 3: Account Statement ===");
        System.out.println(service.generateDocument("statement", "SAV2001", "Rajesh Kumar"));

        // Scenario 4: KYC Form
        System.out.println("=== Scenario 4: KYC Form ===");
        System.out.println(service.generateDocument("kyc", "SAV2001", "Rajesh Kumar"));

        // Scenario 5: Generate 100 loan agreements (all cloned from cache)
        System.out.println("=== Scenario 5: Bulk Generation (100 documents) ===");
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            service.generateDocument("loan", "ACC" + i, "Customer " + i);
        }
        long elapsed = System.currentTimeMillis() - start;
        System.out.println("Generated 100 documents in " + elapsed + "ms (all cloned)");
        System.out.println(DocumentTemplateRegistry.getCacheStats());

        // Scenario 6: Deep clone verification
        System.out.println("\n=== Scenario 6: Deep Clone Verification ===");
        var t1 = DocumentTemplateRegistry.getTemplate("loan");
        var t2 = DocumentTemplateRegistry.getTemplate("loan");
        System.out.println("t1 == t2? " + (t1 == t2));                     // false
        System.out.println("t1.templateId == t2.templateId? "
            + (t1.getTemplateId() == t2.getTemplateId()));                   // true (String interned)
        System.out.println("Same formatting object? "
            + (t1.toString().contains(t2.toString())));                      // similar content, different objects
    }
}

Expected Output

=== Loading templates from DB (one-time costly operation) === [COSTLY] Loading template: Loan Agreement (simulating 500ms DB call) [COSTLY] Loading template: Account Statement (simulating 500ms DB call) [COSTLY] Loading template: KYC Form (simulating 500ms DB call) Templates loaded: 3 in 5ms === Scenario 1: Loan Agreement === === LOAN AGREEMENT === Format: Times New Roman 12pt, margins(20,20), watermark: DRAFT NATIONAL BANK OF INDIA - LOAN AGREEMENT Customer: Rajesh Kumar (SAV2001) Date: 2026-11-02 LOAN DETAILS: Borrower: Rajesh Kumar Account: SAV2001 This agreement certifies that the above borrower has been sanctioned a loan as per the terms discussed and agreed upon. This is a computer-generated document. No signature required. Legal: This loan agreement is governed by the laws of India. The borrower agrees to re... === Scenario 2: Loan Agreement for Priya === === LOAN AGREEMENT === Format: Times New Roman 12pt, margins(20,20), watermark: DRAFT NATIONAL BANK OF INDIA - LOAN AGREEMENT Customer: Priya Singh (CUR3001) Date: 2026-11-02 ... === Scenario 5: Bulk Generation (100 documents) === Generated 100 documents in 8ms (all cloned) Templates cached: 3 | Total clones: 106 === Scenario 6: Deep Clone Verification === t1 == t2? false

Key observations from the project

  1. 3 template objects serve 106 document generations. Templates loaded from "DB" once. All subsequent calls clone from cache.
  2. Deep clone protects the cache. DocumentFormatting is a mutable nested object. Deep cloning ensures modifying one document's formatting does not affect the template or other documents.
  3. Extrinsic data reset on clone. The clone() method resets customerName, customerId, generatedDate to null. Each clone starts clean and is filled with customer-specific data.
  4. 100 documents generated in 8ms. Without prototype, loading 100 templates from DB would take 100 x 500ms = 50 seconds. With prototype, it takes 8ms.
  5. Template types are extensible. Adding a new template (e.g., ChequeBookTemplate) requires one new class and one registry entry. No other code changes.

7. When to Use and When NOT to Use

When to use Prototype

ScenarioExampleWhy Prototype
Object creation is expensiveLoading from DB, parsing XML, network callClone from cache instead of re-creating
Many similar objects neededGame entities, document templates, form copiesCreate one prototype, clone for each use
Object configuration is complexMulti-step builder result needs copiesBuild once, clone many times
Runtime object creationObjects whose type is determined at runtimeRegistry returns clone of correct prototype

When NOT to use Prototype

ScenarioWhy NOTUse Instead
Object creation is cheapCloning overhead not worth itnew keyword
Objects have circular referencesDeep cloning circular references causes infinite recursionSerialization-based cloning or Builder
Few objects neededRegistry overhead for 2-3 objects is excessiveDirect creation

8. Prototype vs Other Creational Patterns

AspectPrototypeFlyweightFactorySingleton
ReturnsClone (new object)Same cached objectNew objectSame single object
Object countMany (one per clone)Few (shared)Many (one per call)One
Modifiable?Yes (each clone independent)No (shared, read-only)YesYes (but risky)
Creation costLow (memory copy)Zero (return reference)High (new each time)One-time

9. Quick Reference Card

ComponentRoleIn Document Project
Prototype InterfaceCloneableDocumentTemplate implements Cloneable
Concrete PrototypeThe cacheable, cloneable objectLoanAgreementTemplate, AccountStatementTemplate
Prototype RegistryCache + clone factoryDocumentTemplateRegistry
ClientGets clones, fills dataDocumentService