An exhaustive, production-grade masterclass from absolute zero to staff-level software craftsmanship & clean architecture.
Master the SOLID principles, all 23 Gang of Four (GoF) creational, structural, and behavioral patterns, modern TypeScript implementations, enterprise architectural paradigms (Repository, Unit of Work, CQRS, Event Sourcing), and anti-pattern refactoring.
Getting Started β’ Creational Patterns β’ Structural Patterns β’ Behavioral 1 β’ Behavioral 2 β’ Enterprise Architecture β’ Staff Interview Handbook
flowchart LR
S1["Stage 1: SOLID Principles"] --> S2["Stage 2: Creational Patterns"]
S2 --> S3["Stage 3: Structural Patterns"]
S3 --> S4["Stage 4: Behavioral: Events & State"]
S4 --> S5["Stage 5: Behavioral: Workflows"]
S5 --> S6["Stage 6: Enterprise & CQRS"]
S6 --> S7["Stage 7: Staff Interview Playbook"]
| Stage | Focus Domain | Core Concepts & Engineering Outcomes |
|---|---|---|
| Stage 1 | OOP Foundations & SOLID | Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion, Composition over Inheritance. |
| Stage 2 | Creational Patterns | Singleton (Thread-Safe), Factory Method, Abstract Factory, Builder (Fluent interface), Prototype (Deep Cloning). |
| Stage 3 | Structural Patterns | Adapter (Interface translation), Decorator, Facade, Proxy (Virtual, Protection, Caching), Composite (Tree hierarchies), Flyweight. |
| Stage 4 | Behavioral: Events & State | Observer / Pub-Sub, Strategy (Interchangeable algorithms), Command (Undo/Redo stacks), State Machine pattern. |
| Stage 5 | Behavioral: Workflows | Chain of Responsibility (Middleware pipelines), Mediator, Template Method, Iterator, Visitor (Double Dispatch), Memento. |
| Stage 6 | Enterprise Architecture | Repository & Unit of Work, CQRS & Event Sourcing, Dependency Injection Container, Outbox Pattern, BFF (Backend For Frontend). |
| Stage 7 | Staff Engineer Interview Playbook | Pattern Selection Decision Trees, 6 Anti-Patterns & Refactoring, 25 Staff Interview Q&As, Pattern Matrix Cheat Sheet. |
- Stage 1: Object-Oriented Foundations & The SOLID Principles
- Stage 2: Creational Design Patterns
- Stage 3: Structural Design Patterns
- Stage 4: Behavioral Patterns - Events, State & Strategy
- Stage 5: Behavioral Patterns - Workflow & Traversal
- Stage 6: Enterprise Architectural Patterns
- Stage 7: Staff Software Engineer Interview Handbook
Every architectural decision in software engineering boils down to balancing two fundamental forces:
flowchart TD
subgraph BadArchitecture["Low Cohesion + High Coupling (Rigid & Fragile)"]
GodClass["God Class: UserOrderPaymentNotifier"] --> DB["Direct DB Access"]
GodClass --> Stripe["Direct Stripe API"]
GodClass --> SMTP["Direct SMTP Socket"]
end
subgraph CleanArchitecture["High Cohesion + Low Coupling (Flexible & Testable)"]
UserSvc["User Service"] --> UserRepo["User Repository (Interface)"]
OrderSvc["Order Service"] --> PaymentGateway["Payment Gateway (Interface)"]
Notifier["Notification Service"] --> EmailSender["Email Sender (Interface)"]
end
- Cohesion: How focused and single-purposed a module or class is. High cohesion means elements belong together and collaborate on a unified responsibility.
- Coupling: The degree of direct dependency between separate modules. Low coupling means changes to Class A do not break or force modifications in Class B.
Inheritance (extends) creates the strongest possible coupling in object-oriented programming:
- Fragile Base Class Problem: Changing a single method in a parent class can inadvertently corrupt the invariants of dozens of child subclasses.
- Inflexible Taxonomies: A
Duckinheriting fromBirdandFlyercannot adapt if you introduce aToyRubberDuck(does not fly) or anAirplane(flies, but is not a bird). - The Solution: Favor Composition over Inheritance. Instead of asking "What IS this object?", ask "What BEHAVIORS DOES IT HAVE?". Compose classes using interfaces injected via constructors.
Coined by Robert C. Martin ("Uncle Bob"), the 5 SOLID principles establish the foundation of maintainable code:
| Letter | Acronym | Core Mandate | Architectural Benefit |
|---|---|---|---|
| S | Single Responsibility Principle (SRP) | A class should have one, and only one, reason to change. | Isolates bug fixes; prevents merge collisions across teams. |
| O | Open/Closed Principle (OCP) | Software entities should be open for extension, but closed for modification. | Add new features by writing new classes, not editing existing battle-tested code. |
| L | Liskov Substitution Principle (LSP) | Subtypes must be substitutable for their base types without altering correctness. | Eliminates unexpected runtime exceptions and defensive instanceof checks. |
| I | Interface Segregation Principle (ISP) | Clients should not be forced to depend on methods they do not use. | Lean, focused interfaces prevent bloated implementations. |
| D | Dependency Inversion Principle (DIP) | High-level modules should not depend on low-level modules; both should depend on abstractions. | Decouples business logic from external frameworks, databases, and third-party APIs. |
// VIOLATION: God class violating SRP, OCP, and DIP
class BadOrderProcessor {
process(order: any) {
// 1. Calculates tax (Violates SRP)
const tax = order.amount * 0.1;
// 2. Charges card with hardcoded Stripe dependency (Violates DIP)
const stripe = new StripeClient("secret_key");
stripe.charge(order.amount + tax);
// 3. Sends raw SMTP email (Violates SRP)
sendEmail(order.userEmail, "Your order is confirmed!");
}
}
// CLEAN ARCHITECTURE: SOLID Compliance with Injected Abstractions
export interface PaymentProcessor {
charge(amount: number): Promise<boolean>;
}
export interface TaxCalculator {
calculate(amount: number): number;
}
export interface NotificationService {
notify(recipient: string, message: string): Promise<void>;
}
export class OrderProcessor {
constructor(
private taxCalc: TaxCalculator,
private payment: PaymentProcessor,
private notifier: NotificationService
) {}
async processOrder(orderId: string, amount: number, email: string): Promise<void> {
const tax = this.taxCalc.calculate(amount);
const total = amount + tax;
const success = await this.payment.charge(total);
if (success) {
await this.notifier.notify(email, `Order ${orderId} confirmed for $${total.toFixed(2)}`);
}
}
}The Law of Demeter (LoD) states that an object should only talk to its immediate friends and not know about the inner details of its friends' friends.
- The "Train Wreck" Code Smell:
order.getCustomer().getAddress().getCity().getZipCode()- Highly brittle. If the customer schema changes how address or city is represented, code throughout the application breaks.
- The Fix (Tell, Don't Ask):
order.getDeliveryZipCode()- The
Orderdelegates toCustomer, which delegates toAddress. Callers only interact with their immediate dependency!
- The
Guarantees a class has strictly one instance throughout the application lifecycle and provides a global access point to it:
export class DatabaseConnectionPool {
private static instance: DatabaseConnectionPool | null = null;
private isConnected: boolean = false;
// Private constructor prevents direct 'new DatabaseConnectionPool()' instantiation
private constructor() {
this.isConnected = true;
}
// Thread-safe Lazy Initialization
public static getInstance(): DatabaseConnectionPool {
if (!DatabaseConnectionPool.instance) {
DatabaseConnectionPool.instance = new DatabaseConnectionPool();
}
return DatabaseConnectionPool.instance;
}
public query(sql: string): string {
return `Executing [${sql}] on singleton pool.`;
}
}- Factory Method: Defines an interface for creating an object, but lets subclasses decide which class to instantiate (e.g.
Logistics.createTransport() -> Truck | Ship). - Abstract Factory: Provides an interface for creating families of related or dependent objects without specifying their concrete classes:
flowchart TD
Client["Client Code"] --> GUIFactory["Abstract Factory: GUIFactory"]
GUIFactory --> WinFactory["WindowsFactory"]
GUIFactory --> MacFactory["MacFactory"]
WinFactory --> WinBtn["WindowsButton"]
WinFactory --> WinCheckbox["WindowsCheckbox"]
MacFactory --> MacBtn["MacButton"]
MacFactory --> MacCheckbox["MacCheckbox"]
Separates the construction of a complex object from its representation, allowing the same construction process to create various representations:
export class HttpRequest {
constructor(
public url: string,
public method: string,
public headers: Record<string, string>,
public body?: string,
public timeoutMs?: number
) {}
}
export class HttpRequestBuilder {
private url: string = '';
private method: string = 'GET';
private headers: Record<string, string> = {};
private body?: string;
private timeoutMs: number = 5000;
setUrl(url: string): this {
this.url = url;
return this;
}
setMethod(method: 'GET' | 'POST' | 'PUT' | 'DELETE'): this {
this.method = method;
return this;
}
setHeader(key: string, value: string): this {
this.headers[key] = value;
return this;
}
setBody(body: string): this {
this.body = body;
return this;
}
build(): HttpRequest {
if (!this.url) throw new Error("URL is required to build HttpRequest");
return new HttpRequest(this.url, this.method, this.headers, this.body, this.timeoutMs);
}
}Enables copying existing objects without making your code dependent on their concrete classes (using clone()):
export interface Cloneable<T> {
clone(): T;
}
export class UserPermissions implements Cloneable<UserPermissions> {
constructor(public roles: string[], public metadata: Record<string, any>) {}
clone(): UserPermissions {
// Deep clone arrays and nested objects
return new UserPermissions([...this.roles], JSON.parse(JSON.stringify(this.metadata)));
}
}Converts the interface of a class into another interface clients expect, allowing incompatible classes to work together:
// Legacy third-party payment library
class StripeLegacySDK {
makePaymentInCents(cents: number, customerToken: string): boolean {
return true;
}
}
// Modern enterprise unified interface
export interface UnifiedPaymentGateway {
pay(dollars: number, customerId: string): Promise<boolean>;
}
// Adapter bridges the gap cleanly
export class StripeAdapter implements UnifiedPaymentGateway {
constructor(private legacySdk: StripeLegacySDK) {}
async pay(dollars: number, customerId: string): Promise<boolean> {
const cents = Math.round(dollars * 100);
return this.legacySdk.makePaymentInCents(cents, customerId);
}
}Attaches additional responsibilities to an object dynamically without subclassing:
flowchart LR
Component["Base: SimpleDataService"] --> Decorator1["LoggingDecorator"]
Decorator1 --> Decorator2["CachingDecorator"]
Decorator2 --> Decorator3["EncryptionDecorator"]
export interface DataService {
fetchData(id: string): Promise<string>;
}
export class SimpleDataService implements DataService {
async fetchData(id: string): Promise<string> {
return `Raw data for ${id}`;
}
}
export class CachingDecorator implements DataService {
private cache: Map<string, string> = new Map();
constructor(private wrappee: DataService) {}
async fetchData(id: string): Promise<string> {
if (this.cache.has(id)) {
return `[CACHED] ${this.cache.get(id)}`;
}
const data = await this.wrappee.fetchData(id);
this.cache.set(id, data);
return data;
}
}Provides a surrogate or placeholder for another object to control access to it:
| Proxy Type | Purpose | Production Example |
|---|---|---|
| Virtual Proxy | Lazy Loading | Delays loading massive 50MB image until first render. |
| Protection Proxy | Access Control | Checks RBAC permissions before delegating to document deletion. |
| Remote Proxy | Network Marshalling | Represents a microservice or gRPC client stub residing on a remote host. |
| Caching Proxy | Response Memoization | Caches expensive database query results. |
export interface FileSystemItem {
getName(): string;
getSize(): number;
render(indent: string): void;
}
export class FileItem implements FileSystemItem {
constructor(private name: string, private size: number) {}
getName(): string { return this.name; }
getSize(): number { return this.size; }
render(indent: string): void {
console.log(`${indent}π ${this.name} (${this.size} KB)`);
}
}
export class DirectoryItem implements FileSystemItem {
private children: FileSystemItem[] = [];
constructor(private name: string) {}
add(item: FileSystemItem): this {
this.children.push(item);
return this;
}
getName(): string { return this.name; }
// Recursively aggregates size of all nested subdirectories and files!
getSize(): number {
return this.children.reduce((total, child) => total + child.getSize(), 0);
}
render(indent: string): void {
console.log(`${indent}π ${this.name}/ (${this.getSize()} KB total)`);
for (const child of this.children) {
child.render(indent + " ");
}
}
}| Component | Role in Composite | Polymorphic Guarantee |
|---|---|---|
FileSystemItem |
Component Interface | Declares common operations (getSize, render) for both leaves and composites. |
FileItem |
Leaf Node | Represents terminal elements that have no children; executes work directly. |
DirectoryItem |
Composite Node | Holds child items; delegates operations recursively down the tree hierarchy. |
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified automatically:
flowchart TD
Subject["Subject (EventEmitter / OrderService)"] -->|Notify State Change| Obs1["Email Notification Subscriber"]
Subject -->|Notify State Change| Obs2["Analytics Tracking Subscriber"]
Subject -->|Notify State Change| Obs3["Inventory Reservation Subscriber"]
export interface Observer<T> {
update(data: T): void;
}
export class Subject<T> {
private observers: Observer<T>[] = [];
attach(observer: Observer<T>): void {
this.observers.push(observer);
}
detach(observer: Observer<T>): void {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data: T): void {
for (const observer of this.observers) {
observer.update(data);
}
}
}Defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime:
export interface DiscountStrategy {
calculate(originalPrice: number): number;
}
export class RegularCustomerDiscount implements DiscountStrategy {
calculate(price: number): number {
return price; // 0% discount
}
}
export class VIPCustomerDiscount implements DiscountStrategy {
calculate(price: number): number {
return price * 0.8; // 20% discount
}
}
export class ShoppingCart {
constructor(private discountStrategy: DiscountStrategy) {}
setDiscountStrategy(strategy: DiscountStrategy): void {
this.discountStrategy = strategy;
}
checkout(total: number): number {
return this.discountStrategy.calculate(total);
}
}Encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations:
export interface Command {
execute(): void;
undo(): void;
}
export class DocumentEditor {
private content: string = "";
write(text: string): void {
this.content += text;
}
delete(count: number): string {
const deleted = this.content.slice(-count);
this.content = this.content.slice(0, -count);
return deleted;
}
getText(): string {
return this.content;
}
}
export class WriteTextCommand implements Command {
constructor(private editor: DocumentEditor, private text: string) {}
execute(): void {
this.editor.write(this.text);
}
undo(): void {
this.editor.delete(this.text.length);
}
}export interface DocumentState {
publish(doc: DocumentWorkflow): void;
render(doc: DocumentWorkflow): string;
}
export class DocumentWorkflow {
private state: DocumentState;
constructor() {
this.state = new DraftState();
}
setState(state: DocumentState): void {
this.state = state;
}
publish(): void {
this.state.publish(this);
}
render(): string {
return this.state.render(this);
}
}
export class DraftState implements DocumentState {
publish(doc: DocumentWorkflow): void {
console.log("Submitting draft for moderation review...");
doc.setState(new ModerationState());
}
render(doc: DocumentWorkflow): string {
return "[DRAFT] Watermarked Preview Mode";
}
}
export class ModerationState implements DocumentState {
publish(doc: DocumentWorkflow): void {
console.log("Approved by moderation! Publishing document live...");
doc.setState(new PublishedState());
}
render(doc: DocumentWorkflow): string {
return "[MODERATION] Pending Editor Approval";
}
}
export class PublishedState implements DocumentState {
publish(doc: DocumentWorkflow): void {
console.log("Document is already published and live.");
}
render(doc: DocumentWorkflow): string {
return "[PUBLIC] Live Production Document";
}
}| State Class | Allowed Transition | Invariant Enforced |
|---|---|---|
DraftState |
Advances strictly to ModerationState. |
Prevents unreviewed drafts from becoming publicly accessible. |
ModerationState |
Advances to PublishedState or reverts to DraftState. |
Requires editor sign-off. |
PublishedState |
Terminal live state (or triggers archive). | Read-only public presentation. |
Passes requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain:
flowchart LR
Request["HTTP Request"] --> Auth["AuthHandler"]
Auth -->|Valid Token| RateLimit["RateLimitHandler"]
RateLimit -->|Within Quota| Validate["ValidationHandler"]
Validate -->|Payload Clean| Controller["API Controller"]
export abstract class AbstractHandler {
private nextHandler: AbstractHandler | null = null;
setNext(handler: AbstractHandler): AbstractHandler {
this.nextHandler = handler;
return handler;
}
handle(request: any): boolean {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return true;
}
}
export class AuthMiddleware extends AbstractHandler {
handle(request: any): boolean {
if (!request.token) {
return false; // Break chain!
}
return super.handle(request);
}
}Reduces chaotic dependencies between objects by forcing them to communicate solely through a mediator object (e.g. Air Traffic Controller):
flowchart TD
Airplanes["Airplane 1, 2, 3..."] <--> ATC["Air Traffic Controller (Mediator)"]
ATC <--> Runway["Runway Resource"]
export interface ExpressionVisitor<R> {
visitNumber(expr: NumberLiteral): R;
visitBinary(expr: BinaryExpression): R;
}
export interface Expression {
accept<R>(visitor: ExpressionVisitor<R>): R;
}
export class NumberLiteral implements Expression {
constructor(public value: number) {}
accept<R>(visitor: ExpressionVisitor<R>): R {
return visitor.visitNumber(this); // First dispatch on expression type
}
}
export class BinaryExpression implements Expression {
constructor(
public left: Expression,
public operator: '+' | '-' | '*',
public right: Expression
) {}
accept<R>(visitor: ExpressionVisitor<R>): R {
return visitor.visitBinary(this); // First dispatch on expression type
}
}
// Visitor 1: Evaluator
export class EvaluatorVisitor implements ExpressionVisitor<number> {
visitNumber(expr: NumberLiteral): number {
return expr.value;
}
visitBinary(expr: BinaryExpression): number {
const left = expr.left.accept(this);
const right = expr.right.accept(this);
switch (expr.operator) {
case '+': return left + right;
case '-': return left - right;
case '*': return left * right;
}
}
}| Visitor Operation | Benefit of Visitor Pattern | Double Dispatch Advantage |
|---|---|---|
EvaluatorVisitor |
Evaluates arithmetic tree. | Adding pretty-printers or optimizers requires zero modifications to Expression classes! |
PrettyPrinterVisitor |
Prints formatted string (2 + (3 * 4)). |
New operations are encapsulated in cohesive visitor classes. |
- Repository Pattern: Mediates between the domain logic and data mapping layers using a collection-like interface for accessing domain objects (
find(),save(),delete()). - Unit of Work: Maintains a list of business objects affected by a business transaction and coordinates writing out changes and resolving concurrency problems.
flowchart TD
Service["Billing Service"] --> UoW["Unit of Work"]
UoW --> UserRepo["User Repository"]
UoW --> InvoiceRepo["Invoice Repository"]
UoW -->|Commit Single DB Transaction| Database["PostgreSQL"]
- CQRS (Command Query Responsibility Segregation): Separates read and update operations for a data store. Commands (
POST/PUT/DELETE) write to a normalized relational database; Queries (GET) read from denormalized Elasticsearch or Redis read-models. - Event Sourcing: Instead of storing just the current state of an entity, stores an immutable append-only log of every domain event that ever happened (
AccountCreated,MoneyDeposited,MoneyWithdrawn). Current state is materialized by replaying events!
flowchart LR
Command["Write Command (Deposit $50)"] --> EventStore["Event Store (Append-Only Log)"]
EventStore --> Projection["Async Projection Engine"]
Projection --> ReadDB["Read Database (Elasticsearch / Redis)"]
Query["Read Query (Get Balance)"] --> ReadDB
type Factory<T> = (container: IoCContainer) => T;
export class IoCContainer {
private services = new Map<string, { factory: Factory<any>; singleton: boolean; instance?: any }>();
registerTransient<T>(token: string, factory: Factory<T>): void {
this.services.set(token, { factory, singleton: false });
}
registerSingleton<T>(token: string, factory: Factory<T>): void {
this.services.set(token, { factory, singleton: true });
}
resolve<T>(token: string): T {
const entry = this.services.get(token);
if (!entry) {
throw new Error(`Service token [${token}] is not registered in IoC container.`);
}
if (entry.singleton) {
if (!entry.instance) {
entry.instance = entry.factory(this);
}
return entry.instance;
}
return entry.factory(this);
}
}| Lifetime Mode | Instantiation Behavior | Production Use Case |
|---|---|---|
Transient |
Creates a new fresh instance on every resolve() call. |
Request handlers, short-lived database transactions. |
Singleton |
Instantiates once on first resolution; reuses across app. | Database connection pools, configuration registries, Redis clients. |
flowchart TD
Start{"What problem are you solving?"} --> Creational{"Object Creation Complexity?"}
Start --> Structural{"Incompatible Interfaces or Composition?"}
Start --> Behavioral{"Communication or Algorithm Flow?"}
Creational -->|Need only one instance| Singleton["Singleton"]
Creational -->|Complex multi-step construction| Builder["Builder"]
Creational -->|Family of related products| AbstractFactory["Abstract Factory"]
Structural -->|Incompatible API or legacy SDK| Adapter["Adapter"]
Structural -->|Adding dynamic behaviors at runtime| Decorator["Decorator"]
Structural -->|Simplifying a complex subsystem| Facade["Facade"]
Behavioral -->|Event notification to multiple listeners| Observer["Observer"]
Behavioral -->|Interchangeable algorithms at runtime| Strategy["Strategy"]
Behavioral -->|Undo/Redo or command queuing| Command["Command"]
| Anti-Pattern | Manifestation in Code | Architectural Damage | Clean Refactoring Solution |
|---|---|---|---|
| God Object | A single massive class with 3,000+ lines doing DB, validation, UI, and networking. | High coupling, impossible to unit test. | Decompose using Single Responsibility Principle and Facade. |
| Golden Hammer | Using the same tool/pattern for every problem (e.g. storing everything in MongoDB). | Performance collapse, impedance mismatch. | Use polyglot persistence and evaluate trade-offs. |
| Poltergeist | Pointless transient classes created just to invoke a method on another class. | Unnecessary cognitive indirection. | Inline or remove the dummy intermediary. |
| Spaghetti Code | Unstructured procedural logic with dozens of nested if/else checks. |
Extreme regression bugs when changing logic. | Refactor using State or Strategy patterns. |
| Hardcoding Singletons | Direct access to Database.getInstance() scattered across all classes. |
Zero testability; cannot mock DB in tests. | Replace with Dependency Injection. |
| Circular Dependency | Class A depends on Class B, which depends on Class A. | Memory leaks, build initialization stalls. | Extract shared interface or introduce a Mediator. |
Q1: What is the Liskov Substitution Principle (LSP) and how is the Classic Rectangle-Square problem a violation?
Answer: LSP states that objects of a superclass should be replaceable with objects of its subclasses without breaking application correctness. In geometry, a Square is a Rectangle. However, if class Square extends Rectangle, and setting width also sets height to maintain equal sides, any client expecting a standard rectangle (rect.setWidth(5); rect.setHeight(10); assert(rect.getArea() === 50)) will fail because setHeight(10) overwrote width to 10, producing an area of 100. This breaks client invariants and violates LSP. The solution is to have both implement a common Shape interface instead of inheriting.
Answer: Structurally, both look identical (they share a context class holding an interface reference). The distinction lies in intent:
- In the Strategy Pattern, the client explicitly configures the context with a specific algorithm (e.g. choose CreditCard vs PayPal discount strategy); strategies are typically independent and unaware of each other.
- In the State Pattern, the context changes its internal state object dynamically in response to events (e.g.
Draft -> Review -> Published), and concrete state classes know about and trigger transitions to succeeding states.
Answer:
- Hidden Global State: It conceals dependencies inside methods rather than exposing them through constructor parameters.
- Breaks Unit Testing: Singletons persist state across test runs, causing tests to be non-hermetic and order-dependent unless complex teardown reflection is used.
- Violates SRP: It controls its own lifecycle and creation in addition to its core business logic.
- Alternative: Register dependencies as singletons inside a Dependency Injection (DI) container instead of implementing static singletons.
Answer: Subclassing adds responsibilities statically at compile time; if you need combinations of 4 features (Logging, Caching, Encryption, Compression), inheritance requires LoggingCachedEncryptedDataService). The Decorator pattern adds responsibilities dynamically at runtime by wrapping instances, requiring only 4 decorator classes that can be nested in any arbitrary order.
Answer: Abstract Factory specializes in creating whole families of related objects immediately in a single method call (factory.createButton(), factory.createWindow()). Builder specializes in constructing a single complex, multifaceted object step-by-step (builder.setCPU().setRAM().setGPU().build()), often allowing different construction paths.
Q6: Explain the Dependency Inversion Principle (DIP) and how it enables Clean / Hexagonal Architecture.
Answer: DIP states that high-level policy (core business domain logic) should not depend on low-level details (SQL databases, REST clients, AWS SDKs). Instead, the core domain defines interfaces (ports), and the low-level infrastructure implements them (adapters). This enables swapping PostgreSQL for MongoDB or MockDB in tests without touching a single line of core business rules.
Answer: The Composite Pattern composes objects into tree structures to represent part-whole hierarchies, allowing clients to treat individual objects and compositions of objects uniformly. Classic examples:
- Graphic UI components: A
ContainerholdsButtonsand childPanels, and callingrender()recursively renders all children. - Filesystems: A
DirectorycontainsFilesand sub-Directories, and callinggetSize()computes total size uniformly.
Answer:
- In Observer, the Subject maintains a direct reference to its Observers and synchronously invokes their notification callback in the same memory process.
- In Pub-Sub, publishers and subscribers never know each other exists; they communicate through a completely separate intermediary Message Broker (e.g. Redis Pub/Sub, Kafka, RabbitMQ), enabling asynchronous cross-network distributed messaging.
Answer: The Visitor pattern allows adding new operations to existing class hierarchies without modifying their source code. Because most object-oriented languages only support single dispatch (routing methods based on the runtime type of the receiver), Visitor uses Double Dispatch: the element accepts the visitor (element.accept(visitor)), and immediately calls back the visitor with itself (visitor.visitElementA(this)), ensuring both the element type and visitor operation are dynamically bound at runtime.
Answer: Flyweight minimizes memory usage by sharing common immutable state among multiple objects. It divides object data into:
- Intrinsic State: Constant, invariant, and shareable across millions of instances (e.g. 3D mesh model, texture).
-
Extrinsic State: Unique to each instance and passed in externally during method invocation (e.g.
$(x,y,z)$ coordinate in a gaming world).
Answer: The Command pattern encapsulates a state mutation as a self-contained object holding the method name, target receiver, and parameter values. In Event Sourcing, every command is validated and converted into an immutable historical event record appended to a log, which can be replayed to reconstruct any historical state.
Answer: An Object Adapter uses composition; it holds an instance of the adaptee class inside its private fields. A Class Adapter uses multiple inheritance (where supported, like C++ or Python); it inherits from both the target interface and the adaptee class simultaneously. Object adapters are favored because they adhere to composition over inheritance.
Answer: ISP dictates that clients should not be forced to depend upon interfaces they do not use. The primary code smell indicating an ISP violation is an implementing class writing dummy throwaway stubs: e.g. throw new UnsupportedOperationException("Method not supported"). The solution is breaking the fat interface into smaller role interfaces.
Answer: A Facade provides a clean, simplified high-level interface to a complex library, framework, or collection of subsystems. Clients interact exclusively with the facade, shielding them from complex internal initialization, class wiring, and dependency chains.
Answer:
- Template Method: Uses inheritance. An abstract base class defines the skeleton algorithm in a
finalmethod, and subclasses override specific hook steps. - Strategy: Uses composition. The algorithm is encapsulated inside an independent strategy class injected into the context.
Answer: Memento captures and externalizes an object's internal state without violating encapsulation, allowing the object to be restored later. Components:
- Originator: The object whose state is being saved.
- Memento: The immutable state snapshot.
- Caretaker: Manages the history of mementos (undo stack) without inspecting memento contents.
Answer: Instead of returning null when an object is not found, the Null Object pattern returns a polymorphic instance that implements the expected interface with neutral, no-op behavior (e.g. NullLogger that does nothing). This eliminates defensive if (obj !== null) checks throughout the codebase.
Answer: Unit of Work tracks all objects modified, inserted, or deleted during a business transaction. At the end of the transaction, it calculates the minimal set of database updates required, batches them efficiently, and executes them inside a single database transaction, eliminating redundant round-trips.
Answer: In microservices, writing to a database and publishing a Kafka event cannot be done in a single distributed transaction. The Outbox pattern writes both the entity update and an outbox message into the same local ACID database transaction. A separate relay process (CDC / Debezium) tails the outbox table and reliably publishes messages to the message broker.
Answer: It decouples the sender of a request from its receivers by giving more than one object a chance to handle the request. Handlers are linked in a chain, and each handler inspects the request and either processes it, modifies it, or delegates it down the chain (e.g. Express.js middleware, filter chains in Spring Security).
Answer: Named after the Australian strangler fig vine, this pattern modernizes a monolithic system by gradually replacing specific functionalities with new microservices behind an API gateway facade. Over time, the new microservices completely replace the old monolith until the old codebase can be cleanly decommissioned with zero downtime.
Answer:
- Shallow Copy: Copies the immediate fields of an object. If a field is a reference to an array or nested object, the copy shares the exact same memory pointer with the original. Modifying the copy mutates the original!
- Deep Copy: Recursively duplicates all nested objects and data structures, creating completely independent memory allocations.
Answer: In a complex form with multiple interdependent dropdowns, checkboxes, and buttons, having each UI element directly call others results in an
Answer: IoC is the broad design principle where the control flow of a program is inverted: instead of custom code calling libraries, a framework calls custom code (the "Hollywood Principle": Don't call us, we'll call you). Dependency Injection is a specific design pattern that implements IoC by passing dependencies into objects rather than having objects construct them internally.
Answer:
- Apply Guard Clauses: Invert conditions and return early to eliminate nested indentation.
- Apply Extract Method: Group cohesive statements into small, descriptive helper functions.
- Replace Conditional with Polymorphism / Strategy: If conditionals switch on an object type, replace with polymorphic subclasses or injected strategies.
| Pattern | Category | Intent & Core Purpose |
|---|---|---|
| Singleton | Creational | Single application-wide instance with global access. |
| Factory Method | Creational | Creates objects via subclass method override. |
| Abstract Factory | Creational | Creates families of related products without concrete classes. |
| Builder | Creational | Step-by-step construction of complex objects. |
| Prototype | Creational | Clones existing objects in memory. |
| Adapter | Structural | Bridges incompatible interfaces. |
| Decorator | Structural | Dynamically wraps objects to add behaviors. |
| Facade | Structural | Simplified entry point to a complex subsystem. |
| Proxy | Structural | Surrogate controlling access, caching, or lazy loading. |
| Composite | Structural | Treats tree hierarchies of objects uniformly. |
| Flyweight | Structural | Shares intrinsic state to conserve massive RAM. |
| Observer | Behavioral | One-to-many event notification to subscribers. |
| Strategy | Behavioral | Interchangeable runtime algorithm family. |
| Command | Behavioral | Encapsulates request as an undoable object. |
| State | Behavioral | State machine changing behavior as state changes. |
| Chain of Resp. | Behavioral | Pipeline of handlers passing requests sequentially. |
| Mediator | Behavioral | Centralized coordinator decoupling peer objects. |
| Template Method | Behavioral | Skeleton algorithm in base class with subclass hooks. |
| Iterator | Behavioral | Sequential traversal of collections without exposing internals. |
| Visitor | Behavioral | Adds new operations to classes via double dispatch. |
| Memento | Behavioral | Captures and restores object state snapshots. |
| Repository | Enterprise | Collection-like abstraction isolating domain from database. |
| Unit of Work | Enterprise | Coordinates transactional atomic commits across repositories. |
| CQRS | Enterprise | Segregates read and write data models. |
| Event Sourcing | Enterprise | Stores state changes as an immutable sequence of events. |
Contributions are welcome! Please review our CONTRIBUTING.md and CODE_OF_CONDUCT.md guidelines before opening issues or submitting pull requests.
This project is open-source software licensed under the MIT License.