Prototype Design Pattern
1. What is Prototype Pattern?
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
| Aspect | Flyweight | Prototype |
|---|---|---|
| What it returns | The SAME cached object (shared reference) | A CLONE/COPY of the cached object (new instance) |
| Object ownership | Shared among all clients | Each client gets its own independent copy |
| Modification safe? | No (modifying affects everyone) | Yes (each clone is independent) |
| Use when | Objects are immutable/read-only | Each client needs a modifiable copy |
2. The 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.
3. The Solution: Clone from Cache
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.
Four Components
| Component | Role |
|---|---|
| a) Prototype Interface | Declares the clone method (Cloneable interface in Java) |
| b) Concrete Prototype | Implements cloning. The actual object that gets cached and cloned. |
| c) Prototype Registry/Factory | Maintains a HashMap cache of prototype objects. Returns clones on request. |
| d) Client | Gets 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)
}
}
5. Shallow Clone vs Deep Clone
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 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 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);
}
}
| Aspect | Shallow Clone | Deep Clone |
|---|---|---|
| Mechanism | super.clone() | Manually clone inner objects or use serialization |
| Primitives | Copied by value | Copied by value |
| Object references | Shared (same reference) | Cloned (independent copy) |
| Performance | Fast (one memory copy) | Slower (multiple copies) |
| Safety | Unsafe if inner objects are mutable | Safe (fully independent) |
| Use when | Object has only primitives/immutables | Object has mutable inner objects |
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
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)
Low-Level Design (LLD)
Project Structure
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
Key observations from the project
- 3 template objects serve 106 document generations. Templates loaded from "DB" once. All subsequent calls clone from cache.
- Deep clone protects the cache.
DocumentFormattingis a mutable nested object. Deep cloning ensures modifying one document's formatting does not affect the template or other documents. - 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. - 100 documents generated in 8ms. Without prototype, loading 100 templates from DB would take 100 x 500ms = 50 seconds. With prototype, it takes 8ms.
- 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
| Scenario | Example | Why Prototype |
|---|---|---|
| Object creation is expensive | Loading from DB, parsing XML, network call | Clone from cache instead of re-creating |
| Many similar objects needed | Game entities, document templates, form copies | Create one prototype, clone for each use |
| Object configuration is complex | Multi-step builder result needs copies | Build once, clone many times |
| Runtime object creation | Objects whose type is determined at runtime | Registry returns clone of correct prototype |
When NOT to use Prototype
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Object creation is cheap | Cloning overhead not worth it | new keyword |
| Objects have circular references | Deep cloning circular references causes infinite recursion | Serialization-based cloning or Builder |
| Few objects needed | Registry overhead for 2-3 objects is excessive | Direct creation |
8. Prototype vs Other Creational Patterns
| Aspect | Prototype | Flyweight | Factory | Singleton |
|---|---|---|---|---|
| Returns | Clone (new object) | Same cached object | New object | Same single object |
| Object count | Many (one per clone) | Few (shared) | Many (one per call) | One |
| Modifiable? | Yes (each clone independent) | No (shared, read-only) | Yes | Yes (but risky) |
| Creation cost | Low (memory copy) | Zero (return reference) | High (new each time) | One-time |
9. Quick Reference Card
| Component | Role | In Document Project |
|---|---|---|
| Prototype Interface | Cloneable | DocumentTemplate implements Cloneable |
| Concrete Prototype | The cacheable, cloneable object | LoanAgreementTemplate, AccountStatementTemplate |
| Prototype Registry | Cache + clone factory | DocumentTemplateRegistry |
| Client | Gets clones, fills data | DocumentService |
Prototype Design Pattern in Java
1. What is Prototype Pattern?
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
| Aspect | Flyweight | Prototype |
|---|---|---|
| What it returns | The SAME cached object (shared reference) | A CLONE/COPY of the cached object (new instance) |
| Object ownership | Shared among all clients | Each client gets its own independent copy |
| Modification safe? | No (modifying affects everyone) | Yes (each clone is independent) |
| Use when | Objects are immutable/read-only | Each client needs a modifiable copy |
2. The 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.
3. The Solution: Clone from Cache
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.
Four Components
| Component | Role |
|---|---|
| a) Prototype Interface | Declares the clone method (Cloneable interface in Java) |
| b) Concrete Prototype | Implements cloning. The actual object that gets cached and cloned. |
| c) Prototype Registry/Factory | Maintains a HashMap cache of prototype objects. Returns clones on request. |
| d) Client | Gets 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)
}
}
5. Shallow Clone vs Deep Clone
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 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 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);
}
}
| Aspect | Shallow Clone | Deep Clone |
|---|---|---|
| Mechanism | super.clone() | Manually clone inner objects or use serialization |
| Primitives | Copied by value | Copied by value |
| Object references | Shared (same reference) | Cloned (independent copy) |
| Performance | Fast (one memory copy) | Slower (multiple copies) |
| Safety | Unsafe if inner objects are mutable | Safe (fully independent) |
| Use when | Object has only primitives/immutables | Object has mutable inner objects |
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
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)
Low-Level Design (LLD)
Project Structure
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
Key observations from the project
- 3 template objects serve 106 document generations. Templates loaded from "DB" once. All subsequent calls clone from cache.
- Deep clone protects the cache.
DocumentFormattingis a mutable nested object. Deep cloning ensures modifying one document's formatting does not affect the template or other documents. - 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. - 100 documents generated in 8ms. Without prototype, loading 100 templates from DB would take 100 x 500ms = 50 seconds. With prototype, it takes 8ms.
- 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
| Scenario | Example | Why Prototype |
|---|---|---|
| Object creation is expensive | Loading from DB, parsing XML, network call | Clone from cache instead of re-creating |
| Many similar objects needed | Game entities, document templates, form copies | Create one prototype, clone for each use |
| Object configuration is complex | Multi-step builder result needs copies | Build once, clone many times |
| Runtime object creation | Objects whose type is determined at runtime | Registry returns clone of correct prototype |
When NOT to use Prototype
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Object creation is cheap | Cloning overhead not worth it | new keyword |
| Objects have circular references | Deep cloning circular references causes infinite recursion | Serialization-based cloning or Builder |
| Few objects needed | Registry overhead for 2-3 objects is excessive | Direct creation |
8. Prototype vs Other Creational Patterns
| Aspect | Prototype | Flyweight | Factory | Singleton |
|---|---|---|---|---|
| Returns | Clone (new object) | Same cached object | New object | Same single object |
| Object count | Many (one per clone) | Few (shared) | Many (one per call) | One |
| Modifiable? | Yes (each clone independent) | No (shared, read-only) | Yes | Yes (but risky) |
| Creation cost | Low (memory copy) | Zero (return reference) | High (new each time) | One-time |
9. Quick Reference Card
| Component | Role | In Document Project |
|---|---|---|
| Prototype Interface | Cloneable | DocumentTemplate implements Cloneable |
| Concrete Prototype | The cacheable, cloneable object | LoanAgreementTemplate, AccountStatementTemplate |
| Prototype Registry | Cache + clone factory | DocumentTemplateRegistry |
| Client | Gets clones, fills data | DocumentService |