Table of contents
- Familiar Does Not Mean Easy
- Singleton — The Constraint of Having Only One
- Iterator — Standardizing Traversal
- Prototype — Creation Through Cloning
- Lessons from These Three Patterns
- Closing the Series
Familiar Does Not Mean Easy
Singleton, Iterator, Prototype. These three patterns are among the first names you encounter when learning design patterns. Singleton is “the pattern that ensures only one instance,” Iterator is “the pattern used in loops,” Prototype is “the pattern that copies” — the one-line summaries are so easy that they discourage deeper thought.
But in practice, things are different. Every time you use Singleton, the question “isn’t this an anti-pattern?” follows. Iterator is already provided by every language, so it is hard to see why it needs to be studied as a pattern. And Prototype triggers bugs between shallow copy and deep copy.
In this part, we pin down the exact intent of these three patterns, where they are actually used, and where problems arise. As the final part of the series, we also include a thread that runs through all 8 parts at the end.
Singleton — The Constraint of Having Only One
Intent
The intent of the Singleton pattern is straightforward. Ensure a class has only one instance and provide a global access point to it. It is used when dealing with objects that only make sense if exactly one exists across the entire program — database connection pools, configuration managers, logging systems.
The problem is how much controversy this simple intent generates during implementation and usage.
Singleton Implementation in Java — Trickier Than You Think
There are several ways to implement Singleton in Java, each with different pros and cons. Let us compare three representative approaches.
flowchart TB
Q{"Multi-threaded\nenvironment?"}
Q -->|No| EAGER["Eager Initialization\n(Created at class loading)"]
Q -->|Yes| Q2{"Is lazy\ninitialization needed?"}
Q2 -->|No| EAGER
Q2 -->|Yes| Q3{"Java? Kotlin?"}
Q3 -->|Java| HOLDER["LazyHolder\n(Inner class approach)"]
Q3 -->|Kotlin| OBJ["object keyword\n(Language-level support)"]
style EAGER fill:#4a90d9
style HOLDER fill:#5ca45c
style OBJ fill:#d4943a
1. Eager Initialization
Creates the instance at the moment the class is loaded.
public class AppConfig {
private static final AppConfig INSTANCE = new AppConfig();
private AppConfig() {
// Block external instantiation
}
public static AppConfig getInstance() {
return INSTANCE;
}
}
The static final field is initialized once by the JVM during class loading, making it thread-safe. It is the simplest approach, but the downside is that the instance is always created even if never actually used. If the creation cost is not significant, this is the safest bet.
2. LazyHolder (Lazy Initialization via Inner Class)
An approach that defers creation until the instance is actually needed while still guaranteeing thread safety.
public class AppConfig {
private AppConfig() {}
private static class Holder {
private static final AppConfig INSTANCE = new AppConfig();
}
public static AppConfig getInstance() {
return Holder.INSTANCE;
}
}
The Holder class is not loaded until getInstance() is called for the first time. The JVM’s class loading mechanism guarantees thread safety, so it is safe without the synchronized keyword. This is the most widely recommended way to implement Singleton in Java.
3. DCL (Double-Checked Locking)
An approach that was once popular but is no longer recommended. It is worth a brief mention because knowing why it is not used is also important.
public class AppConfig {
private static volatile AppConfig instance;
private AppConfig() {}
public static AppConfig getInstance() {
if (instance == null) { // First check
synchronized (AppConfig.class) {
if (instance == null) { // Second check
instance = new AppConfig();
}
}
}
return instance;
}
}
Without the volatile keyword, instruction reordering can cause another thread to see an incompletely initialized object. Adding volatile fixes this, but the code is more complex than LazyHolder and offers virtually no performance advantage, so it is rarely used in practice.
Kotlin’s object — The Language Absorbs the Pattern
In Kotlin, Singleton is built into the language as a keyword. That is the object declaration.
object AppConfig {
var dbUrl: String = "jdbc:postgresql://localhost:5432/mydb"
var maxPoolSize: Int = 10
fun validate(): Boolean {
return dbUrl.isNotBlank() && maxPoolSize > 0
}
}
Declaring with object simultaneously defines the class and creates a single instance. When compiled, it internally generates a static final field + private constructor pattern, so thread safety is guaranteed at the JVM level.
It is accessed directly by class name.
fun main() {
println(AppConfig.dbUrl)
AppConfig.maxPoolSize = 20
println(AppConfig.validate()) // true
}
What took dozens of lines in Java is accomplished with a single line (object) in Kotlin. A prime example of a design pattern being absorbed into a language feature.
Is Singleton an Anti-Pattern?
Criticism of Singleton is fierce. The main arguments are:
- Global State: Singleton is effectively a global variable. Since it is accessible from anywhere, dependencies are hidden rather than explicit in the code. You cannot tell from a class’s constructor alone whether it depends on a Singleton
- Testing difficulty: If Singleton state is shared across tests, tests are not isolated. One test changing the Singleton’s state affects the next
- Increased coupling: If
AppConfig.getInstance()is called directly throughout the code, every call site is affected whenAppConfig’s implementation changes
These criticisms are mostly valid. But it is not Singleton itself that is bad — it is the way Singleton is misused that is bad. The solution is to use DI (Dependency Injection) alongside it.
// Bad example: direct reference
class UserService {
fun getUser(id: String): User {
val db = DatabasePool.getInstance() // Hidden dependency
return db.query("SELECT * FROM users WHERE id = ?", id)
}
}
// Good example: constructor injection
class UserService(private val db: DatabasePool) {
fun getUser(id: String): User {
return db.query("SELECT * FROM users WHERE id = ?", id)
}
}
Even if DatabasePool is a Singleton, injecting it makes the dependency explicit and allows it to be replaced with a Mock during testing. This is exactly why the Spring Framework manages beans as Singleton scope by default while injecting them through DI.
In conclusion, Singleton should be used only where the constraint “there must be exactly one instance” is genuinely needed, and the access mechanism should be wrapped with DI — that is the modern practice.
Iterator — Standardizing Traversal
Traversing Without Knowing the Internals
Arrays, lists, trees, hash maps. These data structures all have different internal structures. Arrays are accessed by index, lists follow node links, trees require recursive traversal, and hash maps iterate through buckets.
The Iterator pattern provides a way to sequentially access elements of a collection without exposing its internal structure. The client just requests “give me the next element,” and does not need to know whether that element is in which slot of an array or at what depth of a tree.
Traversal Sequence Diagram
The process of how an Iterator works, shown as a sequence diagram:
sequenceDiagram
participant Client as Client
participant Collection as Iterable
participant Iter as Iterator
Client->>Collection: iterator()
Collection-->>Client: Return Iterator object
loop While hasNext() is true
Client->>Iter: hasNext()
Iter-->>Client: true
Client->>Iter: next()
Iter-->>Client: Next element
end
Client->>Iter: hasNext()
Iter-->>Client: false
When the client requests iterator() from a collection, the collection returns an Iterator object capable of traversing its internal structure. The client checks with hasNext() whether there are remaining elements and retrieves the next element with next(). Following this protocol enables traversing any data structure in the same way.
Java/Kotlin’s Iterable and Iterator
Java standardized this pattern with the java.util.Iterator and java.lang.Iterable interfaces. Kotlin uses the same interfaces but provides more convenient syntax.
First, let us look at Java’s Iterator interface structure.
public interface Iterator<E> {
boolean hasNext();
E next();
}
public interface Iterable<T> {
Iterator<T> iterator();
}
Implementing Iterable enables the for-each loop. ArrayList, HashSet, LinkedList, and all other Java collections implement Iterable.
Building an Iterator from scratch makes the pattern’s structure clear. Here is a simple example in Kotlin that iterates over integers in a given range.
class IntRange(private val start: Int, private val end: Int) : Iterable<Int> {
override fun iterator(): Iterator<Int> = IntRangeIterator()
private inner class IntRangeIterator : Iterator<Int> {
private var current = start
override fun hasNext(): Boolean = current <= end
override fun next(): Int {
if (!hasNext()) throw NoSuchElementException()
return current++
}
}
}
Since IntRange implements Iterable, it can be used directly in a for loop.
fun main() {
val range = IntRange(1, 5)
for (num in range) {
print("$num ") // 1 2 3 4 5
}
}
Kotlin’s for (x in collection) syntax is internally transformed into code that calls iterator() -> hasNext() -> next(). The Iterator pattern operates even when the developer is unaware of it.
Custom Data Structures and Iterator
The true value of the Iterator pattern shines when building custom data structures. Let us create an Iterator that traverses a binary tree in inorder traversal.
class BinaryTree<T : Comparable<T>> : Iterable<T> {
private var root: Node<T>? = null
data class Node<T>(val value: T, var left: Node<T>? = null, var right: Node<T>? = null)
fun insert(value: T) {
root = insertRecursive(root, value)
}
private fun insertRecursive(node: Node<T>?, value: T): Node<T> {
if (node == null) return Node(value)
if (value < node.value) node.left = insertRecursive(node.left, value)
else if (value > node.value) node.right = insertRecursive(node.right, value)
return node
}
override fun iterator(): Iterator<T> = InorderIterator(root)
private class InorderIterator<T>(root: Node<T>?) : Iterator<T> {
private val stack = ArrayDeque<Node<T>>()
init {
pushLeft(root)
}
private fun pushLeft(node: Node<T>?) {
var current = node
while (current != null) {
stack.addLast(current)
current = current.left
}
}
override fun hasNext(): Boolean = stack.isNotEmpty()
override fun next(): T {
if (!hasNext()) throw NoSuchElementException()
val node = stack.removeLast()
pushLeft(node.right)
return node.value
}
}
}
The inorder traversal is implemented non-recursively using a stack. pushLeft() follows left children all the way down, pushing them onto the stack, and next() pops from the stack while processing the right subtree.
Now the binary tree can be traversed with a for loop just like a regular list.
fun main() {
val tree = BinaryTree<Int>()
listOf(5, 3, 7, 1, 4, 6, 8).forEach { tree.insert(it) }
for (value in tree) {
print("$value ") // 1 3 4 5 6 7 8 (sorted order)
}
}
From the client’s perspective, there is no way to tell whether this is a tree or a list. The complex traversal logic of the tree is hidden behind the standard Iterable interface. This is the core value of the Iterator pattern.
How Iterator Has Merged Into Languages
In modern languages, the Iterator pattern is so deeply embedded that awareness of it as a “pattern” has faded. Kotlin’s Sequence, Java’s Stream, and Python’s generators are all variations of the Iterator pattern.
In Kotlin, the sequence builder lets you create a lazily evaluated Iterator.
val fibonacci = sequence {
var a = 0L
var b = 1L
while (true) {
yield(a)
val next = a + b
a = b
b = next
}
}
fibonacci is an infinite sequence, but computation only happens when values are actually pulled. yield() corresponds to Iterator’s next(), and the coroutine maintains state, returning the next value on each call.
fun main() {
fibonacci.take(10).forEach { print("$it ") }
// 0 1 1 2 3 5 8 13 21 34
}
When a pattern is absorbed into a language feature, the boilerplate disappears, and the developer can focus solely on “what to traverse.”
Prototype — Creation Through Cloning
Why Cloning Is Needed
Sometimes creating an object is expensive. It may require reading data from a database, going through a complex setup process, or making network calls. If you need multiple variants of such an object, creating each from scratch is wasteful.
The Prototype pattern creates new objects by cloning an existing one. You keep one original (prototype) and, whenever needed, clone it and modify only the parts that differ.
Shallow Copy and Deep Copy
The trickiest issue in the Prototype pattern is the depth of copying.
Shallow Copy: Copies the field values as-is. Primitive types get their values copied, but reference types only get their addresses copied. The original and clone end up sharing the same objects.
Deep Copy: Recursively creates new instances for all referenced objects. The original and clone become completely independent.
This is why Java’s clone() method is notorious.
public class Document implements Cloneable {
private String title;
private List<String> tags; // Reference type
@Override
public Document clone() {
try {
Document copy = (Document) super.clone();
// super.clone() only does a shallow copy!
// tags still references the same list as the original
copy.tags = new ArrayList<>(this.tags); // Deep copy needed
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
Since super.clone() performs a shallow copy, failing to create a new tags list means modifying the original’s tags also affects the clone. As fields multiply and nested references deepen, it becomes easy to miss something.
The design of the Cloneable interface is also problematic. This interface contains no methods at all. It serves only as a marker that says “it is okay to call clone().” The clone() method is defined in Object and is protected, so calling it from outside requires an override. Joshua Bloch’s description in Effective Java — “Cloneable is a broken interface” — captures this context.
Kotlin’s data class copy()
Kotlin’s data class automatically generates a copy() method. This method is effectively the modern implementation of the Prototype pattern.
data class GameCharacter(
val name: String,
val level: Int,
val hp: Int,
val skills: List<String>
)
Using copy(), you can create a new object based on the original’s values with only some fields changed.
fun main() {
val warrior = GameCharacter(
name = "Warrior",
level = 10,
hp = 1000,
skills = listOf("Slash", "Block")
)
// Create a variant with different name and skills
val mage = warrior.copy(
name = "Mage",
hp = 600,
skills = listOf("Fireball", "Teleport")
)
println(warrior) // GameCharacter(name=Warrior, level=10, hp=1000, skills=[Slash, Block])
println(mage) // GameCharacter(name=Mage, level=10, hp=600, skills=[Fireball, Teleport])
}
copy() uses the new value only for explicitly changed fields, keeping the rest from the original. level stays at 10, the same as warrior.
But there is a trap here too. copy() performs a shallow copy.
data class Team(
val name: String,
val members: MutableList<String>
)
fun main() {
val original = Team("Team A", mutableListOf("Alice", "Bob"))
val copied = original.copy(name = "Team B")
copied.members.add("Charlie")
println(original.members) // [Alice, Bob, Charlie] <- Original is also modified!
}
Because members is a MutableList and copy() only copies the reference, adding a member to the clone also affects the original. There are two ways to avoid this.
Approach 1: Use immutable collections. The simplest and recommended method. Using List instead of MutableList prevents the clone from modifying the original. When modification is needed, assign a new list.
data class Team(
val name: String,
val members: List<String> // Immutable
)
val original = Team("Team A", listOf("Alice", "Bob"))
val copied = original.copy(
name = "Team B",
members = original.members + "Charlie" // Creates a new list
)
Approach 2: Manual deep copy. When mutable collections are truly necessary, create new collections at copy time.
fun Team.deepCopy(name: String = this.name): Team {
return Team(
name = name,
members = this.members.toMutableList() // Creates a new list
)
}
In practice, using immutable objects wherever possible is the best way to fundamentally eliminate copy issues. Since values do not change, the result is the same whether the copy is shallow or deep.
When Prototype Is Useful
Here are the specific situations where the Prototype pattern shines.
- Configuration templates: Create one base configuration object and clone it with minor modifications for each environment (development/staging/production)
- Game development: Keep archetypes for enemy characters or items and mass-produce copies with slightly tweaked stats
- Document templates: Clone a base document form and fill in just the content
The key is situations where you need “multiple objects that are mostly the same but slightly different.” In such cases, cloning a prototype and modifying only the differences is far more concise than calling a constructor and filling in every field each time.
Lessons from These Three Patterns
Singleton, Iterator, and Prototype are among the most used and most transformed patterns in the GoF catalog. A common lesson can be read across all three.
Patterns evolve with the language.
Singleton required dozens of lines of boilerplate in Java but shrunk to a single object line in Kotlin. Iterator was absorbed into language features like for-each, Stream, and Sequence. Prototype’s clone() was replaced by Kotlin’s data class copy().
When the GoF cataloged design patterns in 1994, these patterns had to be implemented manually. Thirty years later, some have become language keywords or been absorbed into standard libraries. The real reason to study patterns is not to memorize boilerplate code. It is to understand the problems they were trying to solve. If you understand the problem, when you discover a modern tool that solves it better, you can recognize “ah, that is that pattern.”
Closing the Series
Over 8 parts, we have examined the major GoF design patterns. Strategy, State, Observer, Command, Template Method, Chain of Responsibility, Factory Method, Abstract Factory, Builder, Decorator, Proxy, Adapter, Facade, Composite, Singleton, Iterator, Prototype. Even just listing the names, it is quite a lot.
The principle that runs through all these patterns is ultimately one thing. Separate what changes from what does not. Strategy separated algorithms because they change. Observer separated observers because they change. Factory separated creation logic because it changes. Adapter inserted a middle layer because interfaces did not match. Composite unified the difference between parts and wholes through an interface. The names differ, but the fundamental question is the same: “What part of this code might change?”
There is a common trap when studying patterns. Applying patterns becomes the goal itself. You use patterns because the code has a problem — finding a problem in order to use a pattern is getting the order backwards. If the code is simple enough, no pattern is needed, and that simplicity is often a virtue in itself.
Design patterns are also a shared language for design. When you say “let’s apply Strategy here,” every team member who knows the pattern can visualize the resulting structure. Knowing patterns is not only about writing better code, but also about reading others’ design intent and precisely conveying your own.




Loading comments...