A pattern is a named answer to a pressure that keeps recurring. That is the whole subject, and also where it goes wrong: the names are easy to learn and the judgment is not, so most codebases hurt by patterns were hurt by someone applying a real one to a problem they did not have. Each buys something and charges for it, usually in indirection. This guide is about the charge as much as the purchase — what each family is for, what it costs, and how to tell apart shapes that differ only in intent.
Each chapter opens with the short version. Tap one to read the detail.
What a pattern is, and what it costs
~2 min
A pattern is a named solution to a recurring problem in a stated context, together with the forces it balances — not a snippet, not a library. Applied before the variation it absorbs exists it buys pure indirection, which is why more patterns does not mean better design.
Documented patterns share a fixed shape: a name, the context the problem arises in, the solution, and the consequences — the trade-offs you inherit. That last section is the one beginners skip and the one that decides whether the pattern was worth it.
It is also not code you install. A library hands you an implementation and charges a dependency; a pattern is programmed anew into every application, so its cost lands on whoever reads the code next.
Structure never identifies a pattern, and the standing mistake is naming one from a diagram. Suppose a class holding a single collaborator and delegating to it: if the caller chooses that collaborator, this is an interchangeable algorithm; if the object swaps its own as conditions change, it is a state machine; if the collaborator is a stored request run later, it is a captured command. One shape, three designs.
Nor is the catalog a checklist of good design. An often-cited analysis found sixteen of the classic twenty-three become simpler or vanish in languages with richer built-in abstractions, and one of the original catalog's authors later said he would drop the singleton.
Rule: state the force in one sentence before naming a pattern; if you cannot, you are buying indirection.
Creating objects without naming the class
~2 min
Creational patterns hide which concrete classes the system uses and how instances are made. The misconception is that a factory signals maturity: with one implementation it adds a type and a jump and buys nothing, and the singleton is global mutable state with better manners.
Hard-coded construction is inflexible: changing part of the design means reworking the thing that builds it. These patterns remove the concrete class name from the code that needs an instance.
They differ in what they let vary. A factory method lets a subclass decide which single product is built. An abstract factory groups a family behind one interface, so members meant to be used together cannot be mismatched. A builder separates construction from the finished representation, which makes an object with nine optional parts readable — the alternative is a constructor whose positional arguments nobody can decode. A prototype clones a configured exemplar; the trap is that a clone copying references shares mutable state, so the copy is not one.
Suppose exactly one implementation exists. The factory then costs a type, a jump between caller and result, and one more place for wiring to be wrong, and buys nothing until a second product arrives.
The singleton is the same error at a larger scale. Its damage is the global reach, not the single instance: it hides the dependency inside whatever method reads it, makes a class police its own uniqueness alongside its real job, and complicates testing. Reference material now files it under anti-patterns.
Rule: add a creational pattern when the second thing to construct arrives, not in anticipation of it.
Wrappers, and the four things a wrapper is for
~2 min
Most structural patterns wrap something, and the wrapping looks identical, so they are told apart by purpose: convert an interface, simplify one, add behavior, or control access. Every one costs a hop per call and a frame in every stack trace.
An adapter converts an interface a class already has into the one a client expects, touching neither side — retrofitted onto code you cannot edit. A facade goes the other way, inventing a simpler entry point over a complex subsystem that stays available underneath.
A decorator keeps the wrapped object's interface and adds behavior at runtime; being per-object and stackable, it replaces the class-per-combination explosion that subclassing every mixture of features produces. A proxy also keeps the interface, but its job is access: building an expensive object only when first touched, standing in for one in another address space, or refusing a caller without rights. A composite is a tree whose every node presents the same interface, so client code stops asking whether it holds a leaf or a container. A bridge separates an abstraction from its implementation so the two vary independently instead of multiplying.
The misconception is that these are identified by shape. Suppose two wrappers with identical signatures: the one that logs and retries is a decorator, the one that rejects the call unless you are authorized is a proxy.
What they charge is the same either way — a hop per call, longer stack traces, and many small objects to track when something misbehaves.
Rule: say what the wrapper is for — convert, simplify, add, or control — before naming it.
Behavior: who decides what happens next
~2 min
Behavioral patterns distribute the choice of what runs, so the useful question is who chooses: the caller, the object itself, or a request captured earlier. The recurring mistake is treating subscription as the whole of the observer, when teardown is half of it.
A strategy selects one of a family of algorithms at runtime through composition; a template method makes the same choice through inheritance, the base fixing the skeleton and subclasses filling named steps. That is the real difference between them: a decision you can change while running, versus one settled at compile time.
A state object alters its behavior as its internal state changes, with the states choosing their own successors. A command packages an action with its parameters, which enables queueing, logging, thread pools and multi-level undo — undo keeps a stack of executed commands and asks the popped one to reverse itself, so each must carry enough to do that. It costs a class per command and a call site that no longer says what will happen.
An observer keeps a list of dependents and notifies them on change, needing to know nothing but their update method. Suppose a screen subscribes to a data source and is closed without unregistering: the strong reference keeps both alive for the rest of the process. The recurring mistake is stopping at subscription.
A mediator concentrates the knowledge of how peers interact, an iterator externalizes traversal, a visitor lifts an operation out of the classes it runs over.
Rule: ask who selects the next behavior; the answer names the pattern.
Principles: what they buy and what they charge
~2 min
The five principles — single-responsibility, open-closed, Liskov-substitution, interface-segregation and dependency-inversion — are each a bet that one axis will vary. The misconception is that an interface with one implementation is abstraction; it is indirection until a second implementation exists.
Compactly: a class should have no more than one reason to change; software should be open for extension and closed for modification; code holding a base reference must work with a derived one without knowing; clients should not depend on methods they never call; and code should depend on abstractions rather than concretes.
Each is a claim about coupling — how interdependent two modules are — and its partner cohesion, how related one module's parts are. The vocabulary is worth more than the mnemonic. Content coupling is one module reaching into another's internals; common coupling is two modules talking through shared global data; control coupling is passing a flag that selects the callee's branch — suppose a render(item, isPreview) whose second argument picks between two unrelated bodies — which is the callee's design leaking into its caller.
None of this is free, and the common mistake is calling a single-implementation interface abstraction. It is indirection: one more file, one more jump while reading, and no flexibility earned. Composition carries the same bargain — holding a collaborator is black-box reuse, whereas inheritance exposes the parent's internals and is fixed at compile time, and you pay for the switch in forwarding code.
Rule: apply a principle on an axis that has already changed twice.
Dependency injection, and the argument it does not win
~2 min
Injection hands a collaborator in rather than letting the class look it up, and constructor injection is the default because a half-wired object then cannot exist. The misconception is that injection is what makes code testable — the source that named the pattern disagrees.
There are three forms. Constructor injection passes collaborators as constructor arguments, which keeps fields immutable and guarantees a valid object from the moment it exists. Setter injection allows reconfiguration afterwards and copes with a long dependency list, at the price of an object that is briefly incomplete. Interface injection, where a class implements a provider-supplied interface, is invasive and largely out of use.
The real contrast is with a service locator, which the class asks for what it needs. The difference is visibility. Suppose a pricing class needs a tax table: injected, it sits in the constructor where every caller sees it; fetched from a locator, nothing on the class's surface says the table is needed, and every user also acquires a dependency on the locator.
The misconception is that injection is what makes code testable. Both approaches stub equally well, and the source that named the pattern attributes the difficulty people blame on this choice to other design faults. Injection's real advantage appears when you ship a component into applications you do not control.
Its cost is that the object graph must be assembled somewhere, and when a container assembles it from configuration the wiring is no longer something a compiler can check. Passing collaborators by hand is already injection.
Rule: put the dependency in the signature; the wiring mechanism is a detail.
Where the business logic lives
~2 min
A procedure per request is right while logic is thin; objects carrying data and behavior pay off once rules multiply. The misconception is that naming classes after domain nouns produces the second — it produces the mapping bill without the benefit.
A transaction script organizes logic as one procedure per request from the presentation layer, hitting the database directly or through a thin wrapper. It suits applications that really are a series of view-validate-calculate transactions, and is correct while rules stay simple. Growth erodes it, as similar procedures repeat fragments.
A domain model is a web of objects, each standing for something meaningful in the business — an order, a line item, a customer — holding data and the behavior over it. It suits domains where a lot of often-messy logic needs organizing, and it costs a mapping layer between those objects and the database.
Suppose an Order holding nothing but getters and setters, beside an OrderService holding every rule. That is an anemic model: it looks like the second option, so it pays for the mapping layer, while the behavior that layer was bought to organize lives somewhere else entirely. A thin coordinating service layer is fine; the more behavior accumulates there, the less the model is doing.
Presentation is the same discipline one layer out: splitting interface work into model, view and controller separates what the application knows from how it is shown.
Rule: put the rule next to the data it constrains, or admit you are writing procedures and skip the mapping bill.
Objects and rows
~2 min
Object models have collections and inheritance; tables have neither, and the persistence family exists to manage that mismatch. The misconception is that a mapper is simply the grown-up choice — on a schema that mirrors the model it is machinery with nothing to buy.
An active record wraps one row, encapsulates the database access for it, and adds domain logic over that data. Its conditions are explicit: the schema lines up with the object model and the logic stays straightforward. When either stops being true, one class carries both persistence and complicated rules, and every test drags a database along.
A data mapper is a separate layer moving data between objects and tables while keeping them ignorant of each other. Domain objects hold no queries and no schema knowledge — without that independence, a change on either side propagates to the other. The mistake is reaching for it by default: what it buys is the freedom for model and schema to evolve apart, and what it charges is a layer you now own.
Around a mapper sit the patterns that make it usable. A unit of work records every object touched during a business transaction and writes the changes together, because a call per change is needless traffic and holding a database transaction open across a whole user interaction is not workable. A lazy load leaves a marker that fetches on first access — suppose a loop over a lazily loaded collection: it issues one query per element, from a line that looks free.
Rule: keep persistence out of the domain object once schema and model must move independently.
Boundaries: the wire, and edits that outlive a transaction
~2 min
The misconception is that a remote call is a slow method call. Granularity that is free in memory becomes the design across a process boundary — and once an edit spans several requests, the database transaction no longer protects it, which is why offline locking exists.
A data transfer object carries data between processes precisely to reduce the number of calls: it is serializable, holds what one interaction needs, and carries no behavior. An assembler translates between it and the domain objects, so neither side learns the other's shape. The cost is a second set of types to keep in step with the first, so it only pays across a real boundary.
Concurrency is the other boundary. Suppose a business transaction that opens a record, edits it across three screens and saves: it spans several system transactions, so the database alone cannot hold it consistent. Two sessions can lose each other's updates, or read a combination of values that never existed together.
Optimistic offline locking assumes conflicts are rare and validates at commit; the winner writes, the loser is rolled back and redoes the work. Pessimistic locking assumes the opposite, takes the lock up front, and buys safety with restricted concurrency. Neither is the safe default — the choice follows from how often conflicts really occur and how expensive the discarded work would be.
Rule: set call granularity and lock strategy from measured behavior, not from what reads well in memory.
Smells and anti-patterns: signals, not verdicts
~2 min
A smell is a surface indication that usually corresponds to a deeper problem — fast to spot, and deliberately not proof. The misconception is that the list is a lint configuration. An anti-pattern is the stronger claim, and it requires a documented way out.
Smells are defined by being quick to notice. A function that runs too long, a class holding data and no behavior, the same fragment in three places, one change that forces edits across many classes, a parameter list nobody can read. Suppose the same four fields — street, city, region, postcode — handed to nine different functions: spotting that clump takes two seconds, and it is a type asking to be born.
The mistake is reading the list as a lint configuration. Smells do not always indicate a problem, and what counts as one varies by language, by developer and by team; they tell you where to look, not what to do. Eliminating every hit mechanically manufactures churn and calls it quality.
Their value is that someone new can spot a smell without being able to diagnose it, which makes them a shared vocabulary.
An anti-pattern is defined more tightly: a commonly used structure that looked appropriate, cost more than it gave, and has a documented, repeatable way out. Without that second half you have an opinion. The big ball of mud — a system with no discernible architecture — usually arrives through schedule pressure and turnover rather than incompetence.
Rule: act on a smell when you can name the better alternative; otherwise leave a note.
Refactoring: structure, without behavior
~2 min
Refactoring changes internal structure while leaving observable behavior alone, in a series of small steps. Two misconceptions to drop: that the word covers any large rewrite, and that replacing a conditional with polymorphism is automatically the better shape.
The definition is narrow on purpose, and the commonest mistake is using the word for any large rewrite. As a noun it is one change to internal structure that leaves observable behavior untouched; as a verb it is restructuring by applying a series of those. Both halves earn their keep: a catalogd step is small enough to be obviously safe and easy to reverse, and preserving behavior is what leaves a red test with exactly one candidate cause.
The published catalog runs to over seventy named moves, grouped by what they touch — extracting and inlining, encapsulation, moving features between modules, organizing data, simplifying conditional logic, working with inheritance. Their value is not cleverness but that each is small and named, so a large change becomes a sequence you can stop in the middle of.
Replacing a conditional with polymorphism is where judgment enters. Suppose a shipping-cost function switching on a carrier code: the move gives each carrier a class that answers for itself. That is a clear win when the same switch repeats across the codebase, and a clear loss when there is exactly one — one readable branch traded for behavior scattered over five files.
Rule: change structure or behavior, never both in one commit.
Modeling a domain, and the architecture-level bets
~2 min
Domain-driven design centers development on a model carrying the domain's real processes and rules, in language shared with the people who know it. The misconception is that the class stereotypes are the point — adopt them without the shared language and you have renamed some files.
The core commitment is a vocabulary embedded in the software and evolved along with it, instead of a translation step between what the business says and what the code calls things.
The building blocks follow from asking what identity means for each thing you model. An entity is the same thing over time regardless of its field values — an order remains that order after every edit. A value object is equal to another when its fields are, so money is an amount together with its currency, and two identical amounts are interchangeable. Suppose one is mutable and shared by two owners: changing it under one changes it under the other, silently. That aliasing bug is why value objects are kept immutable.
At the edges, a gateway wraps an external resource behind an interface in your vocabulary rather than the vendor's, so the awkward parts stay in one class and can be swapped in tests.
Large domains get bounded contexts — separate models that interoperate, rather than one model forced across every team. Command query responsibility segregation is the cautionary one: the source that popularized it reports most uses it saw made things worse, that it belongs inside a single bounded context, and that it brings eventual consistency.
Rule: adopt an architecture-level pattern for one context you can name.
Written by Keentune. We are not affiliated with or endorsed by the organizations whose documentation informs this guide, and any linked sources belong to their respective owners.
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.