Loading...
28 interactive patterns with animated diagrams, code examples, and interview tips.
One instance to rule them all
You need exactly one instance of a class, shared across your entire application. Multiple instances would cause conflicts (database connections, loggers, config managers).
Let subclasses decide what to create
Your code needs to create objects, but the exact type depends on context. Hardcoding `new ConcreteClass()` everywhere makes it impossible to extend without modifying existing code. Adding a new product type means touching every call site.
Families of related objects
You need to create groups of related objects (e.g., UI components for different themes or platforms) that must be used together. Mixing products from different families causes visual inconsistency or runtime errors.
Complex objects, step by step
Constructing a complex object requires many parameters, some optional. A constructor with 10+ parameters is unreadable. Telescoping constructors (overloads) explode in number. You need different representations of the same construction process.
Clone instead of construct
Creating an object from scratch is expensive (e.g., loading config from disk, querying a database, performing complex calculations). You need many similar objects that differ only slightly. Copying fields manually is error-prone and breaks when new fields are added.
Make incompatible interfaces work together
You have existing code that expects one interface, but the library or service you need to integrate provides a completely different interface. Rewriting either side is costly or impossible (third-party code).
Add responsibilities dynamically
You need to add behavior to objects at runtime without altering their class. Subclassing for every combination of behaviors leads to class explosion (LoggingCachingValidatingService, etc.).
Simplified interface to a complex subsystem
A subsystem has many classes with complex interdependencies. Clients must learn and coordinate multiple APIs just to perform common tasks. This coupling makes the system fragile and the client code bloated.
Control access to another object
You need to control access to an object: add lazy loading, access control, logging, or caching without the client knowing. Modifying the real object would violate single responsibility.
Treat parts and wholes uniformly
You have a tree-like hierarchy (files and folders, UI components, org charts). Client code needs to distinguish between leaf nodes and containers, leading to type-checking conditionals everywhere.
Don't call us, we'll call you
Multiple objects need to react when another object changes state. Polling wastes resources; tight coupling makes the system rigid.
Swap algorithms at runtime
You have multiple algorithms for the same task (sorting, compression, routing). Hardcoding them creates a giant if-else chain that's impossible to extend.
Encapsulate requests as objects
You need to parameterize objects with operations, queue requests for later execution, support undo/redo, or log operations for replay. Direct method calls can't be stored, queued, or reversed.
Sequential access without exposing structure
Collections have different internal structures (arrays, trees, graphs, linked lists). Client code that traverses them must know the internal structure, creating tight coupling. Adding a new collection type means rewriting traversal logic everywhere.
Object behavior changes with internal state
An object behaves differently depending on its current state. Code becomes riddled with if/else checks for the current state in every method. Adding a new state means updating every method with another branch.
Separate what you see from what you know
Business logic, UI rendering, and user input handling are tangled together. Changing the UI requires modifying business logic. Testing is impossible without rendering the entire screen. Multiple views of the same data get out of sync.
Abstraction layer over data access
Data access code (SQL queries, API calls, file reads) is scattered throughout business logic. Switching databases or data sources requires modifying every query location. Testing business logic requires a real database.
Loose coupling via events
Services call each other directly, creating tight coupling. Adding a new feature (send an email after payment) requires modifying the payment service. Synchronous calls create cascading failures when downstream services are slow.
Decompose into independent services
A monolithic application grows until it becomes too large for any team to understand. Deployments require coordinating across all features. A bug in one module brings down the entire system. Scaling requires scaling everything, even components that don't need it.
Dependency rule, layers of abstraction
Business rules depend on frameworks, databases, and UI. Changing your ORM requires rewriting business logic. Framework upgrades break domain models. The core value of your application is buried under infrastructure concerns.
Prevent cascade failures
When a downstream service fails, callers keep sending requests that will fail. This wastes resources, increases latency (waiting for timeouts), and can cascade failures through the entire system. A single slow database can bring down all services.
Separate read and write models
A single data model serves both reads and writes. Read queries need denormalized data for performance (joins, aggregations), while writes need normalized data for consistency. Optimizing for one hurts the other. Complex queries slow down the write path.
Manage distributed transactions
A business transaction spans multiple services (payment, inventory, shipping). Traditional database transactions (ACID) don't work across services. If step 3 fails, you need to undo steps 1 and 2, but each runs in a different database.
Store state changes as events
Traditional databases store only current state. You can't answer 'what was the balance last Tuesday?' or 'why did this order get into a bad state?' Audit trails are afterthoughts. Debugging production issues means guessing what happened.
Cross-cutting concerns alongside your service
Every microservice needs logging, monitoring, TLS, retries, circuit breaking, and service discovery. Implementing these in each service duplicates code, creates inconsistency, and couples business logic to infrastructure. Different language services (Go, Node, Python) each need their own implementation.
Decouple production from consumption
Producers generate work faster than consumers can process it (or vice versa). Without buffering, fast producers overwhelm slow consumers, and slow producers leave consumers idle. Direct coupling means both must run at the same speed.
Reuse threads for tasks
Creating a new thread (or process) for every task is expensive: thread creation takes time, threads consume memory (1-2MB stack each), and too many threads cause context-switching overhead. A web server creating 10,000 threads for 10,000 requests will crash.
Isolated actors communicating via messages
Shared mutable state with locks is the root of concurrency bugs: deadlocks, race conditions, and livelocks. As complexity grows, reasoning about lock ordering becomes impossible. Traditional thread synchronization doesn't scale.