Explore patterns and best practices that you can implement in your project!
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different configurations.
- When an object requires multiple configuration steps before it's ready to use
- When you want to avoid constructors with many parameters ("telescoping constructor")
- When the construction order shouldn't matter
FileProcessorBuilder constructs a FileProcessor — an object that reads a file and applies a chain of text transformations (preprocessors).
const processor = new FileProcessorBuilder()
.setFilePath("./data.txt")
.setEncoding("utf-8")
.addPreprocessor((data) => data.toUpperCase())
.addPreprocessor((data) => data.trim())
.build();
const result = await processor.process();FileProcessorBuilder— Accumulates configuration via a fluent API. Validates required fields (e.g.filePath) inbuild()before constructing the product.FileProcessor— The product. Reads a file and applies preprocessors sequentially usingreduce. All fields arereadonlyafter construction.Preprocessor— A simple function type(data: string) => stringthat enables composable text transformations.
- Validation at build time — The builder throws if
filePathis missing, catching misconfiguration before any I/O happens. - Immutable product —
FileProcessorfields arereadonly, ensuring the built object can't be modified after construction. - Composable pipeline — Preprocessors are plain functions, making them easy to test, reuse, and reorder.
The Prototype pattern creates new objects by cloning existing ones, avoiding expensive initialization that would otherwise run on every instantiation.
- When object creation involves a costly operation (file I/O, network calls, heavy computation)
- When you need many similar objects that differ only slightly
- When you want to avoid subclassing just to configure different initial states
Soldier is the base prototype with a clone() method. Sniper extends it with range-based attack logic. The heavy initialization runs once during construction — all subsequent copies are created via clone(), skipping it entirely.
const baseSniper = new Sniper({ speed: 5, strength: 10, range: 250 });
// Heavy operation runs once ^^^
const clone1 = baseSniper.clone(); // no heavy operation
const clone2 = baseSniper.clone(); // no heavy operation
clone1.attack(100); // Attacking 50
clone2.attack(300); // Out of range -> 0Prototype<T>— Interface that enforces aclone(): Tcontract on all cloneable classes.Soldier— Base class implementingPrototype. UsesstructuredClonefor deep copying andObject.setPrototypeOfto preserve the prototype chain.Sniper— Subclass that adds range-based attack logic. Inheritsclone()without needing to override it.
- Deep copy via
structuredClone— Ensures clones are fully independent. Nested objects and arrays are copied, not shared by reference. - Prototype chain preservation —
Object.setPrototypeOfrestores the correct prototype afterstructuredClone, soinstanceofand method resolution work on clones. - Dependency injection for initialization — The
initparameter defaults toheavyInitin production but can be replaced with a no-op in tests, eliminating the need for spies or mocks. - No redundant
clone()overrides — Subclasses inheritclone()fromSoldiersincestructuredClonealready copies all own properties.