# What Is a Design Pattern > A design pattern is a named, reusable solution shape for a recurring design problem. Learn what one contains and how it differs from an algorithm or library. - kind: guide - category: design-patterns - aliases: Design pattern, Pattern - canonical: https://aipatterns.dev/guides/what-is-a-pattern - inspired-by: https://refactoring.guru/design-patterns/what-is-pattern ## Definition A design pattern is a description of how to arrange classes, objects or functions to solve a problem that keeps coming back in different programs. It is not code you copy. It is a shape you adapt. **A pattern names a recurring problem, the forces that make it hard, and a solution structure that balances those forces.** Think of a pattern as a blueprint for a kind of building, not the building itself. Two teams applying [Observer](https://aipatterns.dev/design-patterns/observer.md) will write different code, in different languages, with different names. A reader who knows the pattern still recognizes both. ## Pattern vs algorithm vs library | Thing | What it gives you | Example | |---|---|---| | Algorithm | Exact steps that produce a result | Binary search, Dijkstra | | Library | Working code you call | `lodash`, `java.util.concurrent` | | Pattern | A structure of roles and relationships | [Strategy](https://aipatterns.dev/design-patterns/strategy.md), [Adapter](https://aipatterns.dev/design-patterns/adapter.md) | | Idiom | A language-specific habit | Python context managers, Go `defer` | An algorithm tells you *what to compute*. A pattern tells you *who should know about whom*. You can implement the same algorithm with or without a pattern, and the same pattern around many algorithms. ## What a pattern description contains Every pattern page on this site follows the same structure, because an agent should find the same facts in the same place: - **Intent**: the problem and the solution in two or three sentences. - **Forces**: the competing pressures. Without forces, a pattern reads like a rule; with them, you can tell when it does not apply. - **Therefore**: the solution in one sentence. - **Structure**: the participants (roles) and how they connect, as a class diagram. - **Use it when** and **Do not use it when**: concrete triggers, and the simpler alternative for each misfit. - **Example**: one realistic scenario in four languages. - **Trade-offs** and **Relations**: what you pay, and which neighbors to compare against. The "Do not use it when" section matters most. See [Using this reference as an agent](https://aipatterns.dev/guides/for-agents.md). ## Roles, not classes A pattern describes *roles*. In [Strategy](https://aipatterns.dev/design-patterns/strategy.md), the roles are context, strategy interface and concrete strategy. One class can play more than one role, and a role can be played by something that is not a class at all: ```ts // The "strategy" role played by a plain function type. type PriceRule = (subtotal: number) => number; const tenPercentOff: PriceRule = (s) => s * 0.9; const noDiscount: PriceRule = (s) => s; function checkout(subtotal: number, rule: PriceRule): number { return rule(subtotal); } checkout(200, tenPercentOff); // 180 ``` There is no `interface` keyword and no class here. The pattern is still present: a caller delegates a varying step to an interchangeable value. Recognizing patterns by role instead of by syntax keeps you from adding ceremony that the language already makes unnecessary. ## Levels of scale Patterns exist at several sizes: - **Idioms**: tied to one language. Not covered here. - **Design patterns**: a handful of collaborating objects. The 22 entries in this catalog. - **Architectural patterns**: the shape of a whole system, such as layered architecture, ports and adapters, or event sourcing. The classic catalog sorts design patterns into three groups by purpose. See [Classification of patterns](https://aipatterns.dev/guides/classification-of-patterns.md), or browse [creational](https://aipatterns.dev/design-patterns/creational.md), [structural](https://aipatterns.dev/design-patterns/structural.md) and [behavioral](https://aipatterns.dev/design-patterns/behavioral.md) directly. ## A worked recognition You find this in a codebase: ```ts class ReportExporter { export(report: Report, format: "csv" | "pdf" | "xlsx"): Buffer { if (format === "csv") { /* 40 lines */ } else if (format === "pdf") { /* 60 lines */ } else { /* 50 lines */ } } } ``` The problem is recurring (pick one of several algorithms at runtime). The forces are present (new formats keep arriving, each branch is hard to test alone). That matches the intent of [Strategy](https://aipatterns.dev/design-patterns/strategy.md). It also matches the [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md) smell, and the refactoring path is [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md). If there were only two formats and no third in sight, the right call is to leave the `if` alone. A pattern is a response to forces, not a goal. ## Key points for agents - Match on the problem and forces, never on surface keywords in the request. - A pattern is a vocabulary item. Naming it in a commit message or comment ("adapter for the legacy billing API") tells the next reader a lot in a few words. - Prefer the smallest structure that plays the needed roles. Functions, closures and modules count. - Read [Choosing a pattern](https://aipatterns.dev/guides/choosing-a-pattern.md) before introducing one, and [Criticism of patterns](https://aipatterns.dev/guides/criticism-of-patterns.md) to know the failure modes. --- # History of Patterns > Where design patterns came from, from Christopher Alexander's architecture books to the Gang of Four catalog, and what changed in how they are used today. - kind: guide - category: design-patterns - aliases: Gang of Four, GoF - canonical: https://aipatterns.dev/guides/history-of-patterns - inspired-by: https://refactoring.guru/design-patterns/history ## Origins in building architecture The idea of a pattern language predates software. In the 1970s, the architect Christopher Alexander and his colleagues published *A Pattern Language* and *The Timeless Way of Building*. They described recurring problems in towns and buildings, such as where to put windows in a room people sit in, and named a solution for each. Each of Alexander's patterns had the same parts: a context, a problem shaped by competing forces, and a solution that resolves those forces. The patterns linked to each other, so small ones refined larger ones. That structure, more than any single pattern, is what software borrowed. ## Arrival in software In the late 1980s, Kent Beck and Ward Cunningham experimented with applying Alexander's ideas to user interface design in Smalltalk. Others in the object-oriented community picked up the idea through workshops and conferences such as OOPSLA. The turning point came in 1994 with *Design Patterns: Elements of Reusable Object-Oriented Software* by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. The four authors became known as the Gang of Four (GoF). Their book catalogued 23 patterns with examples in C++ and Smalltalk and grouped them by purpose into creational, structural and behavioral. This site covers 22 of those patterns. The 23rd, Interpreter, is rarely used outside language tooling and is left out of the catalog. ## Timeline | Period | Milestone | |---|---| | 1977–1979 | Alexander publishes *A Pattern Language* and *The Timeless Way of Building* | | 1987 | Beck and Cunningham present early software pattern work | | 1994 | The GoF book is published; the first Pattern Languages of Programs (PLoP) conference runs | | 1996–2003 | Pattern books spread into architecture, enterprise integration and concurrency | | 1999 | Martin Fowler's *Refactoring* links code smells to step-by-step fixes | | 2000s | Mainstream languages add generics, lambdas and closures | | 2010s onward | Functional styles and language features absorb several patterns into idioms | ## Patterns and refactoring meet Early on, patterns were often treated as up-front design tools: pick the patterns, then write the code. Refactoring changed that. Fowler's book, and later work by Joshua Kerievsky on refactoring *toward* patterns, made the case that you should arrive at a pattern by improving working code, one tested step at a time. That is the approach this site takes. Each [code smell](https://aipatterns.dev/refactoring/smells/bloaters.md) points to [techniques](https://aipatterns.dev/refactoring/techniques/composing-methods.md), and some techniques end in a pattern. For example, [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md) lands you at [State](https://aipatterns.dev/design-patterns/state.md) or [Strategy](https://aipatterns.dev/design-patterns/strategy.md). See [What is refactoring](https://aipatterns.dev/guides/what-is-refactoring.md). ## How the languages changed the patterns The GoF examples were written for C++ and Smalltalk in the early 1990s. Several patterns compensated for features those languages lacked or made awkward: - **First-class functions** make many uses of [Strategy](https://aipatterns.dev/design-patterns/strategy.md) and [Command](https://aipatterns.dev/design-patterns/command.md) a function parameter. - **Built-in iteration protocols** (`for...of`, Python iterators, Java `Iterable`) make hand-rolled [Iterator](https://aipatterns.dev/design-patterns/iterator.md) classes rare. - **Modules and dependency injection** replaced many uses of [Singleton](https://aipatterns.dev/design-patterns/singleton.md). - **Pattern matching and sealed types** cover cases that once needed [Visitor](https://aipatterns.dev/design-patterns/visitor.md). See [Visitor and double dispatch](https://aipatterns.dev/guides/visitor-and-double-dispatch.md). - **Event emitters and reactive streams** in standard libraries implement [Observer](https://aipatterns.dev/design-patterns/observer.md) for you. The problems did not go away. The solution moved into the language or the standard library. Knowing the pattern still helps you recognize what the built-in feature is doing and when it is not enough. ## What lasted Three ideas from this history still pay off: 1. **Names.** A shared vocabulary lets two developers, or a developer and an agent, agree on a design in one sentence. 2. **Forces.** Alexander's insistence on stating the tensions is what lets you decide when a pattern does *not* fit. 3. **Composition over inheritance.** The GoF book argued for it in 1994, and most modern code has taken the advice. For the arguments against patterns that grew alongside their popularity, see [Criticism of patterns](https://aipatterns.dev/guides/criticism-of-patterns.md). --- # Why Learn Patterns > What knowing design patterns buys a developer or coding agent: shared vocabulary, faster code reading, better refactoring targets, and knowing when to stop. - kind: guide - category: design-patterns - canonical: https://aipatterns.dev/guides/why-learn-patterns - inspired-by: https://refactoring.guru/design-patterns/why-learn-patterns ## The short answer You will meet these patterns whether you learn them or not. Frameworks, standard libraries and existing codebases are full of them. **Learning patterns is mostly about reading code faster and deciding with fewer mistakes, not about writing more abstractions.** ## Benefits ### A shared vocabulary "Wrap the payment client in an [Adapter](https://aipatterns.dev/design-patterns/adapter.md)" says in six words what would otherwise take a paragraph: a new class, a target interface the rest of the app already uses, and translation of calls and errors. In code review, a commit message or an agent's plan, names compress design discussion. ### Faster code reading When you recognize a role, you can predict the rest of the structure. Spotting a class named `*Handler` with a `next` field tells you to look for a [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) and find where the chain is assembled. Spotting `subscribe`/`unsubscribe` tells you to look for an [Observer](https://aipatterns.dev/design-patterns/observer.md) and check for listener leaks. Common places you will see patterns without being told: | You see | Likely pattern | |---|---| | Express or Koa middleware, servlet filters | [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) | | `addEventListener`, RxJS, Node `EventEmitter` | [Observer](https://aipatterns.dev/design-patterns/observer.md) | | Query builders, `StringBuilder`, test data builders | [Builder](https://aipatterns.dev/design-patterns/builder.md) | | Java I/O streams, HTTP client interceptors | [Decorator](https://aipatterns.dev/design-patterns/decorator.md) | | ORM lazy-loaded relations, JS `Proxy` | [Proxy](https://aipatterns.dev/design-patterns/proxy.md) | | Undo stacks in editors | [Command](https://aipatterns.dev/design-patterns/command.md) and [Memento](https://aipatterns.dev/design-patterns/memento.md) | | Compiler AST passes | [Visitor](https://aipatterns.dev/design-patterns/visitor.md) | ### Better refactoring targets Smells tell you something is wrong. Patterns tell you what "better" can look like. A growing type switch ([Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md)) has a known destination in [Strategy](https://aipatterns.dev/design-patterns/strategy.md) or [State](https://aipatterns.dev/design-patterns/state.md). Two incompatible classes doing the same job ([Alternative Classes with Different Interfaces](https://aipatterns.dev/refactoring/smells/alternative-classes-with-different-interfaces.md)) may need an [Adapter](https://aipatterns.dev/design-patterns/adapter.md). Having a destination keeps refactoring from drifting. ### Knowing when to stop Each pattern on this site has a "Do not use it when" section. Learning a pattern properly includes learning its misfits. Developers who know only the solution side apply it everywhere. Developers who know the forces apply it where it pays. ## Why it matters more for agents An AI coding agent generates code quickly, so the cost of over-engineering is paid by the humans who maintain the result. Patterns help an agent in specific ways: - **Recognize existing structure before editing.** If a codebase already uses [Strategy](https://aipatterns.dev/design-patterns/strategy.md) for pricing rules, add a new strategy; don't add a new `if` inside the context. - **Communicate intent.** A plan that says "introduce a [Facade](https://aipatterns.dev/design-patterns/facade.md) over the three reporting services" is quick for a human to approve or reject. - **Avoid reinventing.** Many requests ("let users undo", "notify subscribers", "support a new storage backend") map onto known structures with known pitfalls. - **Resist pattern inflation.** The checklists on each page exist to stop an agent from turning a 20-line function into five classes. See [Using this reference as an agent](https://aipatterns.dev/guides/for-agents.md) for the full workflow. ## What not to expect - Patterns do not make a design correct. A wrong abstraction behind a well-named pattern is still wrong. - Patterns are not a checklist to maximize. Code with zero named patterns can be excellent. - Patterns are not language-neutral in cost. Some vanish in languages with closures, generics or pattern matching. See [History of patterns](https://aipatterns.dev/guides/history-of-patterns.md). ## How to learn them efficiently 1. Read the intent and the "Do not use it when" section of every pattern first. That gives you recognition and restraint. 2. Learn the frequently seen ones in depth: [Strategy](https://aipatterns.dev/design-patterns/strategy.md), [Observer](https://aipatterns.dev/design-patterns/observer.md), [Adapter](https://aipatterns.dev/design-patterns/adapter.md), [Decorator](https://aipatterns.dev/design-patterns/decorator.md), [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md), [Builder](https://aipatterns.dev/design-patterns/builder.md), [Facade](https://aipatterns.dev/design-patterns/facade.md). 3. Learn the confusable pairs side by side: Strategy vs State, Decorator vs Proxy, Adapter vs Facade, Factory Method vs Abstract Factory ([Factory comparison](https://aipatterns.dev/guides/factory-comparison.md)). 4. Practice by refactoring toward a pattern from a real smell, not by writing one from scratch. --- # Criticism of Patterns > The main arguments against design patterns, including missing language features, misuse, and over-engineering, and how to apply patterns without those failures. - kind: guide - category: design-patterns - aliases: Anti-patterns, Pattern overuse - canonical: https://aipatterns.dev/guides/criticism-of-patterns - inspired-by: https://refactoring.guru/design-patterns/criticism ## Why the criticism matters Patterns are a tool with a well-known failure mode: applying them where they don't fit. **Most criticism of patterns is criticism of misuse, and each argument maps to a rule you can follow.** This page lists the arguments and the rule each one implies. ## "Patterns are workarounds for weak languages" Several classic patterns exist to supply features that early-1990s C++ lacked. In a language with first-class functions, a one-method [Strategy](https://aipatterns.dev/design-patterns/strategy.md) or [Command](https://aipatterns.dev/design-patterns/command.md) is a function value. With pattern matching over sealed types, much of [Visitor](https://aipatterns.dev/design-patterns/visitor.md) is a `match` or `switch` expression. With built-in iterators, you rarely write an [Iterator](https://aipatterns.dev/design-patterns/iterator.md) class. ```ts // Ceremony: a Command interface and a class per action. interface Command { execute(): void } class SaveCommand implements Command { constructor(private doc: Doc) {} execute() { this.doc.save(); } } // Same intent in TypeScript when no undo or metadata is needed. type Action = () => void; const save: Action = () => doc.save(); ``` **Rule:** before writing an interface plus classes, ask whether a function, a closure, a union type or a standard library feature already plays the role. ## "Patterns are applied without understanding" A developer learns [Singleton](https://aipatterns.dev/design-patterns/singleton.md) and uses it for every service. Another wraps every class in a [Factory](https://aipatterns.dev/design-patterns/factory-method.md) because "factories are good practice". The result is indirection without a reason. **Rule:** name the force the pattern resolves. If you can't state it in one sentence tied to this codebase ("we need to add payment providers without editing checkout"), don't apply the pattern. ## "Patterns cause over-engineering" Patterns introduce types, interfaces and indirection. Applied for a future that never comes, they are the [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md) smell. A reader now jumps through four files to find code that once fit on one screen. **Rule:** apply a pattern to solve a problem you have now, or one that is concretely planned. Refactor toward it later when the second or third variant actually arrives. Refactoring is cheap when tests exist. See [When to refactor](https://aipatterns.dev/guides/when-to-refactor.md). ## "Patterns hide the domain" Code full of `AbstractProcessorFactoryProvider` names tells the reader about the plumbing and nothing about the business. Pattern names belong in documentation and commit messages more than in class names. **Rule:** name classes after domain concepts (`ShippingRule`, `InvoiceExporter`). Mention the pattern in a short comment or doc only when it helps the reader. ## "Inheritance-based patterns are brittle" [Template Method](https://aipatterns.dev/design-patterns/template-method.md) and some uses of [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) rely on subclassing. Deep hierarchies create the [Parallel Inheritance Hierarchies](https://aipatterns.dev/refactoring/smells/parallel-inheritance-hierarchies.md) and [Refused Bequest](https://aipatterns.dev/refactoring/smells/refused-bequest.md) smells. **Rule:** prefer the composition-based version of a pattern. Pass a function or object in rather than subclass. See [Replace Inheritance with Delegation](https://aipatterns.dev/refactoring/techniques/replace-inheritance-with-delegation.md). ## "Patterns are hard to test or debug" Indirection makes stack traces longer and behavior harder to trace. Global state in a [Singleton](https://aipatterns.dev/design-patterns/singleton.md) leaks between tests. Long [Observer](https://aipatterns.dev/design-patterns/observer.md) chains make "who changed this?" hard to answer. **Rule:** keep wiring explicit at one composition root, avoid global access points, and prefer constructor injection so tests can substitute parts. ## Summary table | Criticism | Symptom in code | What to do instead | |---|---|---| | Missing language features | One-method interfaces, hand-written iterators | Use functions, generics, built-in protocols | | Applied without understanding | Patterns in every class, no stated reason | State the force first | | Over-engineering | Abstractions with one implementation | Wait for a real second variant | | Hides the domain | Class names made of pattern words | Name after domain concepts | | Brittle inheritance | Deep hierarchies, refused methods | Compose; inject behavior | | Hard to debug | Global access, implicit wiring | Explicit composition root | ## What survives the criticism None of these arguments says the *problems* that patterns address are imaginary. Systems still need to swap algorithms, adapt third-party APIs, notify listeners and build complex objects. The useful position is narrow: know the pattern, know its cost, and apply the smallest structure that fits. [Choosing a pattern](https://aipatterns.dev/guides/choosing-a-pattern.md) has a "prefer no pattern when" section that turns this into a decision procedure. --- # Classification of Patterns > How the 22 classic design patterns are grouped by purpose into creational, structural and behavioral, plus other useful ways to slice them. - kind: guide - category: design-patterns - aliases: Pattern categories - canonical: https://aipatterns.dev/guides/classification-of-patterns - inspired-by: https://refactoring.guru/design-patterns/classification ## Three groups by purpose The classic catalog sorts patterns by the kind of problem they solve. **Ask what you are trying to control: object creation, object composition, or the flow of responsibility between objects.** ```mermaid flowchart TD Q[What is the design problem about?] --> C[How objects get created] Q --> S[How objects fit together] Q --> B[How objects divide work and communicate] C --> CR[Creational] S --> ST[Structural] B --> BE[Behavioral] ``` ### Creational Control how and when objects are created, so callers don't depend on concrete classes or construction details. See [Creational patterns](https://aipatterns.dev/design-patterns/creational.md). - [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md): let a subclass or function decide which concrete product to create. - [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md): create families of related products that must match. - [Builder](https://aipatterns.dev/design-patterns/builder.md): assemble a complex object step by step. - [Prototype](https://aipatterns.dev/design-patterns/prototype.md): create new objects by copying a configured instance. - [Singleton](https://aipatterns.dev/design-patterns/singleton.md): guarantee one instance with a global access point. ### Structural Compose classes and objects into larger structures while keeping them flexible. See [Structural patterns](https://aipatterns.dev/design-patterns/structural.md). - [Adapter](https://aipatterns.dev/design-patterns/adapter.md): make one interface usable where another is expected. - [Bridge](https://aipatterns.dev/design-patterns/bridge.md): split an abstraction from its implementation so both vary. - [Composite](https://aipatterns.dev/design-patterns/composite.md): treat trees of objects like single objects. - [Decorator](https://aipatterns.dev/design-patterns/decorator.md): add behavior by wrapping an object with the same interface. - [Facade](https://aipatterns.dev/design-patterns/facade.md): give a simple front to a complex subsystem. - [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md): share common state across many small objects. - [Proxy](https://aipatterns.dev/design-patterns/proxy.md): stand in for another object to control access to it. ### Behavioral Assign responsibilities and define how objects communicate. See [Behavioral patterns](https://aipatterns.dev/design-patterns/behavioral.md). - [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md): pass a request along handlers until one deals with it. - [Command](https://aipatterns.dev/design-patterns/command.md): turn a request into an object you can queue, log or undo. - [Iterator](https://aipatterns.dev/design-patterns/iterator.md): traverse a collection without exposing its structure. - [Mediator](https://aipatterns.dev/design-patterns/mediator.md): route communication through one coordinator. - [Memento](https://aipatterns.dev/design-patterns/memento.md): capture and restore state without breaking encapsulation. - [Observer](https://aipatterns.dev/design-patterns/observer.md): notify subscribers when something changes. - [State](https://aipatterns.dev/design-patterns/state.md): change behavior when internal state changes. - [Strategy](https://aipatterns.dev/design-patterns/strategy.md): swap interchangeable algorithms. - [Template Method](https://aipatterns.dev/design-patterns/template-method.md): fix an algorithm's skeleton and let steps vary. - [Visitor](https://aipatterns.dev/design-patterns/visitor.md): add operations over a structure without editing its classes. ## A second axis: class vs object scope The original catalog also asked whether a pattern works mainly through inheritance (class scope, fixed at compile time) or through composition (object scope, changeable at runtime). | Scope | Mechanism | Examples | |---|---|---| | Class | Subclassing | [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md), [Template Method](https://aipatterns.dev/design-patterns/template-method.md), class-form [Adapter](https://aipatterns.dev/design-patterns/adapter.md) | | Object | Composition and delegation | [Strategy](https://aipatterns.dev/design-patterns/strategy.md), [Decorator](https://aipatterns.dev/design-patterns/decorator.md), [Bridge](https://aipatterns.dev/design-patterns/bridge.md), [Observer](https://aipatterns.dev/design-patterns/observer.md), most others | Prefer object-scope patterns by default. They change at runtime, test more easily, and avoid deep hierarchies. ## A third axis: how often you need them Grouping by frequency is more useful day to day than grouping by purpose: - **Everywhere**: [Strategy](https://aipatterns.dev/design-patterns/strategy.md), [Observer](https://aipatterns.dev/design-patterns/observer.md), [Adapter](https://aipatterns.dev/design-patterns/adapter.md), [Facade](https://aipatterns.dev/design-patterns/facade.md), [Decorator](https://aipatterns.dev/design-patterns/decorator.md), [Builder](https://aipatterns.dev/design-patterns/builder.md), [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md), [Iterator](https://aipatterns.dev/design-patterns/iterator.md) (usually built in). - **Regularly, for specific needs**: [Command](https://aipatterns.dev/design-patterns/command.md), [State](https://aipatterns.dev/design-patterns/state.md), [Composite](https://aipatterns.dev/design-patterns/composite.md), [Proxy](https://aipatterns.dev/design-patterns/proxy.md), [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md), [Template Method](https://aipatterns.dev/design-patterns/template-method.md), [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md). - **Rarely, and with care**: [Singleton](https://aipatterns.dev/design-patterns/singleton.md), [Mediator](https://aipatterns.dev/design-patterns/mediator.md), [Memento](https://aipatterns.dev/design-patterns/memento.md), [Prototype](https://aipatterns.dev/design-patterns/prototype.md), [Bridge](https://aipatterns.dev/design-patterns/bridge.md), [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md), [Visitor](https://aipatterns.dev/design-patterns/visitor.md). Each pattern page carries `popularity` and `complexity` scores (1 to 3) in its frontmatter for the same purpose. ## Confusable neighbors across groups Groups are not walls. Some patterns share a structure but differ in intent: - [Strategy](https://aipatterns.dev/design-patterns/strategy.md) and [State](https://aipatterns.dev/design-patterns/state.md): same delegation shape, different reason for switching. - [Decorator](https://aipatterns.dev/design-patterns/decorator.md) and [Proxy](https://aipatterns.dev/design-patterns/proxy.md): both wrap an object with the same interface; one adds behavior, the other controls access. - [Adapter](https://aipatterns.dev/design-patterns/adapter.md) and [Facade](https://aipatterns.dev/design-patterns/facade.md): both wrap; one converts an interface, the other simplifies a subsystem. - [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) and [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md): see [Factory comparison](https://aipatterns.dev/guides/factory-comparison.md). - [Composite](https://aipatterns.dev/design-patterns/composite.md) and [Visitor](https://aipatterns.dev/design-patterns/visitor.md): often used together; see [Visitor and double dispatch](https://aipatterns.dev/guides/visitor-and-double-dispatch.md). To pick among them from a symptom rather than a category, use [Choosing a pattern](https://aipatterns.dev/guides/choosing-a-pattern.md). --- # Factory Comparison > Tell apart a factory function, static creation method, simple factory, Factory Method and Abstract Factory, and pick the smallest one that fits. - kind: guide - category: design-patterns - aliases: Simple Factory, Static factory method, Creation method - canonical: https://aipatterns.dev/guides/factory-comparison - inspired-by: https://refactoring.guru/design-patterns/factory-comparison ## Why the word is confusing "Factory" is used for at least five different things. Only two of them are GoF patterns. **Pick by asking who decides which concrete class to create, and whether several products must match.** ## The five meanings ### 1. Factory function or creation method Any function or method that creates and returns an object. It replaces a direct `new` so construction can have a descriptive name or hide setup. ```ts class Money { private constructor(readonly cents: number, readonly currency: string) {} static fromDecimal(amount: number, currency: string): Money { return new Money(Math.round(amount * 100), currency); } static zero(currency: string): Money { return new Money(0, currency); } } ``` This is not a design pattern in the catalog; it is good practice. The refactoring that introduces it is [Replace Constructor with Factory Method](https://aipatterns.dev/refactoring/techniques/replace-constructor-with-factory-method.md). ### 2. Static factory that picks a subtype A creation method that returns one of several implementations based on an argument. ```ts interface Notifier { send(to: string, body: string): Promise } function createNotifier(channel: "email" | "sms" | "push"): Notifier { switch (channel) { case "email": return new EmailNotifier(); case "sms": return new SmsNotifier(); case "push": return new PushNotifier(); } } ``` Often called a "simple factory". The `switch` lives in one place, so callers stay free of it. This covers most real needs. Adding a channel means editing this function, which is fine when it is the only place that knows about channels. ### 3. Simple factory class The same idea as a class with one creation method, often so it can be injected or configured. Use it when the factory needs dependencies (API keys, a clock, a logger) that callers shouldn't see. ### 4. Factory Method (GoF) A base class or framework defines a workflow and calls an overridable creation method. Subclasses, or injected functions, decide the concrete product. The point is that **the code calling the factory is written before the product types are known**. ```ts abstract class ReportJob { run(rows: Row[]): string { const writer = this.createWriter(); // the factory method rows.forEach((r) => writer.write(r)); return writer.finish(); } protected abstract createWriter(): RowWriter; } class CsvReportJob extends ReportJob { protected createWriter(): RowWriter { return new CsvWriter(); } } ``` See [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md). In TypeScript you can often pass `() => RowWriter` into the constructor instead of subclassing. ### 5. Abstract Factory (GoF) An object that creates a *family* of related products that must be used together. Swapping the factory swaps the whole family consistently. ```ts interface StorageKit { blobStore(): BlobStore; queue(): Queue; lock(): DistributedLock; } class AwsKit implements StorageKit { /* S3, SQS, DynamoDB lock */ } class LocalKit implements StorageKit { /* filesystem, in-memory queue, mutex */ } function startWorker(kit: StorageKit) { const blobs = kit.blobStore(); const jobs = kit.queue(); // blobs and jobs are guaranteed to come from the same environment } ``` See [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md). ## Comparison table | Variant | Who decides the concrete type | Products | Extend by | |---|---|---|---| | Creation method | The method itself (always one type) | One | Adding a named method | | Simple factory | A `switch` or map inside the factory | One, of several types | Editing the factory | | Factory Method | A subclass or injected function | One, used by a workflow | Adding a subclass or function | | Abstract Factory | The chosen factory object | Several related ones | Adding a whole new factory | ```mermaid flowchart TD A[Need to create an object] --> B{More than one concrete type?} B -- No --> C[Creation method or plain constructor] B -- Yes --> D{Several products that must match?} D -- Yes --> E[Abstract Factory] D -- No --> F{Does a workflow call creation before types are known?} F -- Yes --> G[Factory Method] F -- No --> H[Simple factory function] ``` ## How to choose - Start with a constructor. Move to a creation method when construction needs a name, validation or caching. - Use a simple factory function when callers must not know concrete types, and selection is a lookup. - Use [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) when a reusable workflow, framework hook or base class needs to create something that extensions customize. - Use [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md) only when mixing products from different families is a real bug risk. - If the problem is many optional parameters, not which type to create, you want [Builder](https://aipatterns.dev/design-patterns/builder.md). - If new objects should copy a configured template, you want [Prototype](https://aipatterns.dev/design-patterns/prototype.md). ## Common mistakes - Creating an `XFactory` class for every class with a plain constructor. That is [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md). - Building an Abstract Factory with one product method. That is a Factory Method or a simple factory with extra types. - Subclassing only to override the factory method, in a language where passing a function works. Prefer composition. --- # Visitor and Double Dispatch > Why overloading alone cannot pick a method by an argument's runtime type, how Visitor uses double dispatch to fix it, and when pattern matching replaces it. - kind: guide - category: design-patterns - aliases: Double dispatch, Single dispatch - canonical: https://aipatterns.dev/guides/visitor-and-double-dispatch - inspired-by: https://refactoring.guru/design-patterns/visitor-double-dispatch ## The problem You have a tree of node types (say, invoice line items) and you need many operations over them: compute tax, render HTML, export to an accounting system. You don't want each node class to grow a method for every operation. You want each operation in one place. The natural first attempt is a class per operation with one overload per node type. In most mainstream languages, that attempt fails quietly. ## Single dispatch Languages like Java, C# and C++ pick a method in two stages: - **Which object's method** is chosen at runtime, from the receiver's actual type. This is ordinary polymorphism, called *single dispatch*. - **Which overload** is chosen at compile time, from the argument's *declared* type. ```java interface Item {} class Product implements Item {} class Shipping implements Item {} class TaxCalculator { long tax(Product p) { return 20; } long tax(Shipping s) { return 5; } long tax(Item i) { return 0; } } Item item = new Product(); new TaxCalculator().tax(item); // returns 0, not 20 ``` The compiler sees `Item`, so it binds to `tax(Item)`. The runtime type `Product` is never consulted for overload selection. TypeScript and Python don't have this trap because they have no runtime overloading at all; there you would write a type check by hand. ## Double dispatch The fix is to make two runtime dispatches in a row. First, call a method on the *element*, so its real type is known inside it. Then the element calls back to the operation, passing `this`, whose static type is now the concrete class. ```ts interface ItemVisitor { visitProduct(p: Product): R; visitShipping(s: Shipping): R; } interface Item { accept(v: ItemVisitor): R; } class Product implements Item { constructor(readonly price: number, readonly taxRate: number) {} accept(v: ItemVisitor): R { return v.visitProduct(this); } } class Shipping implements Item { constructor(readonly fee: number) {} accept(v: ItemVisitor): R { return v.visitShipping(this); } } class TaxVisitor implements ItemVisitor { visitProduct(p: Product) { return p.price * p.taxRate; } visitShipping(s: Shipping) { return s.fee * 0.05; } } const items: Item[] = [new Product(100, 0.2), new Shipping(10)]; const tax = items.reduce((sum, i) => sum + i.accept(new TaxVisitor()), 0); // 20.5 ``` Dispatch one picks `Product.accept`. Dispatch two picks `visitProduct` on whichever visitor was passed. The operation now depends on two runtime types: the element's and the visitor's. ```mermaid sequenceDiagram participant Client participant Product participant TaxVisitor Client->>Product: accept(taxVisitor) Product->>TaxVisitor: visitProduct(this) TaxVisitor-->>Product: tax amount Product-->>Client: tax amount ``` This is the core of the [Visitor](https://aipatterns.dev/design-patterns/visitor.md) pattern. ## Trade-off: which axis is cheap to extend | Change | Visitor | Methods on each node | |---|---|---| | Add an operation | One new visitor class | Edit every node class | | Add a node type | Edit every visitor | One new class | **Use Visitor when the set of node types is stable and operations keep growing.** Compilers, AST tools and document exporters fit. If node types change often, put the method on each node instead, using [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md). ## When pattern matching replaces Visitor Languages with sealed types or discriminated unions plus exhaustive matching give you the same guarantees with less code. TypeScript can do this with a discriminated union and a `never` check: ```ts type Item = | { kind: "product"; price: number; taxRate: number } | { kind: "shipping"; fee: number }; function tax(item: Item): number { switch (item.kind) { case "product": return item.price * item.taxRate; case "shipping": return item.fee * 0.05; default: { const unreachable: never = item; // compile error if a kind is missing return unreachable; } } } ``` Java 21 sealed interfaces with `switch` patterns, Kotlin `when` over sealed classes, Rust `match` and Python `match` with type checkers all offer equivalents. The compiler flags every unhandled case when a new kind is added, which is the main safety Visitor provides. Prefer pattern matching when: - The data types are plain records with no behavior of their own. - The language checks exhaustiveness. - You don't need visitors as swappable objects at runtime. Keep a classic Visitor when the element types are classes in a hierarchy you can't turn into a union, or when visitors carry state and are passed around as values. ## Related - [Composite](https://aipatterns.dev/design-patterns/composite.md): the tree that a Visitor usually walks. - [Iterator](https://aipatterns.dev/design-patterns/iterator.md): separates traversal order from the visitor's per-node work. - [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md): a type switch that is repeated in many places is the smell Visitor or polymorphism addresses. One exhaustive switch in one place is not a smell. - [Classification of patterns](https://aipatterns.dev/guides/classification-of-patterns.md): where Visitor sits among behavioral patterns. --- # Choosing a Pattern > A decision guide that maps concrete code symptoms to the pattern that fixes them, and tells you when plain code beats any pattern. - kind: guide - category: design-patterns - aliases: Pattern selection, Pattern catalog - canonical: https://aipatterns.dev/guides/choosing-a-pattern - inspired-by: https://refactoring.guru/design-patterns/catalog ## Start from the symptom, not the pattern **Pick a pattern only after you can name the concrete problem in the current code that it removes.** "This would be more flexible" is not a problem. "Adding a fourth export format means editing the same `switch` in five files" is. Work in this order: 1. Name the pain in one sentence, with a file and function. 2. Check the "Prefer no pattern when" list below. Most pains have a smaller fix. 3. Find the symptom in the table. Read the candidate pattern's `## Do not use it when` section before anything else. 4. If two patterns fit, compare their `## Relations` sections. They state the difference directly. ## Symptom to pattern | Symptom in the code | Candidate | Check first | |---|---|---| | `new ConcreteThing()` scattered in code that should not know which concrete type it gets | [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) | A plain factory function is often enough | | Several related objects must be created as a matched set (all AWS or all GCP clients) | [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md) | Only worth it with two or more real families | | Constructor with many optional parameters, or objects built in several steps | [Builder](https://aipatterns.dev/design-patterns/builder.md) | Named or object parameters may do it | | Creating an object is expensive and you need many near-identical copies | [Prototype](https://aipatterns.dev/design-patterns/prototype.md) | `structuredClone` or a copy constructor | | Exactly one instance must exist and be reachable process-wide | [Singleton](https://aipatterns.dev/design-patterns/singleton.md) | Usually pass the instance in instead | | A third-party or legacy class has the wrong interface for your code | [Adapter](https://aipatterns.dev/design-patterns/adapter.md) | A wrapper function for one call | | Two independent dimensions of variation multiply into many subclasses | [Bridge](https://aipatterns.dev/design-patterns/bridge.md) | Only if both dimensions really vary | | Tree of items where leaves and groups must be treated the same | [Composite](https://aipatterns.dev/design-patterns/composite.md) | A recursive function over plain data | | You need to add behavior (logging, caching, retries) around an object without changing it | [Decorator](https://aipatterns.dev/design-patterns/decorator.md) | Higher-order function or middleware | | Callers wire together many subsystem objects to do one common task | [Facade](https://aipatterns.dev/design-patterns/facade.md) | A single module-level function | | Memory blows up from millions of objects that share most of their data | [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md) | Measure first | | Access to an object needs lazy loading, access control or remote calls | [Proxy](https://aipatterns.dev/design-patterns/proxy.md) | Language-level `Proxy` or a lazy getter | | A request passes through a sequence of handlers, any of which may handle or reject it | [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) | An array of functions in a loop | | Actions must be queued, logged, retried or undone | [Command](https://aipatterns.dev/design-patterns/command.md) | A closure plus a record of inputs | | Callers depend on a collection's internal layout to traverse it | [Iterator](https://aipatterns.dev/design-patterns/iterator.md) | Built-in iterators and generators | | Many components call each other directly, forming a tangle | [Mediator](https://aipatterns.dev/design-patterns/mediator.md) | An event bus you already have | | You need snapshots for undo without exposing internal fields | [Memento](https://aipatterns.dev/design-patterns/memento.md) | Immutable state makes this free | | Other objects must react when something changes | [Observer](https://aipatterns.dev/design-patterns/observer.md) | Built-in events, signals or streams | | Behavior depends on an object's mode, with `switch (this.status)` in many methods | [State](https://aipatterns.dev/design-patterns/state.md) | A transition table if behavior is small | | A `switch` picks between algorithms that compute the same result in different ways | [Strategy](https://aipatterns.dev/design-patterns/strategy.md) | Pass a function | | Subclasses copy the same workflow and differ in a few steps | [Template Method](https://aipatterns.dev/design-patterns/template-method.md) | Higher-order function with hooks | | You keep adding operations over a stable set of node types (AST, document tree) | [Visitor](https://aipatterns.dev/design-patterns/visitor.md) | Pattern matching over a union type | ## Prefer no pattern when - **There are only one or two variants.** An `if` is honest. Wait for the third case. See [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md). - **The language already has the feature.** First-class functions replace most single-method Strategy, Command and Template Method uses. Generators replace Iterator. Discriminated unions with exhaustive `switch` replace many Visitor uses. Module scope replaces Singleton. - **A refactoring technique removes the smell.** [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md), [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md) and [Decompose Conditional](https://aipatterns.dev/refactoring/techniques/decompose-conditional.md) fix more code than any pattern. - **The code is not going to change.** Patterns pay off on the next change. Stable code gets no return. - **The team or codebase does not use the pattern elsewhere.** One lone Abstract Factory in a codebase of plain functions is a reading tax for every future contributor. - **You cannot write a test that the pattern makes easier.** If the test is the same before and after, the pattern bought nothing measurable. - **The framework already owns that concern.** Dependency injection containers, ORMs and UI frameworks implement Factory, Proxy and Observer for you. Use theirs. ## Selection flowchart The first chart decides whether you need a pattern at all and which family to look in. Families are explained in [Classification of patterns](https://aipatterns.dev/guides/classification-of-patterns.md). ```mermaid flowchart TD A{"Can you name a concrete pain in current code?"} A -->|No| N["Use no pattern"] A -->|Yes| B{"Would a function, map or refactoring fix it?"} B -->|Yes| N B -->|No| C{"Is the pain about creating objects?"} C -->|Yes| CR["Creational patterns"] C -->|No| D{"Is it about fitting or wrapping objects?"} D -->|Yes| ST["Structural patterns"] D -->|No| BH["Behavioral patterns"] ``` Behavioral is the largest family and the most common destination. The second chart covers its frequent cases. ```mermaid flowchart TD A{"Does a switch pick an algorithm per call?"} A -->|Yes| S["Strategy, or pass a function"] A -->|No| B{"Does behavior depend on a mode that changes over time?"} B -->|Yes| ST["State"] B -->|No| C{"Must others react when something changes?"} C -->|Yes| O["Observer"] C -->|No| T["Use the symptom table"] ``` ## Telling look-alikes apart Several patterns share a class diagram. Decide by intent: - [Strategy](https://aipatterns.dev/design-patterns/strategy.md) vs [State](https://aipatterns.dev/design-patterns/state.md): strategies are chosen from outside and ignore each other; states switch themselves. - [Decorator](https://aipatterns.dev/design-patterns/decorator.md) vs [Proxy](https://aipatterns.dev/design-patterns/proxy.md): a decorator adds behavior and stacks; a proxy controls access and usually manages the real object's lifecycle. - [Adapter](https://aipatterns.dev/design-patterns/adapter.md) vs [Facade](https://aipatterns.dev/design-patterns/facade.md): an adapter makes one existing interface fit another; a facade invents a simpler interface over many objects. - [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) vs [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md): one product vs a family of products. See [Factory comparison](https://aipatterns.dev/guides/factory-comparison.md). - [Command](https://aipatterns.dev/design-patterns/command.md) vs [Strategy](https://aipatterns.dev/design-patterns/strategy.md): a command represents a request to run later; a strategy represents a way to compute now. ## Agent checklist - [ ] You can state the pain in one sentence that names real code. - [ ] You checked the "Prefer no pattern when" list and none of it applies. - [ ] You read the chosen pattern's `## Do not use it when` section and none of it applies. - [ ] At least three variants exist, or a third is concretely planned. - [ ] A test exists, or will exist, that is easier to write after the change. - [ ] Existing tests still pass after you introduce the pattern. --- # Eval-Driven Development > Write the task-and-verifier pairs before the tool, prompt or agent, then let the numbers decide changes - the same discipline as tests-first, applied to components whose output is text. - kind: guide - category: design-patterns - aliases: Eval-First, Agent Evals - canonical: https://aipatterns.dev/guides/eval-driven-development - inspired-by: https://www.anthropic.com/engineering/writing-tools-for-agents ## The problem it solves Code has tests: run it, compare against expected, red or green. A tool description, a system prompt, or an agent loop has no `assert` - its output is prose and its failures are quiet: slightly worse routes, slightly more tokens, slightly more wrong turns. Teams then change prompts and tools by vibes, and quality drifts without anyone deciding to. Eval-driven development gives text-shaped components the thing tests give code: a fixed bar that a change must clear before it ships. **The rule: if you cannot write a task and a verifier for a change, you are not ready to make it - and if you can, make the evals before the change.** ## The loop 1. **Name the tasks.** Write down what users or agents actually do with the component - "find the customer's recent orders", "route this ticket to billing" - as concrete inputs, not capabilities. Ten real tasks beat a hundred synthetic ones. 2. **Write a verifier per task.** The verifier answers "did this run do the job?" with as little judgment as possible: an exact answer, a schema check, a value bound (tokens, calls, latency), or a checklist another model applies against written criteria. If the verifier needs taste, the task is not defined well enough yet. 3. **Run the suite on the current version.** This is your baseline. Record pass rate plus the operational numbers: runtime, tool-call counts, token usage, errors by type. 4. **Make one change** - a tool description rewrite, a parameter rename, a consolidation. Change one thing; the suite tells you its price and its gain. 5. **Compare and keep or revert.** Improvement on the target tasks with no regression elsewhere keeps. Anything else reverts. The suite, not the reviewer's impression, decides. ## What to measure - **Task success** - the pass rate over the verifier set, split by task type so you can see which tasks a change hurt. - **Tool-call counts** per task - consolidation and description fixes show up here first. - **Token usage** - the real cost of a prompt or schema change, per run. - **Latency/runtime** - what the user experiences, not what the model averages. - **Errors by type** - invalid arguments and wrong-tool picks point at descriptions; execution failures point at the tool. A change that wins on quality but triples tool calls may still be right - but it should win because you saw both numbers, not because you saw neither. ## Task and verifier pairs Anthropic's tool-writing guidance is blunt about the pairing: each evaluation prompt should come with a verifiable response or outcome, and the fastest way to improve an agent is to generate realistic task/verifier pairs and fix what they expose. Two properties make a pair good: - **Realistic input**: drawn from real usage - a messy ticket, a half-specified request, a slug with a typo - not a clean example you invented with the answer in mind. - **Verifiable outcome**: the expected entry slug, the exact structured answer, a bound ("under 2 tool calls, under 500 output tokens"), or a rubric another model applies against written criteria. "Looks good" verifies nothing. Start each new tool, skill, or agent with three pairs - one typical, one edge, one refusal case. Grow the set only when a real failure shows you a gap; a large stale suite is worse than a small live one. ## How this platform runs its evals The benchmark pages on this site run exactly this loop at the platform level: fixed tasks, agents with and without the MCP server, the same measurements, results published. Every tool-description change on the server should be able to name the eval numbers it moved - and when a change is proposed without numbers, the benchmark is where they come from. ## When not to bother - One-off scripts and prototypes nobody will rerun. Evals pay for themselves across revisions; a thing with no next revision has no payer. - Components with a trivially checkable output - a schema-validating parser already has its verifier; write the test, not an eval program. - Exploratory work whose goal is to find out what the tasks even are. Explore first, then write the pairs - but write them before you polish. --- # How Agents Should Use This Reference > Rules for AI coding agents using this reference: diagnose a named smell, read the exclusions first, apply one technique at a time and verify with tests. - kind: guide - category: refactoring - aliases: Agent guide, Using AI Patterns - canonical: https://aipatterns.dev/guides/for-agents - inspired-by: https://refactoring.guru/ ## Purpose This reference describes design patterns, refactoring techniques and code smells in a fixed, predictable shape so that an AI coding agent can look one up mid-task and act on it. It is a set of tools with warning labels, not a list of things to add. **Apply an entry only when the code in front of you shows the problem that entry names, and only after reading why you should not.** ## Read in this order 1. **The exclusions.** On a pattern page, read `## Do not use it when` first. On a technique page, read `## Watch out`. On a smell page, read `## When to ignore`. If any bullet matches your situation, stop and use the simpler alternative it names. 2. **The trigger.** `## Use it when` (pattern), `## Problem` (technique) or `## Signs` (smell). Match it against actual code, not a hypothetical future. 3. **The procedure.** `## Implementation steps` or `## Steps`. Follow them in order. 4. **The checklist.** `## Agent checklist`. Verify each item before and after. ## How each kind of page is structured Headings are fixed so you can jump straight to a section. | Kind | Sections, in order | |---|---| | Pattern | Intent, Forces, Therefore, Structure, Use it when, **Do not use it when**, Implementation steps, Example, Trade-offs, Relations, Agent checklist | | Technique | Problem, Solution, Before, After, Why, Steps, **Watch out**, Related, Agent checklist | | Smell | Signs, Why it happens, Example, Treatment, Payoff, **When to ignore**, Agent checklist | | Category, guide | Free structure; category pages list every member with a one-line summary | Frontmatter carries machine-readable links: `related` on every entry, `fixes` on techniques (the smells a technique treats) and `treatments` on smells (the techniques that fix it). ## Apply only when - The user asked for the change, or the change is required to complete what the user asked. Do not refactor unrelated code because it looks wrong. Report it instead. - You can name the smell or problem, with a file and symbol. "Could be cleaner" does not qualify. - The code is in the path of the current task. See [When to refactor](https://aipatterns.dev/guides/when-to-refactor.md). - Tests cover the behavior you will touch, or you add characterization tests first. - For patterns, there are at least three real variants or a concrete third on the way. Two variants rarely justify an interface. See [Choosing a pattern](https://aipatterns.dev/guides/choosing-a-pattern.md). - The language does not already provide the mechanism. Functions, generators, union types and built-in events replace many classic patterns. ## Verify with tests Every structural change follows the same loop. [How to refactor](https://aipatterns.dev/guides/how-to-refactor.md) covers it in depth. 1. Run the full relevant test suite and the type checker. Record the exact command. If anything is red before you start, stop and report it. 2. Make one named change: one technique, or one step of a pattern's implementation steps. 3. Run the same command. Compare results with the baseline. 4. Green: continue or commit. Red: revert to the last green state and take a smaller step. Do not debug a refactoring forward. 5. Keep behavior changes out of refactoring steps. If you must fix a bug, do it in its own step with its own failing test. 6. At the end, run the whole suite once more and state in your report which commands you ran and what passed. If no tests exist and you cannot add them, restrict yourself to tool-assisted transformations (language-server rename, extract and move) and say so in your report. ## Navigating - From a symptom: open the smell category, such as [Bloaters](https://aipatterns.dev/refactoring/smells/bloaters.md), [OO abusers](https://aipatterns.dev/refactoring/smells/oo-abusers.md), [Change preventers](https://aipatterns.dev/refactoring/smells/change-preventers.md), [Dispensables](https://aipatterns.dev/refactoring/smells/dispensables.md) or [Couplers](https://aipatterns.dev/refactoring/smells/couplers.md). Each smell lists its treatments. - From a desired transformation: open a technique category, such as [Composing methods](https://aipatterns.dev/refactoring/techniques/composing-methods.md) or [Simplifying conditional expressions](https://aipatterns.dev/refactoring/techniques/simplifying-conditional-expressions.md). - From a design problem: open [Choosing a pattern](https://aipatterns.dev/guides/choosing-a-pattern.md), or a pattern family: [Creational](https://aipatterns.dev/design-patterns/creational.md), [Structural](https://aipatterns.dev/design-patterns/structural.md), [Behavioral](https://aipatterns.dev/design-patterns/behavioral.md). A common path: you see a growing `switch` on a type code. Open [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md), follow its treatment to [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md), and, if the variants are algorithms, read [Strategy](https://aipatterns.dev/design-patterns/strategy.md) starting at `## Do not use it when`. ## Reporting When you finish, tell the user: - Which smell you identified and where. - Which technique or pattern you applied, by name and link. - Which exclusions you checked and why they did not apply. - Which tests you ran, before and after. - What you noticed but deliberately did not change. ## Agent checklist - [ ] You read the exclusion section of every entry you applied before applying it. - [ ] Each change maps to a named smell in code you were already changing. - [ ] Tests and type checks were green before the first edit and after the last. - [ ] No refactoring step also changed behavior. - [ ] Your report names the entries used, the tests run and the issues left alone. --- # What Is Refactoring > Refactoring changes the internal structure of code in small, behavior-preserving steps so it becomes cheaper to read and change. - kind: guide - category: refactoring - aliases: Refactoring basics - canonical: https://aipatterns.dev/guides/what-is-refactoring - inspired-by: https://refactoring.guru/refactoring/what-is-refactoring ## Definition Refactoring is a change to the structure of code that leaves its observable behavior unchanged. Inputs that produced a result before still produce the same result after. The only thing that moves is how the code is arranged: names, boundaries, where data lives, how responsibilities split. **If a change alters what the program does, it is not a refactoring, no matter how small it is.** Fixing a bug, adding a field to an API response or changing an error message are behavior changes. They can happen next to a refactoring, but never inside the same step. The word covers two things: - **A refactoring** (noun): one named, mechanical transformation such as [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) or [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md). - **Refactoring** (activity): a sequence of those transformations applied toward a goal, such as "make the pricing rules testable". ## What counts as clean code Refactoring aims at code that is cheap to change. That usually means: - **Names say what things do.** A reader can guess a function's behavior from its signature. - **Each unit has one reason to change.** Pricing rules and email formatting do not share a class. - **No duplication of knowledge.** One rule lives in one place, so a fix lands once. - **Small surface area.** Functions take few arguments, classes expose few public members. - **Tests pass and are fast.** Clean code that cannot be verified is a guess. - **No surprises.** Queries do not modify state; a method named `get` does not write to the database. Clean code is not short code. A three-line function full of clever chaining can be harder to change than ten plain lines. ## What refactoring is not - **Not a rewrite.** A rewrite replaces code wholesale and has to rediscover every edge case. Refactoring keeps the program working at every step. - **Not optimization.** Performance work often makes code harder to read. It is a separate goal with separate verification (benchmarks as well as tests). - **Not adding features.** Kent Beck's "two hats" rule applies: at any moment you are either adding behavior or restructuring, never both. - **Not reformatting.** Running a formatter is harmless, but it does not change structure. Keep formatter churn in its own commit so it does not hide real changes in a diff. ## Why it matters for agents An AI agent reads code it did not write, in a context window that is always too small. Structure decides how much an agent must load to make a correct change: - A 400-line method forces the agent to hold the whole thing in context. Five named 80-line methods let it load one. - Duplicated logic means the agent patches one copy and misses the other three. See [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) and [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md). - Primitive values with implicit meaning (`status: 3`) lead to plausible but wrong code. See [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md). Refactoring before a feature change often makes the feature change smaller and safer. That is the main reason to do it. ## The vocabulary This reference organizes refactoring into three layers: | Layer | What it is | Where | |---|---|---| | Smells | Symptoms that suggest the structure is hurting you | [Bloaters](https://aipatterns.dev/refactoring/smells/bloaters.md), [OO abusers](https://aipatterns.dev/refactoring/smells/oo-abusers.md), [Change preventers](https://aipatterns.dev/refactoring/smells/change-preventers.md), [Dispensables](https://aipatterns.dev/refactoring/smells/dispensables.md), [Couplers](https://aipatterns.dev/refactoring/smells/couplers.md) | | Techniques | Named, safe transformations that remove a smell | [Composing methods](https://aipatterns.dev/refactoring/techniques/composing-methods.md), [Moving features](https://aipatterns.dev/refactoring/techniques/moving-features-between-objects.md), [Organizing data](https://aipatterns.dev/refactoring/techniques/organizing-data.md), [Simplifying conditionals](https://aipatterns.dev/refactoring/techniques/simplifying-conditional-expressions.md), [Simplifying method calls](https://aipatterns.dev/refactoring/techniques/simplifying-method-calls.md), [Generalization](https://aipatterns.dev/refactoring/techniques/dealing-with-generalization.md) | | Patterns | Target designs you may refactor toward when a structure keeps recurring | [Design patterns](https://aipatterns.dev/guides/what-is-a-pattern.md) | A smell page names the techniques that treat it. A technique page names the smells it fixes. Follow those links rather than guessing. ## A small example ```ts // Before: one function computes, formats and decides. function invoiceLine(item: { qty: number; unitCents: number; taxable: boolean }) { const base = item.qty * item.unitCents; const tax = item.taxable ? Math.round(base * 0.2) : 0; return `${item.qty} x ${(item.unitCents / 100).toFixed(2)} = ${((base + tax) / 100).toFixed(2)}`; } // After: same output, three named pieces. const VAT_RATE = 0.2; const lineTotal = (i: { qty: number; unitCents: number; taxable: boolean }) => i.qty * i.unitCents + (i.taxable ? Math.round(i.qty * i.unitCents * VAT_RATE) : 0); const money = (cents: number) => (cents / 100).toFixed(2); function invoiceLineRefactored(i: { qty: number; unitCents: number; taxable: boolean }) { return `${i.qty} x ${money(i.unitCents)} = ${money(lineTotal(i))}`; } ``` The steps were [Replace Magic Number with Symbolic Constant](https://aipatterns.dev/refactoring/techniques/replace-magic-number-with-symbolic-constant.md), then [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) twice. A test that compares both versions on a handful of inputs confirms nothing changed. ## Next - Read [Technical debt](https://aipatterns.dev/guides/technical-debt.md) for why code drifts toward a mess. - Read [When to refactor](https://aipatterns.dev/guides/when-to-refactor.md) to decide whether now is the moment. - Read [How to refactor](https://aipatterns.dev/guides/how-to-refactor.md) for the step-by-step discipline. --- # Technical Debt > Technical debt is the extra cost every future change pays because of shortcuts and drift in today's code. Learn its causes and how to pay it down. - kind: guide - category: refactoring - aliases: Tech debt, Design debt - canonical: https://aipatterns.dev/guides/technical-debt - inspired-by: https://refactoring.guru/refactoring/technical-debt ## The metaphor Ward Cunningham compared shipping imperfect code to taking a loan. You get something now (an earlier release) and you pay interest later: every change in that area takes longer than it would in clean code. The principal is the work needed to fix the structure. The interest is the extra time, bugs and risk paid on each change until you do. **Debt is a tool when you take it on purpose and track it; it becomes a crisis when nobody knows it exists.** Debt is not the same as bad code written by careless people. Most debt comes from code that was reasonable when written and then the world moved: requirements changed, the team learned more, a library evolved. ## Where debt comes from | Cause | What it looks like in code | |---|---| | Deadline pressure | Copy-pasted handlers, `// TODO: clean up` comments, flags that bypass validation | | Changing requirements | A model built for one tenant now carries `if (tenant === ...)` branches everywhere. See [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md) | | Missing tests | Nobody dares touch the module, so new code is bolted on beside it instead of into it | | Missing shared understanding | Two teams build two `Customer` types with overlapping fields. See [Alternative Classes with Different Interfaces](https://aipatterns.dev/refactoring/smells/alternative-classes-with-different-interfaces.md) | | Delayed refactoring | A function grows by ten lines per sprint until it is a [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md) | | Long-lived branches | Merges that resolve conflicts by keeping both versions, creating [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) | | Speculation | Abstractions for futures that never came. See [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md) | | Outdated dependencies | Workarounds for bugs fixed upstream years ago | ## How debt shows up You rarely see debt directly. You see its interest: - Estimates for small changes keep growing. - A one-line fix needs edits in six files ([Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md)). - One class changes in every pull request for unrelated reasons ([Divergent Change](https://aipatterns.dev/refactoring/smells/divergent-change.md)). - Bugs reappear after being fixed, because the logic exists in two places. - New contributors, human or agent, make plausible changes that break hidden assumptions. For an agent, debt has a specific cost: more context to load, more places to check, and more chances to produce a change that compiles, passes the visible tests and is still wrong. ## Kinds of debt - **Deliberate and tracked.** "We ship with a hard-coded currency list and replace it next sprint." Acceptable if the ticket exists. - **Deliberate and hidden.** The same shortcut with no record. It becomes accidental debt for the next reader. - **Accidental.** Nobody knew a better design at the time. You discover it later, usually when a change hurts. - **Bit rot.** Many small, reasonable edits that together erode a once-clean design. ## Paying it down 1. **Pay interest where you work.** The cheapest moment to fix structure is right before you change it. See [When to refactor](https://aipatterns.dev/guides/when-to-refactor.md). 2. **Prioritize by change frequency.** Ugly code nobody touches costs almost nothing. Ugly code changed weekly is expensive. Version control history (`git log --format= --name-only | sort | uniq -c | sort -rn`) shows the hot spots. 3. **Add tests first.** Untested debt cannot be repaid safely. Write characterization tests that pin current behavior before restructuring. 4. **Use small named steps.** Each technique in [Composing methods](https://aipatterns.dev/refactoring/techniques/composing-methods.md) or [Moving features between objects](https://aipatterns.dev/refactoring/techniques/moving-features-between-objects.md) is a safe payment. 5. **Record what you defer.** A tracked ticket or an ADR beats a `TODO` comment. Comments rot; see [Comments](https://aipatterns.dev/refactoring/smells/comments.md). ## Do not - Do not schedule a "refactoring sprint" to fix everything. Big-bang cleanups stall feature work and rarely finish. - Do not rewrite a working system to escape debt unless the platform itself is dead. A rewrite starts its own debt, with fewer tests. - Do not refactor code you are not otherwise changing only because it looks bad. Spend effort where change happens. - Do not mix paydown with feature work in one commit. Reviewers cannot tell which lines change behavior. ## Agent checklist - [ ] Before a feature change, check whether the target area shows smells that will make the change larger than it needs to be. - [ ] Pay down only the debt in the path of the current task. Note the rest instead of fixing it. - [ ] Confirm tests cover the code you restructure, or add characterization tests first. - [ ] Keep paydown commits separate from behavior changes. - [ ] When you take a shortcut, leave a tracked note with the limit and the trigger to revisit it. --- # When to Refactor > Refactor right before you change code, while fixing a bug and during review. Skip it when the code is due for deletion or has no tests. - kind: guide - category: refactoring - aliases: Refactoring triggers - canonical: https://aipatterns.dev/guides/when-to-refactor - inspired-by: https://refactoring.guru/refactoring/when ## The short answer **Refactor the code you are about to change, right before you change it.** That is when the cost of bad structure is real and the benefit lands immediately. Refactoring code that nobody will touch again is spending money on a building scheduled for demolition. ## Good moments ### Before adding a feature Read the area you need to change. If the feature would be awkward to add, first reshape the code so the feature becomes a small, obvious addition. Make the change cheap first, then make the cheap change. Typical sequence: 1. The new payment method needs a fourth branch in a [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md) block that already has three. 2. Apply [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md) or extract a [Strategy](https://aipatterns.dev/design-patterns/strategy.md) map. Tests stay green. 3. Add the new method as one new class or function. ### When fixing a bug A bug is evidence that the code is hard to understand. Before fixing it, restructure enough to make the bug visible: [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) to name the steps, [Decompose Conditional](https://aipatterns.dev/refactoring/techniques/decompose-conditional.md) to name the conditions. Often the bug becomes obvious. Then write a failing test, fix, and commit the refactoring and the fix separately. ### The rule of three The first time, write it. The second time you write something similar, note the duplication. The third time, extract the shared part. Two copies may diverge for good reasons; three copies are a pattern. See [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md). ### During code review Review is where a second reader finds names that mislead and functions that do too much. Small renames ([Rename Method](https://aipatterns.dev/refactoring/techniques/rename-method.md)) and extractions are cheap to apply while the author still has context. ### When you need to understand code Renaming a variable once you understand what it holds stores that understanding in the code. The next reader, human or agent, does not need to rediscover it. ## Bad moments - **The code works, is not changing and has no pending work.** Leave it. Ugly but stable code has zero interest. - **The code is scheduled for deletion or replacement.** Spend the effort on the replacement. - **There are no tests and you cannot add them cheaply.** Refactoring without verification is guessing. Add characterization tests first, or restrict yourself to IDE-automated, provably safe transformations (rename, extract with a tool). - **You are close to a deadline and the change is not on the critical path.** Record the debt (see [Technical debt](https://aipatterns.dev/guides/technical-debt.md)) and come back. - **The rewrite is honestly cheaper.** If the code is so broken that it does not work, refactoring has nothing to preserve. Restructuring assumes correct behavior worth keeping. - **The task is not yours.** An agent asked to fix one bug should not restructure three unrelated modules. Scope creep makes diffs unreviewable. ## Decide in one pass | Question | If yes | If no | |---|---|---| | Will you change this code in the current task? | Continue | Do not refactor | | Do tests cover the behavior you will touch? | Continue | Add tests first, or stop | | Would the task's diff be clearly smaller or safer after restructuring? | Refactor first | Make the change directly | | Does the refactoring stay inside the files the task already touches? | Proceed | Note it, do not do it now | ## Signals in the code The smell catalog lists symptoms worth acting on. The most common triggers: - A function longer than a screen: [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md). - A change that needs edits in many files: [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md). - A method that uses another object's data more than its own: [Feature Envy](https://aipatterns.dev/refactoring/smells/feature-envy.md). - The same group of three or four parameters passed everywhere: [Data Clumps](https://aipatterns.dev/refactoring/smells/data-clumps.md). - Comments explaining what a block does: [Comments](https://aipatterns.dev/refactoring/smells/comments.md). ## Agent checklist - [ ] The code you plan to refactor is in the path of the current task. - [ ] Tests cover the behavior, or you added characterization tests first. - [ ] You can state the concrete way the refactoring makes the task smaller or safer. - [ ] The refactoring stays within the scope the user asked for; anything broader is reported, not done. - [ ] Refactoring and behavior change land as separate commits or separate steps. --- # How to Refactor > Refactor in small named steps, run tests after each one, never mix in behavior changes, and stop when the code is clean enough for the task at hand. - kind: guide - category: refactoring - aliases: Refactoring workflow - canonical: https://aipatterns.dev/guides/how-to-refactor - inspired-by: https://refactoring.guru/refactoring/how-to ## The loop **Make one small structural change, run the tests, commit, repeat.** Every refactoring in this reference is built to fit that loop: each technique page has a `## Steps` section where every step keeps the tests green. 1. Confirm the tests pass before you start. A red suite means you cannot tell what you broke. 2. Pick one smell and one technique. Do not start three at once. 3. Apply the technique in the smallest steps that compile. 4. Run the tests after each step. 5. Commit when green, with a message that names the technique ("Extract Method: computeLateFee"). 6. Stop and reassess. The next smell may have disappeared or changed. ## What a finished refactoring looks like - **The code is cleaner.** If a reviewer cannot see the improvement, the change was not worth the diff. - **No new functionality.** Behavior changes go in a separate commit, before or after. - **All tests pass.** Not "most tests", and not "the ones I touched". If any of these fails, you did something other than refactoring. ## Before you start ### Get a safety net Tests that cover the code you will change are a precondition. If they do not exist: - Write **characterization tests**: call the code with representative inputs, record what it returns today, and assert on that. You are pinning behavior, not judging it. - For code that touches I/O, capture inputs and outputs at the boundary and replay them. - Use the type checker as a second net. In TypeScript, `tsc --noEmit` catches broken call sites after a move or rename. ### Read before you cut Identify the smell by name. The smell page lists its treatments with "if X, then use Y" guidance. Picking the technique from the smell is more reliable than inventing a restructuring. ## During the refactoring - **Prefer automated tools.** IDE or language-server rename, extract and move are safer than manual edits because they update every reference. - **Change one thing per step.** Rename, then move, then extract. Not all three in one edit. - **Keep the old path working while you build the new one.** For example, when moving a method, leave a delegating stub in the old location, migrate callers, then delete the stub ([Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md), then [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md)). - **Watch public interfaces.** Renaming a private helper is free. Renaming an exported function affects callers you cannot see. Deprecate first, then remove. - **If the tests go red, revert.** Do not debug a broken refactoring forward. Undo to the last green commit and take a smaller step. ## Common sequences | Goal | Sequence | |---|---| | Break up a long function | [Extract Variable](https://aipatterns.dev/refactoring/techniques/extract-variable.md) to name pieces, [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) per piece, [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md) where temps block extraction | | Split a god class | [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md), then [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) and [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) into it | | Remove a type switch | [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md) or [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md), then [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md) | | Shrink a parameter list | [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md) or [Preserve Whole Object](https://aipatterns.dev/refactoring/techniques/preserve-whole-object.md) | | Merge copies of logic | [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md), then [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md) or [Form Template Method](https://aipatterns.dev/refactoring/techniques/form-template-method.md) if the copies live in sibling classes | ## When to stop Stop when the change you came to make is straightforward. Refactoring has no natural end; every codebase can be cleaner. The task defines the finish line, not your taste. Signs you have gone too far: - You introduced an interface with one implementation and no test double that needs it ([Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md)). - Readers now jump through four files to follow what used to be one function. - The diff touches files unrelated to the task. ## Agent checklist - [ ] The test suite was green before the first edit, and you recorded the command that runs it. - [ ] Each step is one named technique, applied through tooling where available. - [ ] Tests and type checks run after every step, not only at the end. - [ ] A red result triggers a revert to the last green state, not a forward fix. - [ ] Behavior changes are in separate commits from structural changes. - [ ] You stopped when the target task became straightforward. --- # Creational Patterns > Patterns that control how objects get built, so callers depend on what they need rather than on which concrete class supplies it. - kind: category - category: design-patterns - canonical: https://aipatterns.dev/design-patterns/creational - inspired-by: https://refactoring.guru/design-patterns/creational-patterns ## What these patterns solve Every `new ConcreteThing()` is a hard dependency. It fixes the class, the constructor arguments and often the lifetime of the object at the call site. Creational patterns move that decision somewhere else: into a method, an object, a step-by-step assembler or a copy of an existing instance. The caller asks for something that satisfies an interface and stops caring how it was made. **Reach for a creational pattern only when construction itself is the thing that varies or hurts.** Most code should keep calling constructors directly. ## Members - [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md): a method, overridable in subclasses or passed as a function, decides which concrete product to create. - [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md): one object creates a whole family of related products that must be used together, such as one cloud provider's storage, queue and secrets clients. - [Builder](https://aipatterns.dev/design-patterns/builder.md): assembles a complex object in named steps, so you never call a constructor with twelve positional arguments. - [Prototype](https://aipatterns.dev/design-patterns/prototype.md): creates new objects by copying a configured instance instead of rebuilding it from scratch. - [Singleton](https://aipatterns.dev/design-patterns/singleton.md): guarantees one instance of a class with a global access point. Usually better replaced by dependency injection. ## How to choose within this category - **One product, and the concrete type depends on context:** use [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md). In TypeScript, Python, Go or modern Java, a plain function that returns the interface (`createLogger(env)`) is usually enough. Subclassing a creator class is rarely needed. - **Several products that must match each other:** use [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md). With only one product, it collapses into Factory Method. - **Many optional parameters or a multi-step, validated assembly:** use [Builder](https://aipatterns.dev/design-patterns/builder.md). In languages with named or default arguments (Python, Kotlin) or object literals (TypeScript), an options object often removes the need. Keep Builder for cases where order matters or the result must be validated before it exists. - **Construction is expensive or the configuration is only known at runtime:** use [Prototype](https://aipatterns.dev/design-patterns/prototype.md). Check first that your language's copy tools (`structuredClone`, `copy.deepcopy`, records with `with`) don't already do the job. - **Exactly one shared resource:** create it once at the composition root and inject it. Treat [Singleton](https://aipatterns.dev/design-patterns/singleton.md) as a last resort, because it hides dependencies and makes tests share state. See [Factory comparison](https://aipatterns.dev/guides/factory-comparison.md) for the differences between the factory variants. ## Prefer no pattern when - The concrete class never changes. Call the constructor. - A module-level function or a dependency injection container already decides what to build. - You're adding a factory "in case" a second implementation appears. Wait until it does; that urge is [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md). --- # Structural Patterns > Patterns that combine objects and classes into larger structures while keeping each part replaceable and its interface small. - kind: category - category: design-patterns - canonical: https://aipatterns.dev/design-patterns/structural - inspired-by: https://refactoring.guru/design-patterns/structural-patterns ## What these patterns solve Structural patterns deal with how pieces fit together. One interface doesn't match another, a subsystem is too wide to call directly, a feature must be layered on without editing the class, or millions of small objects use too much memory. Each pattern puts a wrapper, a tree or a shared object between the caller and the real work. **Most structural patterns look the same in code: an object that holds another object and forwards calls to it. Tell them apart by intent, not by shape.** ## Members - [Adapter](https://aipatterns.dev/design-patterns/adapter.md): converts one interface into the interface a caller expects, such as wrapping a vendor SDK behind your own port. - [Bridge](https://aipatterns.dev/design-patterns/bridge.md): splits an abstraction from its implementation so both can vary independently, avoiding a class for every combination. - [Composite](https://aipatterns.dev/design-patterns/composite.md): treats a single item and a tree of items through the same interface, as with files and folders. - [Decorator](https://aipatterns.dev/design-patterns/decorator.md): wraps an object to add behavior (caching, retries, logging) while keeping its interface. - [Facade](https://aipatterns.dev/design-patterns/facade.md): gives a complex subsystem one small, task-oriented entry point. - [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md): shares the immutable part of many similar objects to cut memory use. - [Proxy](https://aipatterns.dev/design-patterns/proxy.md): stands in for another object with the same interface to control access: lazy loading, remote calls or permission checks. ## How to choose within this category - **The interface is wrong and you don't own the other side:** use [Adapter](https://aipatterns.dev/design-patterns/adapter.md). - **The interface is fine but you want extra behavior around calls:** use [Decorator](https://aipatterns.dev/design-patterns/decorator.md). If the extra behavior is about *whether or when* the real object is reached, it's a [Proxy](https://aipatterns.dev/design-patterns/proxy.md). In languages with higher-order functions, wrapping a single function (`withRetry(fetchInvoice)`) often replaces a decorator class. - **The subsystem is too big for most callers:** use [Facade](https://aipatterns.dev/design-patterns/facade.md). It adds a simpler interface; it doesn't translate an incompatible one. - **You have nested parts and wholes:** use [Composite](https://aipatterns.dev/design-patterns/composite.md). A recursive union type with pattern matching can replace it in TypeScript, Rust or modern Java. - **Two dimensions vary and subclasses multiply (`PdfInvoice`, `HtmlInvoice`, `PdfReceipt`, ...):** use [Bridge](https://aipatterns.dev/design-patterns/bridge.md). - **Profiling shows memory pressure from duplicated state:** use [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md). Don't apply it without a measurement. ## Prefer no pattern when - You control both sides of an interface mismatch. Change one side instead of adding an adapter. - A wrapper would forward every call unchanged. That's a [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md). - Only one combination of features ever exists. Compose it directly. --- # Behavioral Patterns > Patterns that assign responsibilities and route requests between objects, so behavior can change without rewriting the code that triggers it. - kind: category - category: design-patterns - canonical: https://aipatterns.dev/design-patterns/behavioral - inspired-by: https://refactoring.guru/design-patterns/behavioral-patterns ## What these patterns solve Behavioral patterns are about who does what and who tells whom. They turn requests into objects, let behavior vary by situation, decouple senders from receivers and walk structures without exposing them. Many of them exist to replace a growing conditional or a tangle of direct calls. **Several of these patterns are a function value in disguise. Check that first-class functions, closures, iterators or your platform's events don't already cover the need.** ## Members - [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md): passes a request along a list of handlers until one handles it, as in HTTP middleware. - [Command](https://aipatterns.dev/design-patterns/command.md): wraps an action and its arguments in an object so it can be queued, logged, retried or undone. - [Iterator](https://aipatterns.dev/design-patterns/iterator.md): walks a collection without exposing how it is stored. - [Mediator](https://aipatterns.dev/design-patterns/mediator.md): routes communication between many components through one coordinator instead of letting them call each other. - [Memento](https://aipatterns.dev/design-patterns/memento.md): captures an object's state as an opaque snapshot so it can be restored later without breaking encapsulation. - [Observer](https://aipatterns.dev/design-patterns/observer.md): lets objects subscribe to events from another object and get notified when they occur. - [State](https://aipatterns.dev/design-patterns/state.md): moves state-specific behavior into separate objects so an object changes behavior when its state changes. - [Strategy](https://aipatterns.dev/design-patterns/strategy.md): puts interchangeable algorithms behind one interface and lets the caller pick one at runtime. - [Template Method](https://aipatterns.dev/design-patterns/template-method.md): fixes an algorithm's skeleton in a base class and lets subclasses fill in specific steps. - [Visitor](https://aipatterns.dev/design-patterns/visitor.md): adds operations over a fixed set of node types without editing those types. ## How to choose within this category - **A `switch` picks between algorithms:** [Strategy](https://aipatterns.dev/design-patterns/strategy.md). A function parameter is often enough. - **A `switch` on a status field appears in many methods, and the status changes over time:** [State](https://aipatterns.dev/design-patterns/state.md). For a handful of states, a transition table or a discriminated union may be clearer. - **The steps are fixed and only a few vary:** [Template Method](https://aipatterns.dev/design-patterns/template-method.md). Passing the varying steps as functions avoids inheritance. - **An action must be queued, retried, logged or undone:** [Command](https://aipatterns.dev/design-patterns/command.md). Undo also needs [Memento](https://aipatterns.dev/design-patterns/memento.md) or an inverse operation. - **Several handlers might process a request, in order:** [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md). - **One change must notify an unknown number of listeners:** [Observer](https://aipatterns.dev/design-patterns/observer.md). Use your platform's event emitter, channels or signals before writing your own. - **Many components call each other directly:** [Mediator](https://aipatterns.dev/design-patterns/mediator.md). - **You need many operations over a stable set of types:** [Visitor](https://aipatterns.dev/design-patterns/visitor.md). Exhaustive pattern matching over a sealed or union type replaces it in TypeScript, Rust, Kotlin and Java 21+. See [Visitor and double dispatch](https://aipatterns.dev/guides/visitor-and-double-dispatch.md). - **You need to traverse a custom structure:** implement your language's iteration protocol (`Symbol.iterator`, `__iter__`, `Iterable`, range-over-func in Go) rather than a separate [Iterator](https://aipatterns.dev/design-patterns/iterator.md) class. ## Prefer no pattern when - There are two branches and no third in sight. Keep the `if`. - The behavior is one stateless function. Pass the function. - Only one listener or handler will ever exist. Call it directly. --- # Resilience Patterns > Patterns for calls that can fail, hang or come back wrong: bound the wait, count the failures, space the repeats, and continue with a lesser answer. - kind: category - category: design-patterns - canonical: https://aipatterns.dev/design-patterns/resilience - inspired-by: https://learn.microsoft.com/en-us/azure/architecture/patterns ## What these patterns solve A dependency that fails slowly is worse than one that fails loudly. A hung socket holds a worker, the held workers exhaust the pool, and a dead shipping API becomes a dead checkout. And once a call does fail, the caller has three honest options: try again because the failure looked transient, stop trying because the dependency is down, or continue without the answer. Without patterns the defaults are the worst ones: unbounded waits, immediate repeats, and no plan B. **These patterns convert slow failure into fast failure, bound every repeat, and decide what the caller serves instead.** ## Members - [Timeout](https://aipatterns.dev/design-patterns/timeout.md): give every remote call a deadline shorter than the caller's own budget, cancel the work when it expires, and surface the timeout as a distinct error. - [Circuit Breaker](https://aipatterns.dev/design-patterns/circuit-breaker.md): count consecutive failures, refuse to call a dependency that just failed repeatedly, and let a probe test recovery before resuming. - [Retry](https://aipatterns.dev/design-patterns/retry.md): re-attempt only retryable, repeatable operations a bounded number of times, honouring the server's `Retry-After`, and surface the last error. - [Exponential Backoff](https://aipatterns.dev/design-patterns/exponential-backoff.md): the spacing policy for repeats - double the wait each attempt, cap it, and jitter so concurrent callers do not retry in lockstep. - [Fallback](https://aipatterns.dev/design-patterns/fallback.md): return a pre-decided lesser answer - stale cache, static default, partial result - so the caller's journey continues, marked as degraded. - [Rate Limiting](https://aipatterns.dev/design-patterns/rate-limiting.md): give each client a refillable budget of calls; refuse with `429` and a `Retry-After` when it runs out. ## How to choose - **A remote call has no explicit deadline:** [Timeout](https://aipatterns.dev/design-patterns/timeout.md). This is the baseline; nothing else here works without it. - **The error is transient-looking (timeouts, 429, 503) and the operation can be repeated safely:** [Retry](https://aipatterns.dev/design-patterns/retry.md) with [Exponential Backoff](https://aipatterns.dev/design-patterns/exponential-backoff.md) spacing. - **Calls keep failing in bursts and callers pile up waiting or retrying:** [Circuit Breaker](https://aipatterns.dev/design-patterns/circuit-breaker.md). It needs timeouts to classify hangs as failures first. - **The user journey must continue without the data:** [Fallback](https://aipatterns.dev/design-patterns/fallback.md) - after retries are exhausted, or immediately while a breaker is open. - **One client's burst starves the others:** [Rate Limiting](https://aipatterns.dev/design-patterns/rate-limiting.md). Ration per client before the resource notices the load. - **The operation cannot be repeated safely (payments, writes without an idempotency key):** none of them. Make it repeatable first ([Command](https://aipatterns.dev/design-patterns/command.md) plus a durable queue with deduplication), or fail and let a human or workflow decide. ## Prefer no pattern when - The call is in-process and cannot hang. A function call that throws needs no deadline. - The platform already enforces deadlines or retries end to end (gRPC deadlines, Go `context`, HTTP clients with built-in retry). Configure those instead of wrapping a second layer - two retry layers multiply, not add. - The failure is permanent (validation, missing resource, business rejection). Fix the input; do not repeat it. - The caller has no latency budget for a second attempt. Fail fast instead. --- # Data Patterns > Patterns for keeping application data honest at speed and across services: copies that exist only to be lost, multi-step transactions with explicit undos, and messages that cannot be lost or doubled. - kind: category - category: design-patterns - canonical: https://aipatterns.dev/design-patterns/data - inspired-by: https://learn.microsoft.com/en-us/azure/architecture/patterns ## What these patterns solve An application that scales up its reads eventually holds data in more than one place, and every extra copy is a way to be wrong. A workflow that spans services cannot share a transaction, and a state change that must notify the world has two ways to fail - silently losing the message, or delivering it twice. These patterns fix the division of labor: the store decides the truth, the copies accelerate it, workflows get all-or-nothing semantics without locks, and messages become data before they become side effects. **The rule they all enforce: writes land in the source of truth first; anything else is a disposable speedup - and every copy or message says exactly how stale or how repeated it may be.** ## Members - [Cache-Aside](https://aipatterns.dev/design-patterns/cache-aside.md): load into a cache on miss, save to the store and evict on write - so reads speed up while the store stays the only source of truth. - [Transactional Outbox](https://aipatterns.dev/design-patterns/transactional-outbox.md): write the outgoing message inside the business transaction; a relay publishes it afterwards - the commit and the message are one fact. - [Saga](https://aipatterns.dev/design-patterns/saga.md): run a multi-service workflow as local steps with compensations, undone in reverse on failure - consistency without distributed locks. - [Idempotent Consumer](https://aipatterns.dev/design-patterns/idempotent-consumer.md): claim each message id in the processing transaction, so at-least-once delivery produces exactly-once effects. - [CQRS](https://aipatterns.dev/design-patterns/cqrs.md): separate the command model from per-view read models joined by projections - each shaped for its job, at the price of two models and read lag. ## How to choose - **Reads dominate, the query is expensive, and slightly stale answers are acceptable:** [Cache-Aside](https://aipatterns.dev/design-patterns/cache-aside.md). Give every entry a TTL and evict on write - never write to both copies and hope. - **A state change must reliably produce a message:** [Transactional Outbox](https://aipatterns.dev/design-patterns/transactional-outbox.md). The outbox row commits with the change; never send from inside or just after a transaction. - **A workflow spans services and must be all-or-nothing:** [Saga](https://aipatterns.dev/design-patterns/saga.md). Name every compensation first; a step without one goes last or becomes retriable. - **Delivery is at-least-once anywhere in the path:** [Idempotent Consumer](https://aipatterns.dev/design-patterns/idempotent-consumer.md) on every counting handler - outbox relays, webhook providers, and queue redelivery all guarantee duplicates. - **Reads and writes measurably fight over one model, on one contentious subdomain:** [CQRS](https://aipatterns.dev/design-patterns/cqrs.md). Elsewhere, CRUD; be very cautious. - **The value must always be current for correctness and lives in one store:** none of these. Read the store. ## Prefer no pattern when - The store is in-process and already fast - a map beside your map is not a cache layer. - Writes are as frequent as reads; the invalidation overhead cancels the read savings. - The whole workflow fits one database transaction - a saga re-implements it without the isolation. - You have not measured the query you are about to cache, or the contention you are about to split. --- # Architecture Patterns > Patterns for system boundaries and change over time: replace without stopping, and give each client a backend shaped for it - decisions measured in routes and releases, not classes. - kind: category - category: design-patterns - canonical: https://aipatterns.dev/design-patterns/architecture - inspired-by: https://learn.microsoft.com/en-us/azure/architecture/patterns ## What these patterns solve Class patterns shape objects; these shape systems and the seams between teams. The pressure they answer is longevity: a system must be replaceable piece by piece, and its API surface must serve clients that evolve on different clocks. Both patterns are really ownership decisions - who owns each route, who owns each client's contract - expressed as structure. **The rule they all enforce: the boundary is the design; everything inside it can change.** ## Members - [Strangler Fig](https://aipatterns.dev/design-patterns/strangler-fig.md): route all traffic through one interception layer and replace handlers one route at a time, until the legacy system has nothing left to serve. - [Backends for Frontends](https://aipatterns.dev/design-patterns/backends-for-frontends.md): give each frontend its own aggregation backend, so no client's roadmap waits on a shared API. ## How to choose - **A legacy system must shrink while it keeps serving:** [Strangler Fig](https://aipatterns.dev/design-patterns/strangler-fig.md). The facade first, then the smallest valuable route, then the trend to zero. - **Two or more frontends fight over one API's shape or release train:** [Backends for Frontends](https://aipatterns.dev/design-patterns/backends-for-frontends.md). One client with no rival gets no BFF. - **One cutover is testable and the system is small:** neither. Rewrite it; migration scaffolding for a shed is a shed. ## Prefer no pattern when - The pain is hypothetical - no team owns the legacy removal, no client actually disputes the shared API. - The real problem is data entanglement; routing patterns cannot untangle a shared schema. - The team cannot own one more deployable per boundary. --- # Bloaters > Smells where methods, classes and parameter lists grow too large to read, test or change safely, usually one small addition at a time. - kind: category - category: refactoring - canonical: https://aipatterns.dev/refactoring/smells/bloaters - inspired-by: https://refactoring.guru/refactoring/smells/bloaters ## What these smells share Bloaters are code that grew past the size a reader can hold in their head. No single commit caused them. Each change added a few lines or one more argument, and each was reasonable alone. The result is a method, class or signature that does too much, or a set of loose values that should have been one type. Bloat raises the cost of every later change: more to read, more to test, more places for a bug to hide. It also hides duplication, because nobody spots the same logic in two 300-line methods. ## Members - [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md): a function long enough that you need comments or scrolling to follow it. - [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md): a class with too many fields, methods or responsibilities to describe in one sentence. - [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md): domain concepts such as money, email or status are passed around as raw strings and numbers. - [Long Parameter List](https://aipatterns.dev/refactoring/smells/long-parameter-list.md): a function takes four or more arguments, often several of the same type. - [Data Clumps](https://aipatterns.dev/refactoring/smells/data-clumps.md): the same group of values travels together through fields and signatures without its own type. ## How to choose within this category - Start from the unit you are looking at. A function that is too long is [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md). A type that is too wide is [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md). - If a signature is too long, check whether the arguments always appear together. If they do, the root cause is [Data Clumps](https://aipatterns.dev/refactoring/smells/data-clumps.md), and the long list is a symptom. - If the clumped values are primitives that carry rules (a currency code, a range that must be ordered), the deeper smell is [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md). Introducing a value type fixes both. - Fix the smallest unit first. Extracting methods from a long method often reveals which fields belong together, which then shows how to split the large class. ## Typical treatments - [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) and [Replace Method with Method Object](https://aipatterns.dev/refactoring/techniques/replace-method-with-method-object.md) for long functions. - [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) and [Extract Subclass](https://aipatterns.dev/refactoring/techniques/extract-subclass.md) for wide classes. - [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md) and [Preserve Whole Object](https://aipatterns.dev/refactoring/techniques/preserve-whole-object.md) for long signatures and clumps. - [Replace Data Value with Object](https://aipatterns.dev/refactoring/techniques/replace-data-value-with-object.md) for primitives that carry rules. ## For agents - Size thresholds are prompts to look, not rules. A 60-line table of constants is not a long method. - Split by responsibility, not by line count. Each extracted piece needs a name that says what it does. - Run the tests after each extraction. Bloated code often has hidden ordering dependencies. --- # Composing Methods > Techniques that split, merge and reshape method bodies so each method does one readable thing and local variables stop blocking change. - kind: category - category: refactoring - canonical: https://aipatterns.dev/refactoring/techniques/composing-methods - inspired-by: https://refactoring.guru/refactoring/techniques/composing-methods ## What these techniques solve Most refactoring starts inside a single function. Long bodies, temporary variables that hold three different things, and parameters reassigned halfway down make code hard to read and hard to move. These techniques work at that level: they change how a method is cut into pieces without changing what it does. **Use them first. Clean method boundaries are what make every other refactoring possible.** ## Members - [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md): move a fragment into its own well-named method and call it. - [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md): replace calls to a method whose body is as clear as its name with the body itself. - [Extract Variable](https://aipatterns.dev/refactoring/techniques/extract-variable.md): name a hard-to-read subexpression by assigning it to a local variable. - [Inline Temp](https://aipatterns.dev/refactoring/techniques/inline-temp.md): remove a variable that only holds a trivial expression and replace its uses with the expression. - [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md): turn a computed local into a method so other methods can reuse it and extraction gets easier. - [Split Temporary Variable](https://aipatterns.dev/refactoring/techniques/split-temporary-variable.md): give each distinct use of a reused local variable its own variable. - [Remove Assignments to Parameters](https://aipatterns.dev/refactoring/techniques/remove-assignments-to-parameters.md): copy a parameter to a local instead of reassigning it. - [Replace Method with Method Object](https://aipatterns.dev/refactoring/techniques/replace-method-with-method-object.md): turn a long method with tangled locals into a class whose fields are those locals. - [Substitute Algorithm](https://aipatterns.dev/refactoring/techniques/substitute-algorithm.md): replace a method body with a clearer algorithm that produces the same results. ## How to choose within this category - **A method is long or needs comments to explain its parts:** start with [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md). Name each piece after what it does, not how. - **Locals prevent extraction because too many of them flow in and out:** first apply [Split Temporary Variable](https://aipatterns.dev/refactoring/techniques/split-temporary-variable.md) and [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md). If that still fails, use [Replace Method with Method Object](https://aipatterns.dev/refactoring/techniques/replace-method-with-method-object.md). - **One expression is dense:** [Extract Variable](https://aipatterns.dev/refactoring/techniques/extract-variable.md). If the value is useful elsewhere in the class, prefer a query method. - **Indirection without meaning:** [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md) or [Inline Temp](https://aipatterns.dev/refactoring/techniques/inline-temp.md). They are the inverses of the two extractions above; use them to undo a bad split before re-cutting it. - **A parameter changes value inside the body:** [Remove Assignments to Parameters](https://aipatterns.dev/refactoring/techniques/remove-assignments-to-parameters.md). In languages that allow `final` or `const` parameters, mark them so after the change. - **The whole thing can be done in fewer lines, often with a standard library call:** [Substitute Algorithm](https://aipatterns.dev/refactoring/techniques/substitute-algorithm.md). Have tests that pin current behavior before you swap it. Watch performance claims: replacing a temp with a query repeats the computation. That rarely matters, but measure in hot loops. --- # Moving Features between Objects > Techniques that relocate methods, fields and whole classes so each responsibility lives next to the data it uses. - kind: category - category: refactoring - canonical: https://aipatterns.dev/refactoring/techniques/moving-features-between-objects - inspired-by: https://refactoring.guru/refactoring/techniques/moving-features-between-objects ## What these techniques solve Code ends up in the wrong place. A method uses another class's fields more than its own, a class has grown two unrelated jobs, or callers walk long chains of getters to reach what they need. These techniques move behavior and data across class boundaries, or redraw the boundaries themselves. **Put each piece of behavior where the data it reads lives, and hide how objects are connected from the code that uses them.** ## Members - [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md): move a method to the class it uses most and leave a delegate or update callers. - [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md): move a field to the class whose methods actually read and write it. - [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md): split a class that does two jobs into two classes. - [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md): fold a class that does almost nothing into the class that uses it. - [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md): add a method on the server so clients stop reaching through it to a delegate. - [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md): let clients call the delegate directly when a class only forwards calls. - [Introduce Foreign Method](https://aipatterns.dev/refactoring/techniques/introduce-foreign-method.md): add a helper in your code that takes an instance of a library class you can't edit. - [Introduce Local Extension](https://aipatterns.dev/refactoring/techniques/introduce-local-extension.md): create a subclass or wrapper of a library class to hold several missing methods. ## How to choose within this category - **A method reads another object's data more than its own:** [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md). If only part of the method envies the other class, extract that part first. - **A class has fields and methods that cluster into two groups:** [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md). The opposite case, a class too thin to justify itself, calls for [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md). - **Callers write `a.getB().getC().doThing()`:** [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md). If the server then becomes all forwarding methods, go the other way with [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md). The right balance shifts as code changes, so expect to move between them. - **A library class lacks one method:** [Introduce Foreign Method](https://aipatterns.dev/refactoring/techniques/introduce-foreign-method.md). In Kotlin, C# or Swift, an extension function does this natively. In TypeScript, Python and Go, a module-level function is the idiomatic form. - **A library class lacks several methods:** [Introduce Local Extension](https://aipatterns.dev/refactoring/techniques/introduce-local-extension.md). Prefer a wrapper over a subclass when the library class is final or its constructor is complex. - **A field is used mostly by another class:** [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md), usually before moving the methods that use it. Move one member at a time and run tests after each move. Cross-class moves are where silent behavior changes hide, especially with inheritance and overrides. --- # Object-Orientation Abusers > Smells where object-oriented features are missing or misapplied, such as type switches, half-used fields and inheritance that does not fit. - kind: category - category: refactoring - aliases: OO Abusers - canonical: https://aipatterns.dev/refactoring/smells/oo-abusers - inspired-by: https://refactoring.guru/refactoring/smells/oo-abusers ## What these smells share These smells come from using classes without using what classes offer. Code branches on a type tag instead of letting each type answer for itself. Fields exist for only one code path. Subclasses inherit methods they refuse to support. Two classes do the same job behind different method names. The cost is fragility. Adding a variant means finding every switch. Calling a method on a subclass may throw. Swapping one implementation for another means rewriting callers. ## Members - [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md): the same `switch` or `if/else` chain over a type code is repeated in several places. - [Temporary Field](https://aipatterns.dev/refactoring/smells/temporary-field.md): a field is set and read only during one operation and is empty or meaningless the rest of the time. - [Refused Bequest](https://aipatterns.dev/refactoring/smells/refused-bequest.md): a subclass ignores, overrides with no-ops, or throws on methods it inherits. - [Alternative Classes with Different Interfaces](https://aipatterns.dev/refactoring/smells/alternative-classes-with-different-interfaces.md): two classes do the same thing but expose different method names and signatures. ## How to choose within this category - If the problem is a conditional on a kind or status, it is [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md). One switch is fine; the same switch in three places is the smell. - If the problem is a field that is often `null` or unset, it is [Temporary Field](https://aipatterns.dev/refactoring/smells/temporary-field.md). Look for the one method that uses it. - If the problem is inheritance that does not hold, such as overridden methods that throw `NotSupported`, it is [Refused Bequest](https://aipatterns.dev/refactoring/smells/refused-bequest.md). - If the problem is two types that could be swapped but cannot because their APIs differ, it is [Alternative Classes with Different Interfaces](https://aipatterns.dev/refactoring/smells/alternative-classes-with-different-interfaces.md). ## Typical treatments - [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md) and [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md) for type switches. - [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) and [Introduce Null Object](https://aipatterns.dev/refactoring/techniques/introduce-null-object.md) for temporary fields. - [Replace Inheritance with Delegation](https://aipatterns.dev/refactoring/techniques/replace-inheritance-with-delegation.md) and [Push Down Method](https://aipatterns.dev/refactoring/techniques/push-down-method.md) for refused bequests. - [Rename Method](https://aipatterns.dev/refactoring/techniques/rename-method.md), [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md) and [Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md) for mismatched interfaces. ## For agents - Polymorphism is not always the answer. In languages with sum types and exhaustive pattern matching (TypeScript discriminated unions, Rust, Kotlin sealed classes, Swift enums), one exhaustive `switch` checked by the compiler is often clearer than a class hierarchy. - Prefer composition when fixing inheritance problems. Do not add a deeper hierarchy to fix a shallow one. --- # Change Preventers > Smells where one change forces edits in many places, or one class changes for many unrelated reasons, making every edit slow and risky. - kind: category - category: refactoring - canonical: https://aipatterns.dev/refactoring/smells/change-preventers - inspired-by: https://refactoring.guru/refactoring/smells/change-preventers ## What these smells share Change preventers show up in the commit history more than in any single file. You notice them when a small feature takes a day because it touches ten files, or when one class appears in every pull request regardless of topic. The structure of the code does not match the axes along which requirements change. The fix is to regroup code so that things which change together live together, and things which change for different reasons live apart. ## Members - [Divergent Change](https://aipatterns.dev/refactoring/smells/divergent-change.md): one class is edited for many unrelated reasons, such as both database changes and pricing rules. - [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md): one logical change requires small edits across many classes. - [Parallel Inheritance Hierarchies](https://aipatterns.dev/refactoring/smells/parallel-inheritance-hierarchies.md): every new subclass in one hierarchy needs a matching subclass in another. ## How to choose within this category - Divergent Change and Shotgun Surgery are mirror images. Ask which way the change spreads. - One class, many reasons to change: [Divergent Change](https://aipatterns.dev/refactoring/smells/divergent-change.md). Split the class. - One reason, many classes to change: [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md). Gather the pieces. - If the scattering follows a pair of class hierarchies that grow in lockstep, it is the special case [Parallel Inheritance Hierarchies](https://aipatterns.dev/refactoring/smells/parallel-inheritance-hierarchies.md). - Use version control to confirm. Files that are always committed together point to shotgun surgery; a file touched by every unrelated change points to divergent change. ## Typical treatments - [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) to split a class along its reasons to change. - [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) and [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) to gather scattered behavior into one place. - [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md) when a scattered concern ended up in many tiny classes. - Move Method and Move Field to fold one parallel hierarchy into the other. ## For agents - Do not over-correct. Splitting a class for every possible reason creates shotgun surgery; merging everything to avoid it creates divergent change. Group by the changes you have actually seen. - Check the git log for co-change patterns before restructuring. It is better evidence than reading the code. --- # Organizing Data > Techniques that turn raw fields, magic values, arrays and type codes into objects and types that carry meaning and protect their own invariants. - kind: category - category: refactoring - canonical: https://aipatterns.dev/refactoring/techniques/organizing-data - inspired-by: https://refactoring.guru/refactoring/techniques/organizing-data ## What these techniques solve Data that is only strings, numbers and loose arrays spreads its rules everywhere. Every caller re-validates an email, remembers that index `2` is the currency, or checks `type === 3`. These techniques give data a proper home: small value objects, encapsulated fields and collections, named constants, and real types in place of codes. **Make illegal states hard to represent and put the rules for a value next to the value.** ## Members - [Self Encapsulate Field](https://aipatterns.dev/refactoring/techniques/self-encapsulate-field.md): access a field through getters and setters even inside its own class. - [Replace Data Value with Object](https://aipatterns.dev/refactoring/techniques/replace-data-value-with-object.md): turn a primitive with behavior attached to it into a small class. - [Change Value to Reference](https://aipatterns.dev/refactoring/techniques/change-value-to-reference.md): replace many equal copies of an entity with one shared instance. - [Change Reference to Value](https://aipatterns.dev/refactoring/techniques/change-reference-to-value.md): turn a small, rarely changing reference object into an immutable value. - [Replace Array with Object](https://aipatterns.dev/refactoring/techniques/replace-array-with-object.md): replace an array whose positions mean different things with an object with named fields. - [Duplicate Observed Data](https://aipatterns.dev/refactoring/techniques/duplicate-observed-data.md): move domain data out of a UI class and keep the two in sync. - [Change Unidirectional Association to Bidirectional](https://aipatterns.dev/refactoring/techniques/change-unidirectional-association-to-bidirectional.md): add a back-reference when both sides need to navigate to each other. - [Change Bidirectional Association to Unidirectional](https://aipatterns.dev/refactoring/techniques/change-bidirectional-association-to-unidirectional.md): drop a back-reference that one side no longer needs. - [Replace Magic Number with Symbolic Constant](https://aipatterns.dev/refactoring/techniques/replace-magic-number-with-symbolic-constant.md): give an unexplained literal a name. - [Encapsulate Field](https://aipatterns.dev/refactoring/techniques/encapsulate-field.md): make a public field private and add accessors. - [Encapsulate Collection](https://aipatterns.dev/refactoring/techniques/encapsulate-collection.md): return a read-only view of a collection and provide add and remove methods. - [Replace Type Code with Class](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-class.md): replace a code that doesn't affect behavior with a dedicated type or enum. - [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md): replace a fixed code that drives behavior with one subclass per value. - [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md): replace a code that drives behavior and changes at runtime with a state or strategy object. - [Replace Subclass with Fields](https://aipatterns.dev/refactoring/techniques/replace-subclass-with-fields.md): collapse subclasses that differ only in constant values into fields. ## How to choose within this category - **A primitive carries rules (format, range, unit):** [Replace Data Value with Object](https://aipatterns.dev/refactoring/techniques/replace-data-value-with-object.md). In TypeScript, a branded type or a small class works; in Python, a frozen dataclass; in Java, a record. - **Positions in an array or tuple mean different things:** [Replace Array with Object](https://aipatterns.dev/refactoring/techniques/replace-array-with-object.md). - **A literal like `86400` or `0.2` appears in logic:** [Replace Magic Number with Symbolic Constant](https://aipatterns.dev/refactoring/techniques/replace-magic-number-with-symbolic-constant.md). - **Outside code writes fields or mutates a list directly:** [Encapsulate Field](https://aipatterns.dev/refactoring/techniques/encapsulate-field.md) and [Encapsulate Collection](https://aipatterns.dev/refactoring/techniques/encapsulate-collection.md). Language properties (C#, Kotlin, Python `@property`, TypeScript `get`) let you add the accessor later without breaking callers, so don't add boilerplate getters up front. - **Type codes:** ask two questions. Does the code change behavior? If not, use [Replace Type Code with Class](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-class.md); a language enum or string-literal union is often enough. Does the code change after creation? If not, use [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md); if it does, use [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md). If subclasses exist only to return different constants, reverse it with [Replace Subclass with Fields](https://aipatterns.dev/refactoring/techniques/replace-subclass-with-fields.md). - **Identity questions:** the same customer loaded twice and updated in one place calls for [Change Value to Reference](https://aipatterns.dev/refactoring/techniques/change-value-to-reference.md). A small object that would be safer immutable calls for [Change Reference to Value](https://aipatterns.dev/refactoring/techniques/change-reference-to-value.md). - **Associations:** add a back-reference only when navigation is needed in both directions, since it doubles the bookkeeping. Remove it as soon as it isn't. - **Domain data trapped in a UI component:** [Duplicate Observed Data](https://aipatterns.dev/refactoring/techniques/duplicate-observed-data.md). Modern UI frameworks with a separate state store often give you this split for free. - **Subclasses or lazy-loading need to intercept field access:** [Self Encapsulate Field](https://aipatterns.dev/refactoring/techniques/self-encapsulate-field.md). Skip it otherwise. --- # Dispensables > Smells where code, comments or abstractions add nothing, and removing them makes the program shorter and clearer with no loss of behavior. - kind: category - category: refactoring - canonical: https://aipatterns.dev/refactoring/smells/dispensables - inspired-by: https://refactoring.guru/refactoring/smells/dispensables ## What these smells share Dispensables are things you can delete. Some were useful once and outlived their purpose. Some were built for a future that never arrived. Some copy logic that already exists elsewhere. All of them cost reading time and maintenance while giving nothing back. The default treatment is removal or merging. That makes these the safest smells to fix, provided you confirm the code is truly unused or truly duplicated. ## Members - [Comments](https://aipatterns.dev/refactoring/smells/comments.md): comments that explain what unclear code does, instead of the code being made clear. - [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md): the same logic appears in two or more places and must be changed in each. - [Lazy Class](https://aipatterns.dev/refactoring/smells/lazy-class.md): a class does too little to justify its existence. - [Data Class](https://aipatterns.dev/refactoring/smells/data-class.md): a class has fields and accessors but no behavior, while others operate on its data. - [Dead Code](https://aipatterns.dev/refactoring/smells/dead-code.md): variables, parameters, methods or branches that are never used or never reached. - [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md): hooks, parameters and abstract classes added for needs that do not exist. ## How to choose within this category - If the code is never executed, it is [Dead Code](https://aipatterns.dev/refactoring/smells/dead-code.md). Delete it. - If the code runs but copies other code, it is [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md). Merge it. - If an abstraction exists with one implementation and no planned second, it is [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md). Collapse it. - If a class exists but does almost nothing, it is [Lazy Class](https://aipatterns.dev/refactoring/smells/lazy-class.md). Inline it into its user. - If a class holds data and others do the work on it, it is [Data Class](https://aipatterns.dev/refactoring/smells/data-class.md). Move the behavior in, rather than deleting the class. - If a comment explains what a block does, it signals [Comments](https://aipatterns.dev/refactoring/smells/comments.md). Extract and name the block, then delete the comment. Keep comments that explain why. ## Typical treatments - [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md), [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md) and [Form Template Method](https://aipatterns.dev/refactoring/techniques/form-template-method.md) for duplication. - [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md) and [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md) for lazy classes and speculative abstractions. - [Remove Parameter](https://aipatterns.dev/refactoring/techniques/remove-parameter.md) for unused arguments. - [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) and [Encapsulate Field](https://aipatterns.dev/refactoring/techniques/encapsulate-field.md) for data classes. ## For agents - Verify "unused" with a whole-repository search, including reflection, string-based lookups, public API exports and configuration files. Static analysis misses dynamic calls. - Do not merge code that only looks alike. Two blocks that change for different reasons are not duplicates. - A data class at a system boundary (DTO, wire format, database row) is often intentional. --- # Simplifying Conditional Expressions > Techniques that flatten, name, merge and replace conditionals so branching logic reads as a list of plain decisions. - kind: category - category: refactoring - canonical: https://aipatterns.dev/refactoring/techniques/simplifying-conditional-expressions - inspired-by: https://refactoring.guru/refactoring/techniques/simplifying-conditional-expressions ## What these techniques solve Conditionals grow by accretion. Each bug fix adds a clause, each feature adds a branch, and the result is nested `if` blocks with duplicated tails, control flags and null checks scattered across callers. These techniques make each decision explicit, remove repetition between branches, and move type-based branching into the types themselves. **A reader should see the normal path first and each exception as one named, early decision.** ## Members - [Decompose Conditional](https://aipatterns.dev/refactoring/techniques/decompose-conditional.md): extract the condition and each branch body into well-named methods. - [Consolidate Conditional Expression](https://aipatterns.dev/refactoring/techniques/consolidate-conditional-expression.md): merge several checks that lead to the same result into one named condition. - [Consolidate Duplicate Conditional Fragments](https://aipatterns.dev/refactoring/techniques/consolidate-duplicate-conditional-fragments.md): move code that runs in every branch outside the conditional. - [Remove Control Flag](https://aipatterns.dev/refactoring/techniques/remove-control-flag.md): replace a boolean that steers a loop with `break`, `continue` or `return`. - [Replace Nested Conditional with Guard Clauses](https://aipatterns.dev/refactoring/techniques/replace-nested-conditional-with-guard-clauses.md): handle special cases with early returns so the main path isn't nested. - [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md): move each branch of a type-based conditional into a method on the matching subclass or strategy. - [Introduce Null Object](https://aipatterns.dev/refactoring/techniques/introduce-null-object.md): return an object with safe default behavior instead of `null`, so callers stop checking. - [Introduce Assertion](https://aipatterns.dev/refactoring/techniques/introduce-assertion.md): make an assumption the code already relies on explicit with an assertion. ## How to choose within this category - **The condition itself is hard to read:** [Decompose Conditional](https://aipatterns.dev/refactoring/techniques/decompose-conditional.md). Several conditions with the same outcome: [Consolidate Conditional Expression](https://aipatterns.dev/refactoring/techniques/consolidate-conditional-expression.md) first, then name the result. - **Branches end or start with the same lines:** [Consolidate Duplicate Conditional Fragments](https://aipatterns.dev/refactoring/techniques/consolidate-duplicate-conditional-fragments.md). - **Deep nesting where outer branches only check for special cases:** [Replace Nested Conditional with Guard Clauses](https://aipatterns.dev/refactoring/techniques/replace-nested-conditional-with-guard-clauses.md). - **A `done` or `found` flag controls a loop:** [Remove Control Flag](https://aipatterns.dev/refactoring/techniques/remove-control-flag.md). Often a standard library call (`find`, `some`, `any`) removes the loop entirely. - **The same `switch` on a type appears in several places:** [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md). For a single `switch` over a closed union, exhaustive pattern matching (TypeScript `never` checks, Rust `match`, Java sealed `switch`) is often clearer than a class per case. - **`if (x == null)` checks repeat for the same default behavior:** [Introduce Null Object](https://aipatterns.dev/refactoring/techniques/introduce-null-object.md). Where optional chaining (`?.`), `??`, or `Optional` already reads well, prefer those. - **Code silently assumes something is true:** [Introduce Assertion](https://aipatterns.dev/refactoring/techniques/introduce-assertion.md). Don't use assertions to validate user input; that is error handling, not an invariant. --- # Couplers > Smells where classes depend too much on each other's data or structure, or where delegation is overused, so one change ripples through others. - kind: category - category: refactoring - canonical: https://aipatterns.dev/refactoring/smells/couplers - inspired-by: https://refactoring.guru/refactoring/smells/couplers ## What these smells share Couplers are about the lines between classes, not the classes themselves. Either behavior sits in the wrong class and keeps reaching for another's data, or callers know too much about how objects are wired together, or a class adds a layer that only forwards. Too much coupling makes changes spread. Too much indirection makes code hard to follow. These smells pull in both directions, so fixing one can create another. ## Members - [Feature Envy](https://aipatterns.dev/refactoring/smells/feature-envy.md): a method uses another object's data more than its own. - [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md): two classes reach into each other's private parts. - [Message Chains](https://aipatterns.dev/refactoring/smells/message-chains.md): clients navigate `a.b().c().d()` to reach what they need. - [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md): a class mostly forwards calls to another and adds nothing. ## How to choose within this category - One method leaning on one other class is [Feature Envy](https://aipatterns.dev/refactoring/smells/feature-envy.md). Move the method. - Two classes leaning on each other, in both directions, is [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md). Separate the owners. - A client walking through several objects is [Message Chains](https://aipatterns.dev/refactoring/smells/message-chains.md). Hide the path behind the first object. - A class that is all forwarding is [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md). Remove the layer. - Message Chains and Middle Man are opposites. [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md) fixes chains but, overused, creates a middle man; [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md) fixes the middle man but can bring chains back. Aim for the balance where clients depend on a near neighbor and each forwarding method has callers. ## Typical treatments - [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) and [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) for envy and intimacy. - [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) and [Change Bidirectional Association to Unidirectional](https://aipatterns.dev/refactoring/techniques/change-bidirectional-association-to-unidirectional.md) to untangle two-way dependencies. - [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md) for chains. - [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md) and [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md) for pure forwarders. ## For agents - Some forwarding is intentional. [Facade](https://aipatterns.dev/design-patterns/facade.md), [Proxy](https://aipatterns.dev/design-patterns/proxy.md), [Adapter](https://aipatterns.dev/design-patterns/adapter.md) and [Decorator](https://aipatterns.dev/design-patterns/decorator.md) are wrappers by design. Do not remove them as middle men. - Some separation of data and behavior is intentional. [Strategy](https://aipatterns.dev/design-patterns/strategy.md) and [Visitor](https://aipatterns.dev/design-patterns/visitor.md) put logic outside the data on purpose. - Check for import cycles before and after moving members between modules. --- # Simplifying Method Calls > Techniques that clean up method signatures, names, parameters and error signaling so interfaces are hard to call incorrectly. - kind: category - category: refactoring - canonical: https://aipatterns.dev/refactoring/techniques/simplifying-method-calls - inspired-by: https://refactoring.guru/refactoring/techniques/simplifying-method-calls ## What these techniques solve A method's signature is its contract with every caller. Unclear names, long or redundant parameter lists, methods that both return a value and change state, setters on objects that should be fixed, and error codes returned as numbers all invite misuse of that contract. These techniques change the interface rather than the body. **Signature changes touch every caller. Find them all first, and keep the old signature as a forwarding wrapper until the last one is migrated if the method is public.** ## Members - [Rename Method](https://aipatterns.dev/refactoring/techniques/rename-method.md): give a method a name that says what it does. - [Add Parameter](https://aipatterns.dev/refactoring/techniques/add-parameter.md): pass in data the method now needs instead of reaching for it some other way. - [Remove Parameter](https://aipatterns.dev/refactoring/techniques/remove-parameter.md): drop a parameter the body no longer uses. - [Separate Query from Modifier](https://aipatterns.dev/refactoring/techniques/separate-query-from-modifier.md): split a method that returns a value and changes state into two methods. - [Parameterize Method](https://aipatterns.dev/refactoring/techniques/parameterize-method.md): merge near-identical methods that differ only in a value into one method with a parameter. - [Replace Parameter with Explicit Methods](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-explicit-methods.md): split a method that switches on a parameter into one method per case. - [Preserve Whole Object](https://aipatterns.dev/refactoring/techniques/preserve-whole-object.md): pass the object instead of several values pulled out of it. - [Replace Parameter with Method Call](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-method-call.md): let the method get a value itself instead of every caller computing and passing it. - [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md): group parameters that always travel together into one object. - [Remove Setting Method](https://aipatterns.dev/refactoring/techniques/remove-setting-method.md): delete a setter for a field that should only be set at construction. - [Hide Method](https://aipatterns.dev/refactoring/techniques/hide-method.md): make a method private or internal when no outside code needs it. - [Replace Constructor with Factory Method](https://aipatterns.dev/refactoring/techniques/replace-constructor-with-factory-method.md): create objects through a named function that can choose a subtype or reuse instances. - [Replace Error Code with Exception](https://aipatterns.dev/refactoring/techniques/replace-error-code-with-exception.md): signal failure with an exception rather than a special return value. - [Replace Exception with Test](https://aipatterns.dev/refactoring/techniques/replace-exception-with-test.md): check a condition up front instead of catching an exception for an expected case. ## How to choose within this category - **The name lies or is vague:** [Rename Method](https://aipatterns.dev/refactoring/techniques/rename-method.md). Do this first; it is the cheapest, highest-value change here. Use your IDE or language server rename so all callers update. - **Too many parameters:** if they come from one object, [Preserve Whole Object](https://aipatterns.dev/refactoring/techniques/preserve-whole-object.md). If they always travel together, [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md). If the method could compute one itself, [Replace Parameter with Method Call](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-method-call.md). - **Near-duplicate methods vs a flag parameter:** these are inverses. Merge with [Parameterize Method](https://aipatterns.dev/refactoring/techniques/parameterize-method.md) when the parameter is a value (a rate, a limit). Split with [Replace Parameter with Explicit Methods](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-explicit-methods.md) when it is a mode or boolean that picks different behavior. - **A getter has side effects:** [Separate Query from Modifier](https://aipatterns.dev/refactoring/techniques/separate-query-from-modifier.md). - **An object should be immutable after creation:** [Remove Setting Method](https://aipatterns.dev/refactoring/techniques/remove-setting-method.md). Records, `readonly` fields and frozen dataclasses make this a declaration. - **The public surface is larger than its use:** [Hide Method](https://aipatterns.dev/refactoring/techniques/hide-method.md), and [Remove Parameter](https://aipatterns.dev/refactoring/techniques/remove-parameter.md) for unused inputs. Check reflection, serialization and other packages before shrinking a public API. - **Constructors can't express intent or choose a subtype:** [Replace Constructor with Factory Method](https://aipatterns.dev/refactoring/techniques/replace-constructor-with-factory-method.md). - **Error signaling:** follow your language's convention. Use [Replace Error Code with Exception](https://aipatterns.dev/refactoring/techniques/replace-error-code-with-exception.md) in exception-based languages. In Go and Rust, return `error` or `Result` instead; a numeric code is still the smell, but an exception is not the cure. Use [Replace Exception with Test](https://aipatterns.dev/refactoring/techniques/replace-exception-with-test.md) when the "exceptional" case is routine and cheap to check. --- # Dealing with Generalization > Techniques that move members up and down class hierarchies, create or collapse hierarchies, and swap inheritance for delegation. - kind: category - category: refactoring - canonical: https://aipatterns.dev/refactoring/techniques/dealing-with-generalization - inspired-by: https://refactoring.guru/refactoring/techniques/dealing-with-generalization ## What these techniques solve Inheritance hierarchies drift. Sibling classes copy the same method, a base class carries fields only one subclass uses, a subclass inherits a contract it can't honor, or a hierarchy has outlived the variation that justified it. These techniques reshape hierarchies, and replace inheritance with composition when the "is-a" relationship no longer holds. **Put shared behavior at the lowest level that all of its users share, and prefer delegation whenever a subclass would ignore part of what it inherits.** ## Members - [Pull Up Field](https://aipatterns.dev/refactoring/techniques/pull-up-field.md): move a field that all subclasses declare into the superclass. - [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md): move identical methods from subclasses into the superclass. - [Pull Up Constructor Body](https://aipatterns.dev/refactoring/techniques/pull-up-constructor-body.md): move shared constructor logic into a superclass constructor. - [Push Down Method](https://aipatterns.dev/refactoring/techniques/push-down-method.md): move a superclass method used by only some subclasses into those subclasses. - [Push Down Field](https://aipatterns.dev/refactoring/techniques/push-down-field.md): move a superclass field used by only some subclasses into those subclasses. - [Extract Subclass](https://aipatterns.dev/refactoring/techniques/extract-subclass.md): move features used only in some instances into a new subclass. - [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md): create a shared parent for two classes with common fields and methods. - [Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md): declare the subset of methods that several clients rely on as an interface. - [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md): merge a subclass and superclass that are no longer different enough to keep apart. - [Form Template Method](https://aipatterns.dev/refactoring/techniques/form-template-method.md): move a shared algorithm skeleton up and leave only the differing steps in subclasses. - [Replace Inheritance with Delegation](https://aipatterns.dev/refactoring/techniques/replace-inheritance-with-delegation.md): hold the former superclass in a field and forward the calls you need. - [Replace Delegation with Inheritance](https://aipatterns.dev/refactoring/techniques/replace-delegation-with-inheritance.md): subclass the delegate when a class forwards nearly its whole interface. ## How to choose within this category - **Siblings duplicate members:** pull them up with [Pull Up Field](https://aipatterns.dev/refactoring/techniques/pull-up-field.md), [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md) and [Pull Up Constructor Body](https://aipatterns.dev/refactoring/techniques/pull-up-constructor-body.md). If the methods are similar but not identical, use [Form Template Method](https://aipatterns.dev/refactoring/techniques/form-template-method.md), or pass the differing step as a function. - **The base class holds things only some subclasses use:** [Push Down Method](https://aipatterns.dev/refactoring/techniques/push-down-method.md) and [Push Down Field](https://aipatterns.dev/refactoring/techniques/push-down-field.md). - **Two unrelated classes share behavior:** consider [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md) only if one really is a kind of the other. Otherwise extract a shared helper class and delegate to it. If clients need only a shared role, [Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md). In Go, TypeScript and Python (with `Protocol`), structural typing means the interface can live with the client, and the classes don't need to declare it. - **Some instances use features others don't:** [Extract Subclass](https://aipatterns.dev/refactoring/techniques/extract-subclass.md) if the variation is fixed at creation. If it changes at runtime, use [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md) instead. - **A subclass adds nothing meaningful:** [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md). - **A subclass uses a small part of its parent or overrides methods to throw:** [Replace Inheritance with Delegation](https://aipatterns.dev/refactoring/techniques/replace-inheritance-with-delegation.md). This is the usual fix for [Refused Bequest](https://aipatterns.dev/refactoring/smells/refused-bequest.md). - **A class forwards almost every method of a delegate it truly is a kind of:** [Replace Delegation with Inheritance](https://aipatterns.dev/refactoring/techniques/replace-delegation-with-inheritance.md). Check that the full inherited contract is acceptable first; when in doubt, keep delegation. Go has no inheritance, so only the interface and delegation techniques apply there. Moving members along a hierarchy can change which override runs, so run the tests for every subclass after each move. --- # Other Smells > Smells that do not fit the main groups, currently the gap left when a library class you cannot edit lacks an operation you need. - kind: category - category: refactoring - canonical: https://aipatterns.dev/refactoring/smells/other - inspired-by: https://refactoring.guru/refactoring/smells/other ## What these smells share This group holds smells that cross the boundaries of the main categories. The current member concerns code you do not own. You cannot refactor a third-party or generated class directly, so the usual moves, such as adding a method where the data lives, are not available. The smell is in how your code works around that limit. ## Members - [Incomplete Library Class](https://aipatterns.dev/refactoring/smells/incomplete-library-class.md): a library type lacks a method you need, and workarounds for it are duplicated across your code. ## How to choose within this category - If the type you want to change is yours, this is not the right category. Look at [Feature Envy](https://aipatterns.dev/refactoring/smells/feature-envy.md) or [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) instead. - If the type is from a library and you need one or two extra operations, treat it as [Incomplete Library Class](https://aipatterns.dev/refactoring/smells/incomplete-library-class.md) with [Introduce Foreign Method](https://aipatterns.dev/refactoring/techniques/introduce-foreign-method.md). - If you need many extra operations on the same library type, use [Introduce Local Extension](https://aipatterns.dev/refactoring/techniques/introduce-local-extension.md), or an [Adapter](https://aipatterns.dev/design-patterns/adapter.md) when you also want to hide the library's interface. ## For agents - Check whether an installed dependency or a newer version of the library already provides the operation before writing one. - Use the language's extension mechanism where one exists: C# extension methods, Kotlin extension functions, Swift extensions, Rust traits. - Never patch the library's prototype or vendored source to add the method. --- # Strangler Fig > Put an interception layer in front of a legacy system and move routes to the replacement one at a time, until the old system has nothing left to serve and can be removed. - kind: pattern - category: architecture - aliases: Strangler Application, Incremental Migration - canonical: https://aipatterns.dev/design-patterns/strangler-fig - inspired-by: https://learn.microsoft.com/en-us/azure/architecture/patterns/strangler-fig ## Intent Replace a legacy system incrementally instead of in one rewrite. An interception layer - a routing facade - sits in front of the legacy system and forwards each request either to it or to the replacement. Migration moves one route at a time: build the new handler, flip the route, watch it, move on. Like the fig that grows around a host tree until the host is gone, the new system grows around the old until nothing remains to strangle. ## Forces - **Rewrite risk** vs **legacy drag**: a big-bang rewrite bets the whole system on one cutover; incremental migration bets one route at a time. - **Two systems** vs **one truth**: during migration both systems serve traffic and may hold data - every shared concept needs an owner and a sync story. - **Completion** vs **eternal coexistence**: without a per-route migration plan and an end state, the strangler becomes permanent infrastructure and the legacy system never dies. - **New freedom** vs **old constraints**: the replacement can redesign freely only where the facade absorbs the difference; leaked legacy shapes become the new system's architecture. ## Therefore **Route all traffic through one interception layer, replace handlers one route at a time, sync or migrate the data each route needs, and delete the legacy handler when its last route moves - with the route table as the migration's public progress bar.** The facade is the whole trick: it makes the cutover a routing change instead of a deployment, and it makes rollback a routing change back. Each moved route must also settle its data - either the new system owns the data for that route from the flip, or reads from the old store until it does not ([Transactional Outbox](https://aipatterns.dev/design-patterns/transactional-outbox.md) carries changes across while both stores live). ## Structure ```mermaid flowchart TD A[Incoming request] --> B[Routing facade] B -- /orders, /users --> C[New handlers] B -- /reports, /billing --> D[Legacy system] C --> E[(New store)] D --> F[(Legacy store)] B -. migrate one route at a time .-> B ``` ### Participants - **Routing facade** (`Gateway`): intercepts every request and maps routes to handlers. Knows nothing about either system's internals - only the route table. - **Legacy handler** (`LegacySystem`): the old system, untouched behind the facade. Serves exactly the routes still pointed at it. - **New handlers** (`NewOrders`, `NewUsers`): the replacement's routes, deployed into the same surface as the facade - each owns its data from its flip. - **Route table** (`routes`): the migration state. Public, reviewable, reversible per entry. ## Use it when - A legacy system must be replaced but cannot stop serving traffic - the rewrite happens beside it, not instead of it. - The system is big enough that a big-bang cutover is untestable, and routes can be migrated independently. - You can intercept the legacy system's entry points - a fronting proxy, a shared gateway, or the legacy app's own routing table. - Risk must be reversible per unit of work: each route flip is a small, watchable, revertible change. ## Do not use it when - The system is small enough to rewrite and cut over in one change - the facade is migration scaffolding, and scaffolding for a shed is a shed. - The legacy system's interface cannot be intercepted (no common entry point, clients baked to internal APIs). Fix interception first or the strangler has nothing to grow around. - Data is so entangled that no route's data can be owned cleanly - every flip corrupts something. Untangle the data model first; the routing is the easy part. - There is no migration endpoint: no team owns finishing it. A strangler without an owner becomes the permanent double system everyone maintains. ## Application grill **Inspect first:** the legacy system's entry points (routes, endpoints, consumers) where interception goes; the routing layer already in front (reverse proxy, load balancer, platform rewrites, gateway); how data is shared while both systems run (one writer or dual writes); the replacement's deploy target per extracted route. **Missing pieces:** an interception layer you control: reuse the reverse proxy or platform router already in front - adding a new proxy layer for strangling is rarely justified. Per-route or per-percentage cutover control comes from feature flags or routing configuration; a flag service earns its place when cutovers must be reversible by non-developers. Each extracted route needs the smallest deploy target that serves it (a serverless function). **Ask before you build:** - **Scale:** what is the cutover unit - per route, per header, per traffic percentage - and does the proxy support splitting? - **Performance:** do proxied routes pay an extra hop during the strangling period, and is that acceptable for months? - **Reliability:** what is the rollback story per route - one flag, or a redeploy? - **Budget:** two systems run in parallel for the duration - is the double-run cost (infrastructure and on-call) planned? - **Implementation:** while both systems run, who writes the data of record - one writer with the other reading, or a sync job? ## Implementation steps 1. Put the facade in front of everything first, forwarding every route to the legacy system, and prove it serves production traffic unchanged. The flip lever must exist before the first flip. 2. Inventory routes and rank them: start with one that is valuable, low-traffic, and data-light, so the first migration exercises the machinery on the cheapest possible mistake. 3. Build the new handler for that route and decide its data ownership at the flip: either it owns the data from day one, or it reads legacy and you sync with an outbox until it owns them. 4. Flip the route in the facade, watch its error and latency metrics against the legacy baseline, and keep the rollback as a one-line revert of the route entry. 5. Handle the stragglers deliberately: deep links, API clients, scheduled jobs that hit old paths. Every alias gets its own flip or a redirect with an expiry. 6. Track the route table as the migration's state - counts of legacy-served routes trend to zero, and the trend is the plan. When no route remains, delete the legacy system, not the facade. 7. Keep the facade thin forever: routing and, at most, request shaping. Business logic in the facade is the legacy system growing back. ## Example A monolith's reporting and billing routes still serve; orders and users have been migrated. The facade's table is the state; flipping a route is one entry. The demo migrates `/reports` live and shows the legacy handler's route count dropping toward zero. ```ts title="gateway.ts" type Handler = (path: string) => string; type Entry = { handler: Handler; owner: "legacy" | "new" }; class LegacySystem { readonly routes = ["/orders", "/users", "/reports", "/billing"]; serve(path: string): string { return `legacy handles ${path}`; } } const newOrders = (path: string) => `new orders handler: ${path}`; const newUsers = (path: string) => `new users handler: ${path}`; // The facade: every request enters here; the table is the migration state. class Gateway { private table = new Map(); constructor(legacy: LegacySystem) { for (const route of legacy.routes) this.table.set(route, { handler: (p) => legacy.serve(p), owner: "legacy" }); } /** One entry flip is one migration step - and its own rollback. */ migrate(route: string, handler: Handler): void { if (!this.table.has(route)) throw new Error(`unknown route ${route}`); this.table.set(route, { handler, owner: "new" }); } handle(path: string): string { const entry = this.table.get(path); return entry ? entry.handler(path) : "404"; } get legacyRoutesLeft(): string[] { return [...this.table.entries()] .filter(([, e]) => e.owner === "legacy") .map(([route]) => route); } } const gateway = new Gateway(new LegacySystem()); gateway.migrate("/orders", newOrders); gateway.migrate("/users", newUsers); console.log(gateway.handle("/orders")); // new orders handler: /orders console.log(gateway.handle("/reports")); // legacy handles /reports console.log(gateway.legacyRoutesLeft); // ["/reports", "/billing"] gateway.migrate("/reports", (p) => `new reports handler: ${p}`); console.log(gateway.handle("/reports")); // new reports handler: /reports console.log(gateway.legacyRoutesLeft); // ["/billing"] ``` ```python title="gateway.py" from collections.abc import Callable Handler = Callable[[str], str] class LegacySystem: def __init__(self) -> None: self.routes = ["/orders", "/users", "/reports", "/billing"] def serve(self, path: str) -> str: return f"legacy handles {path}" def new_orders(path: str) -> str: return f"new orders handler: {path}" def new_users(path: str) -> str: return f"new users handler: {path}" class Gateway: """Every request enters here; the table is the migration state.""" def __init__(self, legacy: LegacySystem) -> None: self._legacy = legacy self._table: dict[str, Handler] = { route: legacy.serve for route in legacy.routes } def migrate(self, route: str, handler: Handler) -> None: """One entry flip is one migration step - and its own rollback.""" if route not in self._table: raise ValueError(f"unknown route {route}") self._table[route] = handler def handle(self, path: str) -> str: handler = self._table.get(path) return handler(path) if handler else "404" @property def legacy_routes_left(self) -> list[str]: return [r for r, h in self._table.items() if h == self._legacy.serve] legacy = LegacySystem() gateway = Gateway(legacy) gateway.migrate("/orders", new_orders) gateway.migrate("/users", new_users) print(gateway.handle("/orders")) # new orders handler: /orders print(gateway.handle("/reports")) # legacy handles /reports print(f"legacy routes left: {gateway.legacy_routes_left}") # ['/reports', '/billing'] gateway.migrate("/reports", lambda p: f"new reports handler: {p}") print(gateway.handle("/reports")) # new reports handler: /reports print(f"legacy routes left: {gateway.legacy_routes_left}") # ['/billing'] ``` ```go title="gateway.go" package main import "fmt" type Handler func(path string) string type LegacySystem struct { routes []string } func (l *LegacySystem) serve(path string) string { return "legacy handles " + path } func newOrders(path string) string { return "new orders handler: " + path } func newUsers(path string) string { return "new users handler: " + path } // Gateway: every request enters here; the table is the migration state. type Gateway struct { legacy *LegacySystem table map[string]Handler } func NewGateway(legacy *LegacySystem) *Gateway { g := &Gateway{legacy: legacy, table: map[string]Handler{}} for _, route := range legacy.routes { g.table[route] = legacy.serve } return g } // Migrate flips one entry - one migration step, and its own rollback. func (g *Gateway) Migrate(route string, handler Handler) { if _, ok := g.table[route]; !ok { panic(fmt.Sprintf("unknown route %s", route)) } g.table[route] = handler } func (g *Gateway) Handle(path string) string { handler, ok := g.table[path] if !ok { return "404" } return handler(path) } func (g *Gateway) LegacyRoutesLeft() []string { var left []string for route, handler := range g.table { isLegacy := fmt.Sprintf("%p", handler) == fmt.Sprintf("%p", g.legacy.serve) if isLegacy { left = append(left, route) } } return left } func main() { legacy := &LegacySystem{routes: []string{"/orders", "/users", "/reports", "/billing"}} gateway := NewGateway(legacy) gateway.Migrate("/orders", newOrders) gateway.Migrate("/users", newUsers) fmt.Println(gateway.Handle("/orders")) // new orders handler: /orders fmt.Println(gateway.Handle("/reports")) // legacy handles /reports gateway.Migrate("/reports", func(p string) string { return "new reports handler: " + p }) fmt.Println(gateway.Handle("/reports")) // new reports handler: /reports fmt.Printf("legacy routes left: %d\n", len(gateway.LegacyRoutesLeft())) // 1 } ``` ```java title="Gateway.java" import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; public class Gateway { interface Handler { String serve(String path); } static final class LegacySystem { final List routes = new ArrayList<>(List.of("/orders", "/users", "/reports", "/billing")); String serve(String path) { return "legacy handles " + path; } } record NewOrders() implements Handler { public String serve(String path) { return "new orders handler: " + path; } } record NewUsers() implements Handler { public String serve(String path) { return "new users handler: " + path; } } private final LegacySystem legacy; // Every request enters here; the table is the migration state. private final Map table = new LinkedHashMap<>(); Gateway(LegacySystem legacy) { this.legacy = legacy; for (String route : legacy.routes) table.put(route, legacy::serve); } /** One entry flip is one migration step - and its own rollback. */ void migrate(String route, Handler handler) { if (!table.containsKey(route)) throw new IllegalArgumentException("unknown route " + route); table.put(route, handler); } String handle(String path) { Handler handler = table.get(path); return handler == null ? "404" : handler.serve(path); } List legacyRoutesLeft() { List left = new ArrayList<>(); for (Map.Entry e : table.entrySet()) { if (e.getValue() instanceof LegacyServing) left.add(e.getKey()); } return left; } /** Marks handlers that still route to the legacy system. */ private interface LegacyServing {} static void main(String[] args) { LegacySystem legacy = new LegacySystem(); Gateway gateway = new Gateway(legacy); // Route the legacy entries through the marker interface. for (String route : legacy.routes) gateway.table.put(route, marking(legacy::serve)); gateway.migrate("/orders", new NewOrders()); gateway.migrate("/users", new NewUsers()); System.out.println(gateway.handle("/orders")); // new orders handler: /orders System.out.println(gateway.handle("/reports")); // legacy handles /reports System.out.println("legacy routes left: " + gateway.legacyRoutesLeft().size()); // 2 gateway.migrate("/reports", p -> "new reports handler: " + p); System.out.println(gateway.handle("/reports")); // new reports handler: /reports } private static Gateway.Handler marking(Gateway.Handler inner) { return new LegacyServing() { public String serve(String path) { return inner.serve(path); } }; } } ``` The Go example leans on pointer identity of the method value; in production the route table carries an explicit owner field (`"legacy" | "new"`) instead of identity tricks - the demo compresses that to keep the participants visible. ## Trade-offs **Gains** - Risk arrives in route-sized slices, each independently watchable and revertible. - The legacy system keeps serving while value ships - no freeze, no big-bang cutover date. - The route table makes migration progress public and honest: count the legacy entries. **Costs** - Two systems and two stores run simultaneously; data ownership per route is a real design task, not a checkbox. - The facade is permanent infrastructure that must stay thin - it is also the one component that can become the new monolith. - Stragglers (deep links, API clients, jobs) extend the tail; the last 10% of routes takes as long as the first 80%. - The migration needs an owner with authority to delete the legacy system, or it never finishes. ## Relations - [Facade](https://aipatterns.dev/design-patterns/facade.md): the interception layer is a facade in front of two systems - but unlike a simplifying facade it must stay thin, because its job is routing, not comprehension. - [Proxy](https://aipatterns.dev/design-patterns/proxy.md): the facade's production position; in practice it is a reverse proxy or gateway, and per-route control is the feature that matters. - [Adapter](https://aipatterns.dev/design-patterns/adapter.md): when a new handler must serve a legacy-shaped request during transition, an adapter at the facade absorbs the interface difference. - [Transactional Outbox](https://aipatterns.dev/design-patterns/transactional-outbox.md): carries changes from the legacy store to the new one (or back) while a migrated route's data settles. ## Agent checklist - [ ] The facade serves all production traffic before any route flips. - [ ] Routes migrate smallest-first, and each flip has watched metrics and a one-line rollback. - [ ] Data ownership per route is decided at its flip - owner, or synced-until-owner, never both. - [ ] Stragglers (aliases, clients, jobs) have their own flips or expiring redirects. - [ ] The route table trends to zero legacy entries under a named owner. - [ ] The facade contains no business logic, only routing and request shaping. --- # Backends for Frontends > Give each frontend its own backend that aggregates and shapes the services it needs - so the web app, the mobile app, and any other client stop negotiating one API into serving them all. - kind: pattern - category: architecture - aliases: BFF - canonical: https://aipatterns.dev/design-patterns/backends-for-frontends - inspired-by: https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends ## Intent When several frontends consume one shared backend, every new client need changes an API everyone depends on. A backend for frontends (BFF) splits that backend per client: each frontend gets its own aggregation layer that calls the downstream services, composes them, and returns exactly the shape, payload, and protocol that client wants. The shared services stay business-owned; the per-client shape moves to a layer that only that client's team can change. ## Forces - **One API** vs **many clients**: web wants full objects, mobile wants compact payloads and few round trips, a kiosk wants three fields - one shape serves none of them well. - **Autonomy** vs **duplication**: each BFF is independently deployable and owned, and each re-implements some composition the others also do. - **Downstream purity** vs **client convenience**: shaping logic must live somewhere; in the services it multiplies per client, in the client it bloats frontends with backend knowledge. - **Team boundaries** vs **release coupling**: a shared backend couples every frontend's release to every other's; per-client backends decouple them at the price of more deployments. ## Therefore **Put one backend layer per frontend, between that client and the shared services: it calls the services, aggregates what the screen needs, and returns the client's exact shape - and nothing downstream knows any client exists.** The BFF is a [Facade](https://aipatterns.dev/design-patterns/facade.md) with an owner: one per client, not one for the world. It may cache, trim, join, and re-protocol (REST to the web, gRPC to mobile), but it holds no business rules - invariants stay in the services. Where two BFFs keep re-implementing the same composition, that composition belongs in a service. ## Structure ```mermaid classDiagram class WebBff { +productPage(id) WebProductView } class MobileBff { +productSummary(id) MobileProductView } class CatalogService { +product(id) Product } class PricingService { +price(id) Price } class ReviewService { +reviews(id) Review[] } WebBff --> CatalogService WebBff --> PricingService WebBff --> ReviewService MobileBff --> CatalogService MobileBff --> PricingService ``` ### Participants - **Client BFFs** (`WebBff`, `MobileBff`): one per frontend. Aggregate the services, shape the payload, own that client's caching and protocol. No invariants. - **Shared services** (`CatalogService`, `PricingService`, `ReviewService`): the business. Knows no client, returns domain data, enforces the rules. - **Views** (`WebProductView`, `MobileProductView`): the per-client shapes - full page data for web, a compact summary for mobile. ## Use it when - Two or more frontends with materially different needs consume the same services, and the shared API has become the meeting point of every roadmap. - Frontend teams deploy independently and a shared backend's release train is the bottleneck. - Clients have different transport or latency constraints - mobile needs one round trip and small payloads; desktop tolerates chatty calls for freshness. - A specific client needs aggressive composition - many small calls joined into one screen - that would otherwise leak into the services. ## Do not use it when - There is one frontend. A BFF for one client is a layer with no one to arbitrate between; it is just a slower way to write the API. - The clients' needs are genuinely identical - one well-designed API serves them; two BFFs would be copies that drift. - The team is too small to own the extra deployables - a BFF per client per team multiplies surfaces; a modular monolith endpoint per view achieves the shape without the fleet. - The difference is just caching or trimming - a CDN, query parameters, or a `fields` parameter is the fix, not a new backend. ## Application grill **Inspect first:** the clients sharing one API today and the shape-mangling each performs; the gateway or proxy layers already in front; the deploy targets the platform offers (serverless functions, containers, edge). **Missing pieces:** each backend needs its own deploy target. Serverless functions are the cheapest start and scale to zero; a long-lived service earns its keep only when a backend holds connections (WebSockets) or warm SDK clients. An API gateway (Kong, a cloud gateway) can serve simple aggregation without new code - check before writing any. **Ask before you build:** - **Scale:** what is each client's traffic shape, and must one client's spikes stay isolated from the others? - **Performance:** does each client really need one aggregated round trip, or would one new endpoint on the existing API do? - **Reliability:** when a downstream service fails, does the backend pass the failure through or serve a degraded view? - **Budget:** one client means one deploy, one on-call surface, one auth integration - is running N of those owned? - **Implementation:** who translates auth and sessions per client, and where does that logic live? ## Implementation steps 1. Split per client only where needs actually differ; start with the client whose pain is loudest and keep the shared API for the rest. 2. Put the BFF on the server, as close to the services as possible: it aggregates server-side so the client makes one call, and it can hold per-client caches. 3. Shape views for the screen, not the entities - `WebProductView` joins product, price, and reviews because the page needs all three, not because the domain does. 4. Keep business rules out. The BFF may default, trim, and reorder; it may not decide anything the services would disagree with. 5. Define each BFF's ownership: the frontend team owns theirs, the service teams own the services, and the contract between them is the service API - reviewed like any API. 6. Watch for composition patterns repeating across BFFs; the second time two BFFs write the same aggregation, promote it into a service. 7. Version per client: a BFF can change whenever its client ships, which is the point - never let one client's BFF grow endpoints for another. ## Example A product page for two clients. Web gets the full page in one call - product, price, and reviews joined; mobile gets a compact summary in one call, reviews omitted and price rounded. The services know no client. ```ts title="bff.ts" type Product = { id: string; name: string; description: string }; type Price = { cents: number }; type Review = { author: string; stars: number }; // Shared services: they know no client. const catalog = { product: (id: string): Product => ({ id, name: "Oolong", description: "oxidized 60%" }), }; const pricing = { price: (id: string): Price => ({ cents: 900 }) }; const reviews = { reviews: (id: string): Review[] => [{ author: "sam", stars: 5 }], }; // Web BFF: one call, full page shape. const webBff = { productPage(id: string) { const product = catalog.product(id); const price = pricing.price(id); const reviewList = reviews.reviews(id); return { ...product, priceCents: price.cents, reviewList, rating: 5 }; }, }; // Mobile BFF: one call, compact shape, no reviews on the list screen. const mobileBff = { productSummary(id: string) { const product = catalog.product(id); const price = pricing.price(id); return { id: product.id, name: product.name, priceCents: price.cents }; }, }; console.log(webBff.productPage("tea-1")); // { id, name, description, priceCents: 900, reviewList: [...], rating: 5 } console.log(mobileBff.productSummary("tea-1")); // { id: "tea-1", name: "Oolong", priceCents: 900 } ``` ```python title="bff.py" from dataclasses import dataclass @dataclass class Product: id: str name: str description: str @dataclass class Price: cents: int @dataclass class Review: author: str stars: int # Shared services: they know no client. class CatalogService: def product(self, id: str) -> Product: return Product(id, "Oolong", "oxidized 60%") class PricingService: def price(self, id: str) -> Price: return Price(900) class ReviewService: def reviews(self, id: str) -> list[Review]: return [Review("sam", 5)] class WebBff: """Web BFF: one call, full page shape.""" def __init__(self, catalog: CatalogService, pricing: PricingService, reviews: ReviewService): self._catalog, self._pricing, self._reviews = catalog, pricing, reviews def product_page(self, id: str) -> dict: product = self._catalog.product(id) price = self._pricing.price(id) review_list = self._reviews.reviews(id) return { **vars(product), "price_cents": price.cents, "reviews": [vars(r) for r in review_list], } class MobileBff: """Mobile BFF: one call, compact shape, no reviews on the list screen.""" def __init__(self, catalog: CatalogService, pricing: PricingService): self._catalog, self._pricing = catalog, pricing def product_summary(self, id: str) -> dict: product = self._catalog.product(id) price = self._pricing.price(id) return {"id": product.id, "name": product.name, "price_cents": price.cents} catalog, pricing, reviews = CatalogService(), PricingService(), ReviewService() web = WebBff(catalog, pricing, reviews) mobile = MobileBff(catalog, pricing) print(web.product_page("tea-1")) print(mobile.product_summary("tea-1")) # {'id': 'tea-1', 'name': 'Oolong', 'price_cents': 900} ``` ```go title="bff.go" package main import "fmt" type Product struct{ ID, Name, Description string } type Price struct{ Cents int } type Review struct{ Author string; Stars int } // Shared services: they know no client. type CatalogService struct{} type PricingService struct{} type ReviewService struct{} func (CatalogService) Product(id string) Product { return Product{ID: id, Name: "Oolong", Description: "oxidized 60%"} } func (PricingService) Price(id string) Price { return Price{Cents: 900} } func (ReviewService) Reviews(id string) []Review { return []Review{{"sam", 5}} } // WebBFF: one call, full page shape. type WebBFF struct { catalog CatalogService pricing PricingService reviews ReviewService } type webProductView struct { ID string Name string Description string PriceCents int Reviews []Review } func (b WebBFF) ProductPage(id string) webProductView { p := b.catalog.Product(id) return webProductView{p.ID, p.Name, p.Description, b.pricing.Price(id).Cents, b.reviews.Reviews(id)} } // MobileBFF: one call, compact shape, no reviews on the list screen. type MobileBFF struct { catalog CatalogService pricing PricingService } type mobileProductView struct { ID string Name string PriceCents int } func (b MobileBFF) ProductSummary(id string) mobileProductView { p := b.catalog.Product(id) return mobileProductView{p.ID, p.Name, b.pricing.Price(id).Cents} } func main() { web := WebBFF{CatalogService{}, PricingService{}, ReviewService{}} mobile := MobileBFF{CatalogService{}, PricingService{}} fmt.Printf("%+v\n", web.ProductPage("tea-1")) fmt.Printf("%+v\n", mobile.ProductSummary("tea-1")) } ``` ```java title="Bff.java" import java.util.List; public class Bff { record Product(String id, String name, String description) {} record Price(int cents) {} record Review(String author, int stars) {} // Shared services: they know no client. static final class CatalogService { Product product(String id) { return new Product(id, "Oolong", "oxidized 60%"); } } static final class PricingService { Price price(String id) { return new Price(900); } } static final class ReviewService { List reviews(String id) { return List.of(new Review("sam", 5)); } } /** Web BFF: one call, full page shape. */ record WebBff(CatalogService catalog, PricingService pricing, ReviewService reviews) { Map productPage(String id) { Product p = catalog.product(id); return Map.of( "id", p.id(), "name", p.name(), "description", p.description(), "priceCents", pricing.price(id).cents(), "reviews", reviews.reviews(id)); } } /** Mobile BFF: one call, compact shape, no reviews on the list screen. */ record MobileBff(CatalogService catalog, PricingService pricing) { Map productSummary(String id) { Product p = catalog.product(id); return Map.of("id", p.id(), "name", p.name(), "priceCents", pricing.price(id).cents()); } } public static void main(String[] args) { var web = new WebBff(new CatalogService(), new PricingService(), new ReviewService()); var mobile = new MobileBff(new CatalogService(), new PricingService()); System.out.println(web.productPage("tea-1")); System.out.println(mobile.productSummary("tea-1")); } } ``` When mobile later needs reviews on its detail screen, only `MobileBff` changes - the web BFF, the services, and every other client stay untouched, which is the autonomy the pattern is buying. ## Trade-offs **Gains** - Each client's API evolves on the client's release cadence, owned by the client's team. - Clients make fewer calls; aggregation happens server-side, close to the services. - Services stop accumulating per-client flags and shapes and stay domain-shaped. **Costs** - Some aggregation logic is written twice; the second copy is the signal to promote it into a service. - More deployables, more ownership boundaries, more things to monitor. - Business rules can leak into a BFF silently - "just this once" becomes a second source of truth. - For one client or identical clients, the pattern is pure overhead. ## Relations - [Facade](https://aipatterns.dev/design-patterns/facade.md): each BFF is a facade over the services - but with a single, known, owned consumer, which is what keeps it honest. - [Gateway Aggregation](https://learn.microsoft.com/en-us/azure/architecture/patterns/gateway-aggregation): the same join-many-calls move without the per-client split; reach for it when one aggregation serves everyone. - [Adapter](https://aipatterns.dev/design-patterns/adapter.md): the shape translation each BFF performs - service interfaces in, client view out. ## Agent checklist - [ ] One BFF per client whose needs actually differ; shared API remains for the rest. - [ ] The BFF holds no business rules - defaults and shapes only, invariants stay in services. - [ ] Views are shaped for the screen, not mirrored from entities. - [ ] The same aggregation appearing in two BFFs is promoted into a service, not copied again. - [ ] Ownership of each BFF is named, and its contract with the services is reviewed like any API. --- # Chain of Responsibility > Pass a request along a chain of handlers until one decides it, so the sender never knows which handler acts or how many exist. - kind: pattern - category: behavioral - aliases: CoR, Chain of Command - canonical: https://aipatterns.dev/design-patterns/chain-of-responsibility - inspired-by: https://refactoring.guru/design-patterns/chain-of-responsibility ## Intent Give several objects a chance to handle a request by linking them into a chain and passing the request along it. Each handler either decides the request or forwards it to the next one. The sender talks to the head of the chain and never learns who answered. ## Forces - **One entry point for the sender** vs **many rules that each own part of the decision**: callers want one call, but the logic belongs to separate checks. - **Rule order matters** vs **rules should not know each other**: a duplicate check must run before auto-approval, yet neither should reference the other. - **Different chains per context** vs **one hard-coded `if/else` ladder**: retail and marketplace orders need different rule sets, assembled at startup. - **Work around the decision** vs **work inside it**: logging, timing or auditing must wrap whatever the later handlers decide. ## Therefore **Give every handler the same interface and a reference to the next handler, and let each one either return a result or forward the request.** The client builds the chain once, usually at the composition root, and calls the first handler. A handler that cannot decide calls `next`. A handler can also run code before and after forwarding, which is what makes the linked form different from a plain list of rules. When no handler decides, the end of the chain returns a defined fallback instead of failing silently. ## Structure ```mermaid classDiagram class RefundHandler { <> +handle(req) Decision } class Step { <> -RefundHandler next +handle(req) Decision } class AuditTrail { +handle(req) Decision } class DuplicateGuard { +handle(req) Decision } class FraudHold { +handle(req) Decision } class AutoApprove { -Money limit +handle(req) Decision } RefundHandler <|.. Step Step o-- RefundHandler : next Step <|-- AuditTrail Step <|-- DuplicateGuard Step <|-- FraudHold Step <|-- AutoApprove ``` ### Participants - **Handler** (`RefundHandler`): the one method every link implements. It takes the request and returns the outcome. - **Base handler** (`Step`): optional. It stores `next` and supplies the forwarding and end-of-chain fallback, so concrete handlers don't repeat it. - **Concrete handlers** (`DuplicateGuard`, `FraudHold`, `AutoApprove`, `AuditTrail`): each checks one condition, decides or forwards, and knows nothing about its neighbors. - **Client**: assembles the chain in the right order and sends requests to its head. ## Use it when - A request goes through a sequence of checks where any one of them may settle it: refund triage, support ticket routing, input validation with early rejection. - The set or order of checks differs by tenant, plan, region or environment, and you want to assemble it from configuration. - Some handlers must wrap the rest of the chain: audit logging, timing, retries, or transactions around whatever decides. - A long `if/else` ladder in one function keeps growing, and each branch is an independent business rule with its own tests. ## Do not use it when - Every handler always runs in order and none can stop the flow. That is a pipeline: use an array of functions and a loop, or your framework's middleware stack (Express, Koa, ASP.NET, Go `http.Handler` wrapping). Most web frameworks already give you this pattern; don't rebuild it. - The rules are pure predicates that map to an outcome and nothing wraps the call. A list of `(condition, decision)` pairs and a `find` is shorter and easier to read than linked objects. - There are two or three fixed checks that never change order or vary by context. Keep the `if` statements. - Exactly one receiver is known at compile time. Call it directly. - Several receivers must *all* react to the event. That is [Observer](https://aipatterns.dev/design-patterns/observer.md), not a chain. ## Application grill **Inspect first:** the framework middleware already in place (Express, Koa, Next, Fastify middleware, interceptors, guards) - the codebase may already run this pattern under another name; how handlers decide today to act or pass along. **Missing pieces:** usually none: the framework's middleware chain is the pattern, and a plain array of functions covers short chains. A pipeline library pays only when users configure chains at runtime. **Ask before you build:** - **Scale:** how many handlers will the chain hold - three steps is an array, fifteen is a framework decision? - **Performance:** does every request traverse every handler even when the first would decide? - **Reliability:** when no handler claims the request, what answers - a terminal default handler or an error? - **Budget:** handler order is invisible at call sites; who owns keeping it correct? - **Implementation:** is the chain order fixed in code or configurable, and is there a test per decision boundary? ## Implementation steps 1. List the rules in the current conditional and confirm each one can either decide or defer on its own. 2. Define the handler interface: one method that takes the request and returns a decision. Keep the request immutable so handlers can't change it behind each other's backs. 3. Add a base handler (or a helper function) that holds `next` and returns a defined fallback when the chain ends. 4. Move each rule into its own handler. Run the tests after each move. 5. Build the chain at the composition root, in one place, so the order is visible and reviewable. 6. Replace the original conditional with one call to the head of the chain. ## Example A store triages refund requests. Duplicates are rejected, flagged customers are held for review, small refunds are approved automatically, and everything else goes to a manager. Marketplace orders use a stricter chain with no auto-approval. Every decision is audited. ```ts title="refunds.ts" interface RefundRequest { orderId: string; amountCents: number; customerFlagged?: boolean; alreadyRefunded?: boolean; } type Decision = { outcome: "approved" | "rejected" | "held" | "escalated"; reason: string }; interface RefundHandler { handle(req: RefundRequest): Decision; } abstract class Step implements RefundHandler { constructor(private readonly next?: RefundHandler) {} abstract handle(req: RefundRequest): Decision; protected passOn(req: RefundRequest): Decision { if (!this.next) return { outcome: "escalated", reason: "no rule matched; send to a manager" }; return this.next.handle(req); } } class DuplicateGuard extends Step { handle(req: RefundRequest): Decision { return req.alreadyRefunded ? { outcome: "rejected", reason: "order already refunded" } : this.passOn(req); } } class FraudHold extends Step { handle(req: RefundRequest): Decision { return req.customerFlagged ? { outcome: "held", reason: "customer under review" } : this.passOn(req); } } class AutoApprove extends Step { constructor(private readonly limitCents: number, next?: RefundHandler) { super(next); } handle(req: RefundRequest): Decision { return req.amountCents <= this.limitCents ? { outcome: "approved", reason: "under auto limit" } : this.passOn(req); } } // Wraps the rest of the chain: it forwards first, then records the result. class AuditTrail extends Step { handle(req: RefundRequest): Decision { const decision = this.passOn(req); console.log(`[audit] ${req.orderId} -> ${decision.outcome}: ${decision.reason}`); return decision; } } // Order is set here, once. Each handler only knows "next". const retail = new AuditTrail(new DuplicateGuard(new FraudHold(new AutoApprove(5000)))); const marketplace = new AuditTrail(new DuplicateGuard(new FraudHold())); retail.handle({ orderId: "A-100", amountCents: 2500 }); // approved retail.handle({ orderId: "A-101", amountCents: 2500, customerFlagged: true }); // held retail.handle({ orderId: "A-102", amountCents: 9900 }); // escalated retail.handle({ orderId: "A-103", amountCents: 900, alreadyRefunded: true }); // rejected marketplace.handle({ orderId: "M-200", amountCents: 2500 }); // escalated ``` ```python title="refunds.py" from __future__ import annotations from dataclasses import dataclass from typing import Optional @dataclass(frozen=True) class RefundRequest: order_id: str amount_cents: int customer_flagged: bool = False already_refunded: bool = False @dataclass(frozen=True) class Decision: outcome: str # approved | rejected | held | escalated reason: str class Step: def __init__(self, next_step: Optional[Step] = None) -> None: self._next = next_step def handle(self, req: RefundRequest) -> Decision: return self.pass_on(req) def pass_on(self, req: RefundRequest) -> Decision: if self._next is None: return Decision("escalated", "no rule matched; send to a manager") return self._next.handle(req) class DuplicateGuard(Step): def handle(self, req: RefundRequest) -> Decision: if req.already_refunded: return Decision("rejected", "order already refunded") return self.pass_on(req) class FraudHold(Step): def handle(self, req: RefundRequest) -> Decision: if req.customer_flagged: return Decision("held", "customer under review") return self.pass_on(req) class AutoApprove(Step): def __init__(self, limit_cents: int, next_step: Optional[Step] = None) -> None: super().__init__(next_step) self.limit_cents = limit_cents def handle(self, req: RefundRequest) -> Decision: if req.amount_cents <= self.limit_cents: return Decision("approved", "under auto limit") return self.pass_on(req) class AuditTrail(Step): def handle(self, req: RefundRequest) -> Decision: decision = self.pass_on(req) # forward first, then record print(f"[audit] {req.order_id} -> {decision.outcome}: {decision.reason}") return decision if __name__ == "__main__": retail = AuditTrail(DuplicateGuard(FraudHold(AutoApprove(5_000)))) marketplace = AuditTrail(DuplicateGuard(FraudHold())) # sellers never auto-approve retail.handle(RefundRequest("A-100", 2_500)) # approved retail.handle(RefundRequest("A-101", 2_500, customer_flagged=True)) # held retail.handle(RefundRequest("A-102", 9_900)) # escalated retail.handle(RefundRequest("A-103", 900, already_refunded=True)) # rejected marketplace.handle(RefundRequest("M-200", 2_500)) # escalated ``` ```go title="refunds.go" package main import "fmt" type RefundRequest struct { OrderID string AmountCents int CustomerFlagged bool AlreadyRefunded bool } type Decision struct{ Outcome, Reason string } type Handler interface { Handle(r RefundRequest) Decision } // passOn forwards to next, or escalates when the chain runs out. func passOn(next Handler, r RefundRequest) Decision { if next == nil { return Decision{"escalated", "no rule matched; send to a manager"} } return next.Handle(r) } type DuplicateGuard struct{ Next Handler } func (h DuplicateGuard) Handle(r RefundRequest) Decision { if r.AlreadyRefunded { return Decision{"rejected", "order already refunded"} } return passOn(h.Next, r) } type FraudHold struct{ Next Handler } func (h FraudHold) Handle(r RefundRequest) Decision { if r.CustomerFlagged { return Decision{"held", "customer under review"} } return passOn(h.Next, r) } type AutoApprove struct { LimitCents int Next Handler } func (h AutoApprove) Handle(r RefundRequest) Decision { if r.AmountCents <= h.LimitCents { return Decision{"approved", "under auto limit"} } return passOn(h.Next, r) } // AuditTrail wraps the rest of the chain and records its result. type AuditTrail struct{ Next Handler } func (h AuditTrail) Handle(r RefundRequest) Decision { d := passOn(h.Next, r) fmt.Printf("[audit] %s -> %s: %s\n", r.OrderID, d.Outcome, d.Reason) return d } func main() { retail := AuditTrail{Next: DuplicateGuard{Next: FraudHold{Next: AutoApprove{LimitCents: 5000}}}} marketplace := AuditTrail{Next: DuplicateGuard{Next: FraudHold{}}} retail.Handle(RefundRequest{OrderID: "A-100", AmountCents: 2500}) // approved retail.Handle(RefundRequest{OrderID: "A-101", AmountCents: 2500, CustomerFlagged: true}) // held retail.Handle(RefundRequest{OrderID: "A-102", AmountCents: 9900}) // escalated retail.Handle(RefundRequest{OrderID: "A-103", AmountCents: 900, AlreadyRefunded: true}) // rejected marketplace.Handle(RefundRequest{OrderID: "M-200", AmountCents: 2500}) // escalated } ``` ```java title="RefundTriage.java" public class RefundTriage { record RefundRequest(String orderId, long amountCents, boolean customerFlagged, boolean alreadyRefunded) {} record Decision(String outcome, String reason) {} interface RefundHandler { Decision handle(RefundRequest req); } abstract static class Step implements RefundHandler { private final RefundHandler next; Step(RefundHandler next) { this.next = next; } protected Decision passOn(RefundRequest req) { if (next == null) return new Decision("escalated", "no rule matched; send to a manager"); return next.handle(req); } } static final class DuplicateGuard extends Step { DuplicateGuard(RefundHandler next) { super(next); } public Decision handle(RefundRequest req) { return req.alreadyRefunded() ? new Decision("rejected", "order already refunded") : passOn(req); } } static final class FraudHold extends Step { FraudHold(RefundHandler next) { super(next); } public Decision handle(RefundRequest req) { return req.customerFlagged() ? new Decision("held", "customer under review") : passOn(req); } } static final class AutoApprove extends Step { private final long limitCents; AutoApprove(long limitCents, RefundHandler next) { super(next); this.limitCents = limitCents; } public Decision handle(RefundRequest req) { return req.amountCents() <= limitCents ? new Decision("approved", "under auto limit") : passOn(req); } } static final class AuditTrail extends Step { AuditTrail(RefundHandler next) { super(next); } public Decision handle(RefundRequest req) { Decision d = passOn(req); // forward first, then record System.out.printf("[audit] %s -> %s: %s%n", req.orderId(), d.outcome(), d.reason()); return d; } } public static void main(String[] args) { RefundHandler retail = new AuditTrail(new DuplicateGuard(new FraudHold(new AutoApprove(5000, null)))); RefundHandler marketplace = new AuditTrail(new DuplicateGuard(new FraudHold(null))); retail.handle(new RefundRequest("A-100", 2500, false, false)); // approved retail.handle(new RefundRequest("A-101", 2500, true, false)); // held retail.handle(new RefundRequest("A-102", 9900, false, false)); // escalated retail.handle(new RefundRequest("A-103", 900, false, true)); // rejected marketplace.handle(new RefundRequest("M-200", 2500, false, false)); // escalated } } ``` ## Trade-offs **Gains** - The sender is decoupled from the receivers. Adding, removing or reordering a rule touches only the chain assembly. - Each rule is a small class or function you can test with a stub `next`. - Different contexts get different chains from the same parts. - Handlers can wrap the rest of the chain for auditing, timing or error handling. **Costs** - A request can fall off the end unhandled. You need an explicit fallback, and tests for it. - Order dependencies are real but implicit: moving `AutoApprove` above `FraudHold` is a silent bug. - Debugging means stepping through every link to find who decided. - For pure predicate rules, it is more code than a list and a loop. ## Relations - [Decorator](https://aipatterns.dev/design-patterns/decorator.md) has the same linked shape, but every decorator always forwards and adds behavior. A chain handler may stop the request. - [Command](https://aipatterns.dev/design-patterns/command.md) can be the request object that travels down the chain, which makes requests loggable and replayable. - [Composite](https://aipatterns.dev/design-patterns/composite.md) trees often use a chain upward: a UI element that can't handle a click passes it to its parent. - [Mediator](https://aipatterns.dev/design-patterns/mediator.md) centralizes the routing decision in one object. A chain distributes it across handlers. - [Observer](https://aipatterns.dev/design-patterns/observer.md) delivers an event to every subscriber. A chain delivers it until one handler takes it. ## Agent checklist - [ ] At least one handler can stop the request or wraps the rest of the chain. Otherwise use a function array or the framework's middleware. - [ ] The chain is assembled in one visible place, and the order is covered by a test. - [ ] The end of the chain returns a defined fallback (escalate, reject, or a typed "unhandled"), never `undefined` by accident. - [ ] The request object is immutable, or the handlers that may change it are documented. - [ ] Each handler has its own test with a stub `next`, and the original conditional's behavior is preserved, verified by running the existing tests. --- # Command > Wrap an action and its arguments in an object so you can queue, log, retry or undo it separately from the code that triggers it. - kind: pattern - category: behavioral - aliases: Action, Transaction - canonical: https://aipatterns.dev/design-patterns/command - inspired-by: https://refactoring.guru/design-patterns/command ## Intent Turn a request into an object that carries everything needed to perform it later: the receiver, the arguments, and any state needed to reverse it. The code that triggers the action (a button, a scheduler, an API handler) then only holds a command and calls `execute`, without knowing what the action does. ## Forces - **Trigger now** vs **run later**: the user clicks now, but the work goes on a queue, waits for a retry, or runs in a batch. - **Doing an action** vs **remembering it**: undo, audit trails and replay need a record of what happened, not just its effect. - **Many triggers** vs **one behavior**: a menu item, a keyboard shortcut and an API call should all run the same operation. - **Thin callers** vs **rich operations**: the invoker should not know the receiver's API, but the operation needs it. ## Therefore **Package each operation as an object with an `execute` method (and `undo` when needed), and have callers store and run those objects instead of calling the receiver directly.** The command holds a reference to the receiver and the arguments. The invoker keeps commands in a history, a queue or a log. For undo, a command records what it needs to reverse itself at `execute` time. In languages with closures, a command without undo is a function value; a pair of closures `{ run, undo }` covers many undo cases too. Use classes when commands need names, serialization, or inspection. ## Structure ```mermaid classDiagram class StockLedger { -StockCommand[] history +run(cmd) void +undoLast() void } class StockCommand { <> +execute() void +undo() void } class Receive { -String sku -int qty +execute() void +undo() void } class Pick { -String sku -int qty +execute() void +undo() void } class CycleCount { -int before +execute() void +undo() void } class Inventory { +qty(sku) int +set(sku, n) void } StockLedger o-- StockCommand StockCommand <|.. Receive StockCommand <|.. Pick StockCommand <|.. CycleCount Receive --> Inventory Pick --> Inventory CycleCount --> Inventory ``` ### Participants - **Command** (`StockCommand`): the interface the invoker calls. Usually `execute`, plus `undo` and a label if the invoker needs them. - **Concrete commands** (`Receive`, `Pick`, `CycleCount`): bind a receiver to arguments, and capture the state needed for undo when they run. - **Receiver** (`Inventory`): the object that does the real work. It knows nothing about commands. - **Invoker** (`StockLedger`): runs commands and keeps the history, queue or log. It never looks inside a command. - **Client**: creates commands with their receiver and arguments and hands them to the invoker. ## Use it when - You need multi-level undo or redo for user actions. - Actions must be queued, scheduled, retried, or run on another worker, and you want them as data. - You need an audit log of what was done, in order, that can be replayed to rebuild state. - The same operation is triggered from several places (toolbar, shortcut, command palette, API) and should behave identically. - You want to group several operations into one macro or transaction that succeeds or rolls back together. ## Do not use it when - You only need to pass behavior to be called later, with no undo, queue, history or logging. Pass a closure or function reference (`onClick={() => save(doc)}`); a `SaveCommand` class adds nothing. - The "queue" is a background job system. Put plain serializable messages (`{ type: "ship", orderId }`) on the queue and map types to handlers. Command objects holding live receivers can't be serialized across processes. - Undo is the only goal and the state is small or immutable. Keep previous versions of the state, or use [Memento](https://aipatterns.dev/design-patterns/memento.md) snapshots, instead of writing an inverse for every operation. - The operation is a single call from a single place. Call the receiver directly. - The variation is *how* a result is computed, not *which* action runs. That is [Strategy](https://aipatterns.dev/design-patterns/strategy.md). ## Application grill **Inspect first:** the actions that need queueing, retry or undo, and whether a job system already runs (BullMQ, pg-boss, Celery, Sidekiq, SQS, platform queues); how command arguments would be serialized, since a queued command outlives the code version that queued it. **Missing pieces:** asynchronous execution needs a queue. Prefer what the platform already provides (Vercel Queues, Cloudflare Queues, SQS); on an existing Postgres, pg-boss or SKIP LOCKED polling adds no new service; Redis plus BullMQ is the step up when throughput needs it. An undo stack for a session is in-memory; persisted undo is a table with a retention policy. **Ask before you build:** - **Scale:** how many commands per second, and how large are their payloads (queues bill per message and cap payload size)? - **Performance:** how soon must a command run - immediately, or is seconds-later acceptable? - **Reliability:** on failure, what happens - retry, dead-letter - and is the handler safe to run twice (idempotent-consumer)? - **Budget:** managed queue versus Postgres-backed polling - whose operational budget decides? - **Implementation:** who owns argument schema stability across deploys, and how are in-flight commands migrated? ## Implementation steps 1. Define the command interface from what the invoker needs: `execute`, and `undo` or `label` only if they are used. 2. For each operation, create a command that takes the receiver and arguments in its constructor. 3. In `execute`, call the receiver. If the command supports undo, capture the prior state here, not in the constructor. 4. Make the invoker record a command only after `execute` succeeds, so a failed action never lands in the history. 5. Point every trigger (button, shortcut, API route) at the invoker instead of the receiver. 6. Test each command's `execute` followed by `undo` returns the receiver to its original state. ## Example A warehouse tool records stock movements. Clerks receive stock, pick it for orders, and correct counts after a cycle count. Every action is logged, a failed pick is refused without touching stock, and the last action can be undone. ```ts title="stock.ts" class Inventory { private stock = new Map(); qty(sku: string): number { return this.stock.get(sku) ?? 0; } set(sku: string, n: number): void { this.stock.set(sku, n); } } interface StockCommand { readonly label: string; execute(): void; undo(): void; } class Receive implements StockCommand { constructor(private inv: Inventory, private sku: string, private n: number) {} get label() { return `receive ${this.n} ${this.sku}`; } execute() { this.inv.set(this.sku, this.inv.qty(this.sku) + this.n); } undo() { this.inv.set(this.sku, this.inv.qty(this.sku) - this.n); } } class Pick implements StockCommand { constructor(private inv: Inventory, private sku: string, private n: number) {} get label() { return `pick ${this.n} ${this.sku}`; } execute() { const onHand = this.inv.qty(this.sku); if (onHand < this.n) throw new Error(`${this.label}: only ${onHand} on hand`); this.inv.set(this.sku, onHand - this.n); } undo() { this.inv.set(this.sku, this.inv.qty(this.sku) + this.n); } } // Not reversible by arithmetic, so it captures the old value when it runs. class CycleCount implements StockCommand { private before = 0; constructor(private inv: Inventory, private sku: string, private counted: number) {} get label() { return `count ${this.sku} = ${this.counted}`; } execute() { this.before = this.inv.qty(this.sku); this.inv.set(this.sku, this.counted); } undo() { this.inv.set(this.sku, this.before); } } class StockLedger { private history: StockCommand[] = []; readonly log: string[] = []; run(cmd: StockCommand): void { cmd.execute(); // throws before anything is recorded this.history.push(cmd); this.log.push(`do ${cmd.label}`); } undoLast(): void { const cmd = this.history.pop(); if (!cmd) return; cmd.undo(); this.log.push(`undo ${cmd.label}`); } } const inv = new Inventory(); const ledger = new StockLedger(); ledger.run(new Receive(inv, "SKU-7", 40)); ledger.run(new Pick(inv, "SKU-7", 12)); try { ledger.run(new Pick(inv, "SKU-7", 500)); } catch (e) { console.log("refused:", (e as Error).message); } ledger.run(new CycleCount(inv, "SKU-7", 25)); ledger.undoLast(); console.log(inv.qty("SKU-7")); // 28 console.log(ledger.log.join("\n")); ``` ```python title="stock.py" from typing import Protocol class Inventory: def __init__(self) -> None: self._stock: dict[str, int] = {} def qty(self, sku: str) -> int: return self._stock.get(sku, 0) def set(self, sku: str, n: int) -> None: self._stock[sku] = n class StockCommand(Protocol): label: str def execute(self) -> None: ... def undo(self) -> None: ... class Receive: def __init__(self, inv: Inventory, sku: str, n: int) -> None: self.inv, self.sku, self.n = inv, sku, n self.label = f"receive {n} {sku}" def execute(self) -> None: self.inv.set(self.sku, self.inv.qty(self.sku) + self.n) def undo(self) -> None: self.inv.set(self.sku, self.inv.qty(self.sku) - self.n) class Pick: def __init__(self, inv: Inventory, sku: str, n: int) -> None: self.inv, self.sku, self.n = inv, sku, n self.label = f"pick {n} {sku}" def execute(self) -> None: on_hand = self.inv.qty(self.sku) if on_hand < self.n: raise ValueError(f"{self.label}: only {on_hand} on hand") self.inv.set(self.sku, on_hand - self.n) def undo(self) -> None: self.inv.set(self.sku, self.inv.qty(self.sku) + self.n) class CycleCount: def __init__(self, inv: Inventory, sku: str, counted: int) -> None: self.inv, self.sku, self.counted = inv, sku, counted self.label = f"count {sku} = {counted}" self._before = 0 def execute(self) -> None: self._before = self.inv.qty(self.sku) # captured at run time, not construction self.inv.set(self.sku, self.counted) def undo(self) -> None: self.inv.set(self.sku, self._before) class StockLedger: def __init__(self) -> None: self._history: list[StockCommand] = [] self.log: list[str] = [] def run(self, cmd: StockCommand) -> None: cmd.execute() # raises before anything is recorded self._history.append(cmd) self.log.append(f"do {cmd.label}") def undo_last(self) -> None: if self._history: cmd = self._history.pop() cmd.undo() self.log.append(f"undo {cmd.label}") if __name__ == "__main__": inv, ledger = Inventory(), StockLedger() ledger.run(Receive(inv, "SKU-7", 40)) ledger.run(Pick(inv, "SKU-7", 12)) try: ledger.run(Pick(inv, "SKU-7", 500)) except ValueError as e: print("refused:", e) ledger.run(CycleCount(inv, "SKU-7", 25)) ledger.undo_last() print(inv.qty("SKU-7")) # 28 print("\n".join(ledger.log)) ``` ```go title="stock.go" package main import "fmt" type Inventory struct{ stock map[string]int } func NewInventory() *Inventory { return &Inventory{stock: map[string]int{}} } func (i *Inventory) Qty(sku string) int { return i.stock[sku] } func (i *Inventory) Set(sku string, n int) { i.stock[sku] = n } type StockCommand interface { Execute() error Undo() Label() string } type Receive struct { Inv *Inventory SKU string N int } func (c *Receive) Execute() error { c.Inv.Set(c.SKU, c.Inv.Qty(c.SKU)+c.N); return nil } func (c *Receive) Undo() { c.Inv.Set(c.SKU, c.Inv.Qty(c.SKU)-c.N) } func (c *Receive) Label() string { return fmt.Sprintf("receive %d %s", c.N, c.SKU) } type Pick Receive // same fields as Receive, its own methods func (c *Pick) Execute() error { onHand := c.Inv.Qty(c.SKU) if onHand < c.N { return fmt.Errorf("%s: only %d on hand", c.Label(), onHand) } c.Inv.Set(c.SKU, onHand-c.N) return nil } func (c *Pick) Undo() { c.Inv.Set(c.SKU, c.Inv.Qty(c.SKU)+c.N) } func (c *Pick) Label() string { return fmt.Sprintf("pick %d %s", c.N, c.SKU) } type CycleCount struct { Inv *Inventory SKU string Counted int before int // captured by Execute } func (c *CycleCount) Execute() error { c.before = c.Inv.Qty(c.SKU) c.Inv.Set(c.SKU, c.Counted) return nil } func (c *CycleCount) Undo() { c.Inv.Set(c.SKU, c.before) } func (c *CycleCount) Label() string { return fmt.Sprintf("count %s = %d", c.SKU, c.Counted) } type StockLedger struct { history []StockCommand Log []string } func (l *StockLedger) Run(c StockCommand) error { if err := c.Execute(); err != nil { return err // failed commands never enter the history } l.history = append(l.history, c) l.Log = append(l.Log, "do "+c.Label()) return nil } func (l *StockLedger) UndoLast() { if len(l.history) == 0 { return } c := l.history[len(l.history)-1] l.history = l.history[:len(l.history)-1] c.Undo() l.Log = append(l.Log, "undo "+c.Label()) } func main() { inv, ledger := NewInventory(), &StockLedger{} ledger.Run(&Receive{Inv: inv, SKU: "SKU-7", N: 40}) ledger.Run(&Pick{Inv: inv, SKU: "SKU-7", N: 12}) if err := ledger.Run(&Pick{Inv: inv, SKU: "SKU-7", N: 500}); err != nil { fmt.Println("refused:", err) } ledger.Run(&CycleCount{Inv: inv, SKU: "SKU-7", Counted: 25}) ledger.UndoLast() fmt.Println(inv.Qty("SKU-7")) // 28 fmt.Printf("%q\n", ledger.Log) } ``` ```java title="StockLedgerDemo.java" import java.util.*; public class StockLedgerDemo { static final class Inventory { private final Map stock = new HashMap<>(); int qty(String sku) { return stock.getOrDefault(sku, 0); } void set(String sku, int n) { stock.put(sku, n); } } interface StockCommand { String label(); void execute(); void undo(); } record Receive(Inventory inv, String sku, int n) implements StockCommand { public String label() { return "receive " + n + " " + sku; } public void execute() { inv.set(sku, inv.qty(sku) + n); } public void undo() { inv.set(sku, inv.qty(sku) - n); } } record Pick(Inventory inv, String sku, int n) implements StockCommand { public String label() { return "pick " + n + " " + sku; } public void execute() { int onHand = inv.qty(sku); if (onHand < n) throw new IllegalStateException(label() + ": only " + onHand + " on hand"); inv.set(sku, onHand - n); } public void undo() { inv.set(sku, inv.qty(sku) + n); } } // A class, not a record: it holds mutable state captured at execute time. static final class CycleCount implements StockCommand { private final Inventory inv; private final String sku; private final int counted; private int before; CycleCount(Inventory inv, String sku, int counted) { this.inv = inv; this.sku = sku; this.counted = counted; } public String label() { return "count " + sku + " = " + counted; } public void execute() { before = inv.qty(sku); inv.set(sku, counted); } public void undo() { inv.set(sku, before); } } static final class StockLedger { private final Deque history = new ArrayDeque<>(); final List log = new ArrayList<>(); void run(StockCommand cmd) { cmd.execute(); // throws before anything is recorded history.push(cmd); log.add("do " + cmd.label()); } void undoLast() { StockCommand cmd = history.poll(); if (cmd == null) return; cmd.undo(); log.add("undo " + cmd.label()); } } public static void main(String[] args) { Inventory inv = new Inventory(); StockLedger ledger = new StockLedger(); ledger.run(new Receive(inv, "SKU-7", 40)); ledger.run(new Pick(inv, "SKU-7", 12)); try { ledger.run(new Pick(inv, "SKU-7", 500)); } catch (IllegalStateException e) { System.out.println("refused: " + e.getMessage()); } ledger.run(new CycleCount(inv, "SKU-7", 25)); ledger.undoLast(); System.out.println(inv.qty("SKU-7")); // 28 ledger.log.forEach(System.out::println); } } ``` ## Trade-offs **Gains** - Undo, redo, queuing, retry and audit logging all fall out of one abstraction. - Triggers are decoupled from receivers: one command serves a button, a shortcut and an API route. - Commands compose into macros and transactions. - Each command is testable as an `execute` then `undo` round trip. **Costs** - One class per operation. A large app can accumulate hundreds of thin command types. - Undo is only as correct as each command's inverse, and inverses drift when the receiver changes. - Commands holding live receivers can't be persisted or sent across processes without a separate message format. - Undo history holds references to receivers and captured state, which can keep memory alive. ## Relations - [Memento](https://aipatterns.dev/design-patterns/memento.md) stores whole state snapshots. Command stores the delta. Combine them when an operation has no cheap inverse. - [Strategy](https://aipatterns.dev/design-patterns/strategy.md) also puts behavior in an object, but to choose *how* to compute something. Command chooses *what* to do and when. - [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) can pass command objects along its handlers. - [Mediator](https://aipatterns.dev/design-patterns/mediator.md) and [Observer](https://aipatterns.dev/design-patterns/observer.md) can dispatch commands between components without direct references. - [Prototype](https://aipatterns.dev/design-patterns/prototype.md) helps when a command must be copied into the history before it is run again. ## Agent checklist - [ ] There is a concrete need for undo, queuing, logging, retry or replay. Otherwise pass a function. - [ ] Commands capture undo state in `execute`, not in the constructor. - [ ] The invoker records a command only after it succeeds. - [ ] Every command has an `execute` then `undo` round-trip test that compares receiver state before and after. - [ ] If commands cross a process boundary, they are plain serializable data, not objects holding receivers. --- # Iterator > Walk the elements of a collection one at a time through a standard interface, without exposing how the collection stores or fetches them. - kind: pattern - category: behavioral - aliases: Cursor - canonical: https://aipatterns.dev/design-patterns/iterator - inspired-by: https://refactoring.guru/design-patterns/iterator ## Intent Give a collection a separate object that knows how to step through its elements and remembers where it is. Callers ask for the next element until there are none left, and never see whether the data lives in an array, a tree, a file, or a paginated remote API. ## Forces - **Simple loops for callers** vs **complex storage behind them**: a caller wants `for each invoice`, not cursor tokens and page sizes. - **Lazy access** vs **loading everything**: fetching ten thousand records to find the first overdue one wastes time and memory. - **Several traversals at once** vs **position stored in the collection**: if the collection holds the cursor, two loops over it corrupt each other. - **Hiding the representation** vs **letting callers stop early, filter, or map**: the iteration contract must stay small and still compose. ## Therefore **Move the traversal state (current position, buffer, cursor) out of the collection into an iterator object that implements the language's standard iteration protocol.** The collection exposes a factory method that returns a fresh iterator, so each loop gets its own position. Callers use the language's native loop (`for...of`, `for ... in`, `range`, enhanced `for`) and stop whenever they want. In most modern languages you should write the iterator as a generator function (`function*`, `yield`, `iter.Seq`). Write an explicit iterator class only when you need extra methods, explicit resource cleanup, or a language without generators. ## Structure ```mermaid classDiagram class IterableCollection { <> +iterator() Iterator } class Iterator { <> +next() Invoice +hasNext() bool } class Invoices { -InvoiceApi api +iterator() Iterator } class InvoiceIterator { -Invoice[] buffer -String cursor +next() Invoice +hasNext() bool } class InvoiceApi { +fetchPage(cursor) Page } IterableCollection <|.. Invoices Iterator <|.. InvoiceIterator Invoices ..> InvoiceIterator : creates InvoiceIterator --> InvoiceApi Invoices --> InvoiceApi ``` ### Participants - **Iterator** (`Iterator`): the language's protocol for "give me the next element, or tell me you're done". Use the built-in one instead of defining your own. - **Concrete iterator** (`InvoiceIterator`): holds the traversal state and fetches more data when its buffer runs dry. - **Iterable collection** (`IterableCollection`): the protocol that says "I can produce an iterator". - **Concrete collection** (`Invoices`): returns a new iterator on each call, so traversals stay independent. - **Client**: loops with the language's native syntax and can stop at any element. ## Use it when - The data comes from pages, cursors, streams, or files, and callers should see one flat sequence. - The collection has a non-trivial structure (tree, graph, sparse matrix, ring buffer) and callers should not depend on it. - You need several traversal orders over the same data, such as depth-first and breadth-first, newest-first and oldest-first. - Callers often stop early, and producing all elements up front is expensive. - Two pieces of code must traverse the same collection at the same time without interfering. ## Do not use it when - The data is already an array, list or map in memory. Return it, or return a read-only view, and let callers use the native loop. - The traversal is a linear walk where the only state is a cursor. Write a generator function (`function*` in TypeScript, `yield` in Python, `iter.Seq` in Go 1.23+, a `Stream` or `Spliterator` in Java); every modern language ships them, and they give the same laziness with a third of the code. A hand-written iterator class earns its place when iteration state outgrows a cursor - bookmarks, multiple simultaneous positions, or state callers must inspect and close. - Callers always need every element and the set is small. A function that returns a list is simpler to test and debug. - You are inventing a custom `hasNext`/`next` interface. Implement the language's protocol (`Symbol.iterator`, `__iter__`, `java.util.Iterator`) so the result works with every built-in loop and library. - The traversal needs to run an operation that differs per element type. That is [Visitor](https://aipatterns.dev/design-patterns/visitor.md), possibly fed by an iterator. ## Application grill **Inspect first:** the collections callers reach into directly (index access, `.data`, pagination params leaking into business code); the language's built-in iteration (generators, iterables, channels); how large result sets flow from the database today. **Missing pieces:** the language's built-ins are the pattern; nothing to install. For database results, keyset pagination or a driver cursor, never OFFSET on large tables. **Ask before you build:** - **Scale:** what result sizes must stream instead of materialize (a memory bound per request)? - **Performance:** does the walk hit the network per element or per page, and what page size fits? - **Reliability:** what does the iteration do when the source changes mid-walk (an invalidated cursor)? - **Budget:** unifying N ad-hoc loops into one iterator - who migrates the callers? - **Implementation:** is the iterator lazy (pull) or buffered, and do errors surface at first pull or at construction? ## Implementation steps 1. Write down what the caller loops over today, and where the paging, indexing or recursion leaks into the calling code. 2. Choose the language's standard iteration protocol. Prefer a generator; fall back to an iterator class if you need state you can inspect or close. 3. Put all traversal state (index, cursor token, buffer, stack) in the iterator, not in the collection. 4. Make the collection return a fresh iterator from its factory method on each call. 5. Fetch lazily: load the next page or node only when the buffer is empty. 6. Replace the caller's manual paging loop with the native loop, and add a test that stops early and checks how much data was fetched. ## Example A billing service exports invoices from a provider API that returns three invoices per page with a cursor to the next page. Callers loop over invoices without seeing pages, and stopping at the first overdue invoice fetches only the pages it needs. ```ts title="invoices.ts" interface Invoice { id: string; amountCents: number; overdue: boolean } interface Page { items: Invoice[]; next?: string } class InvoiceApi { calls = 0; constructor(private readonly all: Invoice[], private readonly pageSize: number) {} fetchPage(cursor: string): Page { this.calls++; const start = Number(cursor); const end = start + this.pageSize; return { items: this.all.slice(start, end), next: end < this.all.length ? String(end) : undefined }; } } class InvoiceIterator implements Iterator { private buffer: Invoice[] = []; private cursor: string | undefined = "0"; // undefined means no more pages constructor(private readonly api: InvoiceApi) {} next(): IteratorResult { while (this.buffer.length === 0 && this.cursor !== undefined) { const page = this.api.fetchPage(this.cursor); this.buffer = page.items; this.cursor = page.next; } const item = this.buffer.shift(); return item ? { done: false, value: item } : { done: true, value: undefined }; } } class Invoices implements Iterable { constructor(private readonly api: InvoiceApi) {} [Symbol.iterator](): Iterator { return new InvoiceIterator(this.api); // fresh state per loop } } // The same traversal as a generator. Prefer this form in real TypeScript code. function* invoices(api: InvoiceApi): Generator { for (let cursor: string | undefined = "0"; cursor !== undefined; ) { const page = api.fetchPage(cursor); yield* page.items; cursor = page.next; } } const data = Array.from({ length: 7 }, (_, i) => ({ id: `INV-${i + 1}`, amountCents: (i + 1) * 1000, overdue: i === 4, })); const api = new InvoiceApi(data, 3); for (const inv of new Invoices(api)) { if (inv.overdue) { console.log("first overdue:", inv.id); // INV-5 break; } } console.log("pages fetched:", api.calls); // 2 let total = 0; for (const inv of invoices(api)) total += inv.amountCents; console.log(total, "pages fetched:", api.calls); // 28000 pages fetched: 5 ``` ```python title="invoices.py" from collections import deque from dataclasses import dataclass from typing import Iterator, Optional @dataclass(frozen=True) class Invoice: id: str amount_cents: int overdue: bool = False @dataclass(frozen=True) class Page: items: list[Invoice] next_cursor: Optional[str] class InvoiceApi: def __init__(self, invoices: list[Invoice], page_size: int) -> None: self._all, self._page_size, self.calls = invoices, page_size, 0 def fetch_page(self, cursor: str) -> Page: self.calls += 1 start = int(cursor) end = start + self._page_size return Page(self._all[start:end], str(end) if end < len(self._all) else None) class InvoiceIterator: def __init__(self, api: InvoiceApi) -> None: self._api = api self._buffer: deque[Invoice] = deque() self._cursor: Optional[str] = "0" # None means no more pages def __iter__(self) -> "InvoiceIterator": return self def __next__(self) -> Invoice: while not self._buffer and self._cursor is not None: page = self._api.fetch_page(self._cursor) self._buffer.extend(page.items) self._cursor = page.next_cursor if not self._buffer: raise StopIteration return self._buffer.popleft() class Invoices: def __init__(self, api: InvoiceApi) -> None: self._api = api def __iter__(self) -> Iterator[Invoice]: return InvoiceIterator(self._api) # fresh state per loop def invoices(api: InvoiceApi) -> Iterator[Invoice]: """The same traversal as a generator. Prefer this form in real Python code.""" cursor: Optional[str] = "0" while cursor is not None: page = api.fetch_page(cursor) yield from page.items cursor = page.next_cursor if __name__ == "__main__": data = [Invoice(f"INV-{i}", i * 1000, overdue=(i == 5)) for i in range(1, 8)] api = InvoiceApi(data, page_size=3) first = next(inv for inv in Invoices(api) if inv.overdue) print("first overdue:", first.id) # INV-5 print("pages fetched:", api.calls) # 2 print(sum(inv.amount_cents for inv in invoices(api)), "pages fetched:", api.calls) # 28000 5 ``` ```go title="invoices.go" package main import ( "fmt" "iter" "strconv" ) type Invoice struct { ID string AmountCents int Overdue bool } type InvoiceAPI struct { all []Invoice pageSize int Calls int } func (a *InvoiceAPI) FetchPage(cursor string) (items []Invoice, next string) { a.Calls++ start, _ := strconv.Atoi(cursor) end := min(start+a.pageSize, len(a.all)) if end < len(a.all) { next = strconv.Itoa(end) // stays empty on the last page } return a.all[start:end], next } // InvoiceIterator follows the Next/Value shape of bufio.Scanner and sql.Rows. type InvoiceIterator struct { api *InvoiceAPI buf []Invoice cursor string // empty means no more pages cur Invoice } func (it *InvoiceIterator) Next() bool { for len(it.buf) == 0 && it.cursor != "" { it.buf, it.cursor = it.api.FetchPage(it.cursor) } if len(it.buf) == 0 { return false } it.cur, it.buf = it.buf[0], it.buf[1:] return true } func (it *InvoiceIterator) Value() Invoice { return it.cur } type Invoices struct{ api *InvoiceAPI } func (c Invoices) Iterator() *InvoiceIterator { return &InvoiceIterator{api: c.api, cursor: "0"} } // All adapts the iterator to range-over-func (Go 1.23+), the idiomatic form today. func (c Invoices) All() iter.Seq[Invoice] { return func(yield func(Invoice) bool) { for it := c.Iterator(); it.Next(); { if !yield(it.Value()) { return } } } } func main() { var data []Invoice for i := 1; i <= 7; i++ { data = append(data, Invoice{ID: fmt.Sprintf("INV-%d", i), AmountCents: i * 1000, Overdue: i == 5}) } api := &InvoiceAPI{all: data, pageSize: 3} invoices := Invoices{api: api} for inv := range invoices.All() { if inv.Overdue { fmt.Println("first overdue:", inv.ID) // INV-5 break } } fmt.Println("pages fetched:", api.Calls) // 2 total := 0 for it := invoices.Iterator(); it.Next(); { total += it.Value().AmountCents } fmt.Println(total, "pages fetched:", api.Calls) // 28000 pages fetched: 5 } ``` ```java title="InvoiceExport.java" import java.util.*; public class InvoiceExport { record Invoice(String id, long amountCents, boolean overdue) {} record Page(List items, String next) {} // next is null on the last page static final class InvoiceApi { private final List all; private final int pageSize; int calls; InvoiceApi(List all, int pageSize) { this.all = all; this.pageSize = pageSize; } Page fetchPage(String cursor) { calls++; int start = Integer.parseInt(cursor); int end = Math.min(start + pageSize, all.size()); return new Page(all.subList(start, end), end < all.size() ? String.valueOf(end) : null); } } static final class InvoiceIterator implements Iterator { private final InvoiceApi api; private final Deque buffer = new ArrayDeque<>(); private String cursor = "0"; // null means no more pages InvoiceIterator(InvoiceApi api) { this.api = api; } @Override public boolean hasNext() { while (buffer.isEmpty() && cursor != null) { Page page = api.fetchPage(cursor); buffer.addAll(page.items()); cursor = page.next(); } return !buffer.isEmpty(); } @Override public Invoice next() { if (!hasNext()) throw new NoSuchElementException(); return buffer.removeFirst(); } } record Invoices(InvoiceApi api) implements Iterable { public Iterator iterator() { return new InvoiceIterator(api); } // fresh state per loop } public static void main(String[] args) { List data = new ArrayList<>(); for (int i = 1; i <= 7; i++) data.add(new Invoice("INV-" + i, i * 1000L, i == 5)); InvoiceApi api = new InvoiceApi(data, 3); Invoices invoices = new Invoices(api); for (Invoice inv : invoices) { if (inv.overdue()) { System.out.println("first overdue: " + inv.id()); // INV-5 break; } } System.out.println("pages fetched: " + api.calls); // 2 long total = 0; for (Invoice inv : invoices) total += inv.amountCents(); System.out.println(total + " pages fetched: " + api.calls); // 28000 pages fetched: 5 } } ``` ## Trade-offs **Gains** - Callers use one native loop, whatever the storage or transport. - Lazy fetching: early exits load only what was read. - Each iterator has its own position, so traversals don't interfere. - New traversal orders are new iterators; the collection does not change. **Costs** - A hand-written iterator class is more code than a generator and easier to get wrong (idempotent `hasNext`, end-of-data handling). - Lazy iteration hides I/O inside a loop. Errors and latency show up far from the call that started them. - Iterating while the underlying collection changes is undefined unless you design for it. - For small in-memory collections, the indirection buys nothing. ## Relations - [Composite](https://aipatterns.dev/design-patterns/composite.md) trees are a common reason to write an iterator: depth-first or breadth-first over nested nodes. - [Visitor](https://aipatterns.dev/design-patterns/visitor.md) runs a type-specific operation on each element. An iterator supplies the elements. - [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) is how the collection's `iterator()` returns the right concrete iterator. - [Memento](https://aipatterns.dev/design-patterns/memento.md) can capture an iterator's position so a long export can resume after a crash. ## Agent checklist - [ ] You checked whether a generator or the language's built-in iteration covers the need before writing an iterator class. - [ ] The iterator implements the language's standard protocol, not a custom `hasNext`/`next` interface. - [ ] Traversal state lives in the iterator, and each call to the factory returns a fresh one. - [ ] Data is fetched lazily, and a test stops early and asserts how many pages or nodes were loaded. - [ ] End-of-data, empty collections and fetch errors are handled and tested. --- # Mediator > Route interactions between a group of components through one coordinator so they stop referencing each other and the rules live in one place. - kind: pattern - category: behavioral - aliases: Intermediary, Controller - canonical: https://aipatterns.dev/design-patterns/mediator - inspired-by: https://refactoring.guru/design-patterns/mediator ## Intent Stop a set of components from calling each other directly. Each component reports what happened to one mediator object, and the mediator decides which other components to update. The coordination rules move out of the components and into a single place you can read and test. ## Forces - **Components that must react to each other** vs **components you want to reuse alone**: a room picker that calls the price label can't be used on a page without one. - **Rules spread across peers** vs **rules you can read in one place**: "more guests filters rooms, and any change reprices" ends up split over four event handlers. - **N components** vs **N×N links**: every new component adds wiring to every existing one. - **One central coordinator** vs **that coordinator growing without bound**: the cure for tangled peers can become a god object. ## Therefore **Give the components a reference only to a mediator interface, have them report changes to it, and put every cross-component rule in the mediator.** Components (often called colleagues) stay simple: they hold their own state, expose methods to update it, and call `mediator.changed(event)` when the user acts. Methods the mediator calls on a component do not notify back, which avoids feedback loops. The mediator is specific to one workflow. When it starts to serve several unrelated workflows, split it. ## Structure ```mermaid classDiagram class PanelMediator { <> +changed(event) void } class BookingPanel { -Room[] catalog +changed(event) void } class NumberInput { -PanelMediator mediator -int value +set(value) void } class RoomSelect { -PanelMediator mediator -Room selected +choose(code) void +limitTo(rooms) void } class QuoteLabel { -String text +show(text) void } PanelMediator <|.. BookingPanel BookingPanel *-- NumberInput BookingPanel *-- RoomSelect BookingPanel *-- QuoteLabel NumberInput --> PanelMediator RoomSelect --> PanelMediator ``` ### Participants - **Mediator** (`PanelMediator`): the narrow interface components use to report events. Components never see more than this. - **Concrete mediator** (`BookingPanel`): owns the components and holds every rule about how they affect each other. - **Colleagues** (`NumberInput`, `RoomSelect`, `QuoteLabel`): hold their own state and notify the mediator. They do not reference each other. - **Client**: creates the mediator and talks to the colleagues through it, or lets user input drive them. ## Use it when - A group of UI widgets, services or devices has many-to-many update rules, and changing one rule means editing several components. - You want to reuse a component in another screen or flow, but it references its current neighbors directly. - The interaction rules are the hard part of the feature and need their own tests. - Adding a component today means touching every existing component. See [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md). ## Do not use it when - One component updates one other, in one direction. A direct call or a callback is clearer. - Components only need to announce events and listeners don't coordinate with each other. Use [Observer](https://aipatterns.dev/design-patterns/observer.md) or an event emitter. - The coordination is one-way data flow: one owner of state, widgets that only derive from it. Lift the state to a parent, or use a store (Redux, Pinia, a Svelte store) or a view model, and derive each widget's props from that state. A mediator earns its name only when peers talk back and the coordination has rules of its own. - The mediator would coordinate unrelated workflows. Several small mediators beat one god object that every feature edits. If yours passes a few hundred lines or has a `switch` over dozens of events, split it by workflow. - The "mediator" only forwards calls to a subsystem without coordinating peers. That is a [Facade](https://aipatterns.dev/design-patterns/facade.md). ## Application grill **Inspect first:** the component tangle the pattern would untie (who calls whom for one workflow); existing event mechanisms (an event emitter, signals, streams, a message broker) - a mediator over an existing bus may be configuration, not code. **Missing pieces:** in-process: the language's event emitter or a small coordinator object - nothing to buy. Cross-service: a broker (Redis pub/sub for transient fan-out, Kafka or SQS for durability and ordering), which brings retention and ordering semantics to decide - do not add one for a single coordinator. **Ask before you build:** - **Scale:** how many components does the mediator join - three is an object, fifteen is an architecture? - **Performance:** is it on the request path (synchronous) or the event path (asynchronous, with latency tolerance)? - **Reliability:** when one participant fails, who notices - what is the mediator's error policy? - **Budget:** is a broker justified for one interaction, or is in-process enough until a second service needs the events? - **Implementation:** are interactions typed (request and response through the mediator) or fire-and-forget? ## Implementation steps 1. List the components and draw every place one references another. Each arrow is a rule to move. 2. Define a mediator interface with one notification method, for example `changed(event)`. Keep it this narrow. 3. Replace each component's references to its peers with a reference to the mediator interface. 4. Move every cross-component rule into the concrete mediator. Methods the mediator calls on components must not notify back. 5. Have the mediator create or receive the components and set the initial state in one place. 6. Test the mediator's rules directly: change one component and assert the state of the others. ## Example A hotel booking panel has a guest count, a night count, a room picker and a price quote. More guests than a room holds removes that room from the picker and selects one that fits. Any change reprices the quote. None of the widgets knows the others exist. ```ts title="booking.ts" interface Room { code: string; capacity: number; nightlyCents: number } type PanelEvent = "guests" | "nights" | "room"; interface PanelMediator { changed(event: PanelEvent): void; } class NumberInput { constructor(private readonly mediator: PanelMediator, private readonly event: PanelEvent, public value: number) {} set(value: number): void { this.value = value; this.mediator.changed(this.event); } } class RoomSelect { options: Room[] = []; selected?: Room; constructor(private readonly mediator: PanelMediator) {} choose(code: string): void { this.selected = this.options.find((r) => r.code === code) ?? this.selected; this.mediator.changed("room"); } // Called by the mediator. It does not notify back, which prevents loops. limitTo(rooms: Room[]): void { this.options = rooms; if (!this.selected || !rooms.includes(this.selected)) this.selected = rooms[0]; } } class QuoteLabel { text = ""; show(text: string): void { this.text = text; } } class BookingPanel implements PanelMediator { readonly guests = new NumberInput(this, "guests", 2); readonly nights = new NumberInput(this, "nights", 1); readonly rooms = new RoomSelect(this); readonly quote = new QuoteLabel(); constructor(private readonly catalog: Room[]) { this.changed("guests"); } // Every cross-widget rule lives here. changed(event: PanelEvent): void { if (event === "guests") { this.rooms.limitTo(this.catalog.filter((r) => r.capacity >= this.guests.value)); } const room = this.rooms.selected; const nights = this.nights.value; this.quote.show(room ? `${room.code} x${nights}: $${((room.nightlyCents * nights) / 100).toFixed(2)}` : "No room fits this party"); } } const panel = new BookingPanel([ { code: "STD", capacity: 2, nightlyCents: 8900 }, { code: "DBL", capacity: 2, nightlyCents: 11900 }, { code: "FAM", capacity: 4, nightlyCents: 18900 }, ]); panel.rooms.choose("DBL"); panel.nights.set(3); console.log(panel.quote.text); // DBL x3: $357.00 panel.guests.set(4); console.log(panel.quote.text); // FAM x3: $567.00 panel.guests.set(6); console.log(panel.quote.text); // No room fits this party ``` ```python title="booking.py" from __future__ import annotations from dataclasses import dataclass from typing import Optional, Protocol @dataclass(frozen=True) class Room: code: str capacity: int nightly_cents: int class PanelMediator(Protocol): def changed(self, event: str) -> None: ... class NumberInput: def __init__(self, mediator: PanelMediator, event: str, value: int) -> None: self._mediator, self._event, self.value = mediator, event, value def set(self, value: int) -> None: self.value = value self._mediator.changed(self._event) class RoomSelect: def __init__(self, mediator: PanelMediator) -> None: self._mediator = mediator self.options: list[Room] = [] self.selected: Optional[Room] = None def choose(self, code: str) -> None: self.selected = next((r for r in self.options if r.code == code), self.selected) self._mediator.changed("room") def limit_to(self, rooms: list[Room]) -> None: """Called by the mediator. Does not notify back, which prevents loops.""" self.options = rooms if self.selected not in rooms: self.selected = rooms[0] if rooms else None class QuoteLabel: def __init__(self) -> None: self.text = "" def show(self, text: str) -> None: self.text = text class BookingPanel: def __init__(self, catalog: list[Room]) -> None: self._catalog = catalog self.guests = NumberInput(self, "guests", 2) self.nights = NumberInput(self, "nights", 1) self.rooms = RoomSelect(self) self.quote = QuoteLabel() self.changed("guests") def changed(self, event: str) -> None: if event == "guests": self.rooms.limit_to([r for r in self._catalog if r.capacity >= self.guests.value]) room, nights = self.rooms.selected, self.nights.value if room is None: self.quote.show("No room fits this party") else: self.quote.show(f"{room.code} x{nights}: ${room.nightly_cents * nights / 100:.2f}") if __name__ == "__main__": panel = BookingPanel([Room("STD", 2, 8900), Room("DBL", 2, 11900), Room("FAM", 4, 18900)]) panel.rooms.choose("DBL") panel.nights.set(3) print(panel.quote.text) # DBL x3: $357.00 panel.guests.set(4) print(panel.quote.text) # FAM x3: $567.00 panel.guests.set(6) print(panel.quote.text) # No room fits this party ``` ```go title="booking.go" package main import ( "fmt" "slices" ) type Room struct { Code string Capacity, NightlyCents int } type PanelMediator interface{ Changed(event string) } type NumberInput struct { mediator PanelMediator event string Value int } func (n *NumberInput) Set(v int) { n.Value = v; n.mediator.Changed(n.event) } type RoomSelect struct { mediator PanelMediator options []Room Selected *Room } func (s *RoomSelect) Choose(code string) { for i := range s.options { if s.options[i].Code == code { s.Selected = &s.options[i] } } s.mediator.Changed("room") } // LimitTo is called by the mediator. It does not notify back, which prevents loops. func (s *RoomSelect) LimitTo(rooms []Room) { keep := s.Selected s.options, s.Selected = rooms, nil for i := range rooms { // keep the current room if it still fits, else take the first if s.Selected == nil || (keep != nil && rooms[i].Code == keep.Code) { s.Selected = &rooms[i] } } } type QuoteLabel struct{ Text string } func (q *QuoteLabel) Show(text string) { q.Text = text } type BookingPanel struct { catalog []Room Guests, Nights *NumberInput Rooms *RoomSelect Quote *QuoteLabel } func NewBookingPanel(catalog []Room) *BookingPanel { p := &BookingPanel{catalog: catalog, Quote: &QuoteLabel{}} p.Guests = &NumberInput{mediator: p, event: "guests", Value: 2} p.Nights = &NumberInput{mediator: p, event: "nights", Value: 1} p.Rooms = &RoomSelect{mediator: p} p.Changed("guests") return p } func (p *BookingPanel) Changed(event string) { if event == "guests" { tooSmall := func(r Room) bool { return r.Capacity < p.Guests.Value } p.Rooms.LimitTo(slices.DeleteFunc(slices.Clone(p.catalog), tooSmall)) } quote := "No room fits this party" if r, n := p.Rooms.Selected, p.Nights.Value; r != nil { quote = fmt.Sprintf("%s x%d: $%.2f", r.Code, n, float64(r.NightlyCents*n)/100) } p.Quote.Show(quote) } func main() { panel := NewBookingPanel([]Room{{"STD", 2, 8900}, {"DBL", 2, 11900}, {"FAM", 4, 18900}}) panel.Rooms.Choose("DBL") panel.Nights.Set(3) fmt.Println(panel.Quote.Text) // DBL x3: $357.00 panel.Guests.Set(4) fmt.Println(panel.Quote.Text) // FAM x3: $567.00 panel.Guests.Set(6) fmt.Println(panel.Quote.Text) // No room fits this party } ``` ```java title="BookingPanelDemo.java" import java.util.List; public class BookingPanelDemo { record Room(String code, int capacity, long nightlyCents) {} interface PanelMediator { void changed(String event); } static final class NumberInput { private final PanelMediator mediator; private final String event; int value; NumberInput(PanelMediator mediator, String event, int value) { this.mediator = mediator; this.event = event; this.value = value; } void set(int value) { this.value = value; mediator.changed(event); } } static final class RoomSelect { private final PanelMediator mediator; List options = List.of(); Room selected; RoomSelect(PanelMediator mediator) { this.mediator = mediator; } void choose(String code) { options.stream().filter(r -> r.code().equals(code)).findFirst().ifPresent(r -> selected = r); mediator.changed("room"); } // Called by the mediator. It does not notify back, which prevents loops. void limitTo(List rooms) { options = rooms; if (selected == null || !rooms.contains(selected)) selected = rooms.isEmpty() ? null : rooms.get(0); } } static final class QuoteLabel { String text = ""; void show(String text) { this.text = text; } } static final class BookingPanel implements PanelMediator { private final List catalog; final NumberInput guests = new NumberInput(this, "guests", 2); final NumberInput nights = new NumberInput(this, "nights", 1); final RoomSelect rooms = new RoomSelect(this); final QuoteLabel quote = new QuoteLabel(); BookingPanel(List catalog) { this.catalog = catalog; changed("guests"); } public void changed(String event) { if (event.equals("guests")) { rooms.limitTo(catalog.stream().filter(r -> r.capacity() >= guests.value).toList()); } Room room = rooms.selected; int n = nights.value; quote.show(room == null ? "No room fits this party" : String.format("%s x%d: $%.2f", room.code(), n, room.nightlyCents() * n / 100.0)); } } public static void main(String[] args) { BookingPanel panel = new BookingPanel(List.of( new Room("STD", 2, 8900), new Room("DBL", 2, 11900), new Room("FAM", 4, 18900))); panel.rooms.choose("DBL"); panel.nights.set(3); System.out.println(panel.quote.text); // DBL x3: $357.00 panel.guests.set(4); System.out.println(panel.quote.text); // FAM x3: $567.00 panel.guests.set(6); System.out.println(panel.quote.text); // No room fits this party } } ``` ## Trade-offs **Gains** - Components are reusable: they depend on a one-method interface, not on their neighbors. - Interaction rules live in one class you can read top to bottom and test directly. - N×N links become N links to the mediator. - Adding a component touches the mediator only. **Costs** - The mediator tends to absorb every rule and becomes a [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md). - Control flow is indirect: to see what a click does, you read the mediator, not the widget. - Notifications that trigger notifications can loop unless the mediator's own calls are silent. - For two or three components, it is more structure than the problem has. ## Relations - [Observer](https://aipatterns.dev/design-patterns/observer.md) is often how colleagues notify the mediator. Observer broadcasts to anyone; Mediator encodes who should react and how. - [Facade](https://aipatterns.dev/design-patterns/facade.md) simplifies access to a subsystem from outside, and the subsystem doesn't know about it. A mediator is known to its colleagues and coordinates them. - [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) spreads a decision across handlers. Mediator concentrates it in one object. - [Command](https://aipatterns.dev/design-patterns/command.md) objects can be what colleagues send to the mediator, which makes the interactions loggable and undoable. ## Agent checklist - [ ] At least three components have cross-dependencies today. Otherwise keep direct calls or a callback. - [ ] If a framework store or parent component already owns the state, you derived props from it instead of adding a mediator. - [ ] Colleagues reference only the mediator interface, never each other or the concrete mediator. - [ ] Methods the mediator calls on colleagues do not notify back. - [ ] The mediator serves one workflow. Tests cover each rule by changing one colleague and asserting the others. --- # Memento > Capture an object's internal state in an opaque snapshot so it can be restored later without exposing that state to the code holding the snapshot. - kind: pattern - category: behavioral - aliases: Snapshot, Token - canonical: https://aipatterns.dev/design-patterns/memento - inspired-by: https://refactoring.guru/design-patterns/memento ## Intent Let an object save its own state into a snapshot object and later restore from it. Other code can store and pass the snapshot around, but can't read or change what is inside. Rollback and undo work without breaking the object's encapsulation. ## Forces - **Rolling back** vs **keeping fields private**: the code that decides to roll back should not need to know which fields to reset. - **Trying a change** vs **predicting its effect**: sometimes the only way to know whether a change is valid is to apply it and check. - **Complete snapshots** vs **memory cost**: copying the whole state is simple and correct, but expensive when state is large or snapshots are frequent. - **Shallow copies** vs **shared mutable parts**: a snapshot that shares a mutable list with the live object is not a snapshot. ## Therefore **Have the object (the originator) produce and consume its own snapshots, and give everyone else an opaque handle to hold.** The originator's `save()` copies its internal state into a memento, and `restore(m)` copies it back. A separate caretaker decides *when* to save and restore and keeps the mementos, but treats them as tokens. Each language enforces "opaque" differently: nested classes with private fields in Java, unexported fields in Go, a private lookup table or brand type in TypeScript, and naming convention in Python. ## Structure ```mermaid classDiagram class RoutePlan { -Stop[] stops -int shiftMin +add(stop) void +minutes() int +save() RouteSnapshot +restore(s) void } class RouteSnapshot { -Stop[] stops } class Dispatcher { +planDay(route, batches) void } class Stop { -String id -int driveMin -int serviceMin } RoutePlan *-- Stop RoutePlan ..> RouteSnapshot : creates Dispatcher ..> RouteSnapshot : holds Dispatcher --> RoutePlan ``` ### Participants - **Originator** (`RoutePlan`): the object whose state is saved. Only it can create a memento or read one. - **Memento** (`RouteSnapshot`): an immutable copy of the originator's state, opaque to everyone else. - **Caretaker** (`Dispatcher`): decides when to snapshot and when to roll back, and stores mementos without looking inside them. ## Use it when - You apply a change tentatively, check the result, and roll back on failure, and the change touches state the caller can't see. - You need undo for an object whose internals are private, and writing an inverse for every operation is impractical. - You need checkpoints in a long-running computation (a solver, an import, a simulation) to resume or backtrack. - Several fields must be restored together, and resetting them one by one from outside would leak the object's structure. ## Do not use it when - The state is already immutable. Keep a reference to the previous value; persistent data structures and reducer-style state (`state = reduce(state, action)`) give you snapshots for free. - You can validate before mutating. Check the change first and skip the snapshot. It is cheaper and simpler. - The state is a plain public record with no invariants to protect. Copy it (`structuredClone`, `copy.deepcopy`, a copy constructor); encapsulation was the only reason for the pattern. - State is large and changes are small and frequent. Store deltas with [Command](https://aipatterns.dev/design-patterns/command.md) and `undo` instead of full copies. - The snapshot must survive a restart or cross a process. That is serialization or an event log with a versioned format, not an in-memory memento. ## Application grill **Inspect first:** the state being snapshotted and its size; existing undo or history features (editors, forms) and their storage; whether snapshots hold personal data that crosses a trust boundary or retention rule. **Missing pieces:** in-memory: the language's value copies - nothing to install. Persisted: a table or blob store with a retention and deletion policy; snapshots of personal data must age out on the same schedule as the records they mirror. **Ask before you build:** - **Scale:** what is snapshot size times retention count - memory or storage bound? - **Performance:** snapshot on every change or per action, and is coalescing needed? - **Reliability:** under concurrent edits, what does restore mean - is the snapshot stale the moment it restores? - **Budget:** who pays the retention storage and runs the deletion pipeline? - **Implementation:** when the internal state shape changes, do old snapshots still restore (versioned snapshots), and opaque to whom? ## Implementation steps 1. List every field that has to come back on restore, including derived fields and caches, or recompute them after restore. 2. Create the memento type with those fields, immutable, and make it unreadable outside the originator. 3. Add `save()` to the originator. Copy mutable collections deeply enough that later changes don't leak into the snapshot. 4. Add `restore(memento)`. Copy again on the way in, so restoring twice from the same memento works. 5. Move the "when to save and roll back" logic into a caretaker that holds mementos as opaque values. 6. Test that `save`, then a change, then `restore` gives back an object equal to the original, and that the same memento can be restored twice. ## Example A dispatch tool builds a delivery van's route for the day. Stops arrive in batches that must be kept together (several drops for one customer). The dispatcher adds each batch, and if the route no longer fits the shift, rolls the route back to its state before that batch. ```ts title="route.ts" interface Stop { readonly id: string; readonly driveMin: number; readonly serviceMin: number; } // Opaque to callers: they can hold a snapshot but cannot read or build one. declare const brand: unique symbol; type RouteSnapshot = { readonly [brand]: true }; class RoutePlan { private static readonly saved = new WeakMap(); private stops: Stop[] = []; constructor(readonly shiftMin: number) {} add(stop: Stop): void { this.stops.push(stop); } minutes(): number { return this.stops.reduce((t, s) => t + s.driveMin + s.serviceMin, 0); } ids(): string[] { return this.stops.map((s) => s.id); } save(): RouteSnapshot { const snapshot = Object.freeze({}) as RouteSnapshot; RoutePlan.saved.set(snapshot, [...this.stops]); // stops are immutable, so a shallow copy is enough return snapshot; } restore(snapshot: RouteSnapshot): void { const stops = RoutePlan.saved.get(snapshot); if (!stops) throw new Error("snapshot was not created by RoutePlan"); this.stops = [...stops]; } } // Caretaker: decides when to roll back, never looks inside the snapshot. function planDay(route: RoutePlan, batches: Stop[][]): void { for (const batch of batches) { const checkpoint = route.save(); batch.forEach((s) => route.add(s)); if (route.minutes() > route.shiftMin) { route.restore(checkpoint); console.log("rejected:", batch.map((s) => s.id).join(",")); } } } const route = new RoutePlan(480); planDay(route, [ [{ id: "s1", driveMin: 60, serviceMin: 15 }, { id: "s2", driveMin: 40, serviceMin: 20 }], [{ id: "s3", driveMin: 90, serviceMin: 30 }, { id: "s4", driveMin: 50, serviceMin: 10 }], [{ id: "s5", driveMin: 120, serviceMin: 30 }, { id: "s6", driveMin: 45, serviceMin: 15 }], [{ id: "s7", driveMin: 30, serviceMin: 20 }], ]); console.log(route.ids(), route.minutes()); // [ 's1', 's2', 's3', 's4', 's7' ] 365 ``` ```python title="route.py" from dataclasses import dataclass @dataclass(frozen=True) class Stop: id: str drive_min: int service_min: int @dataclass(frozen=True) class RouteSnapshot: """Opaque by convention: only RoutePlan reads _stops.""" _stops: tuple[Stop, ...] class RoutePlan: def __init__(self, shift_min: int) -> None: self.shift_min = shift_min self._stops: list[Stop] = [] def add(self, stop: Stop) -> None: self._stops.append(stop) def minutes(self) -> int: return sum(s.drive_min + s.service_min for s in self._stops) def ids(self) -> list[str]: return [s.id for s in self._stops] def save(self) -> RouteSnapshot: return RouteSnapshot(tuple(self._stops)) # tuple: the snapshot can't be mutated def restore(self, snapshot: RouteSnapshot) -> None: self._stops = list(snapshot._stops) def plan_day(route: RoutePlan, batches: list[list[Stop]]) -> None: """Caretaker: decides when to roll back, never looks inside the snapshot.""" for batch in batches: checkpoint = route.save() for stop in batch: route.add(stop) if route.minutes() > route.shift_min: route.restore(checkpoint) print("rejected:", ",".join(s.id for s in batch)) if __name__ == "__main__": route = RoutePlan(shift_min=480) plan_day(route, [ [Stop("s1", 60, 15), Stop("s2", 40, 20)], [Stop("s3", 90, 30), Stop("s4", 50, 10)], [Stop("s5", 120, 30), Stop("s6", 45, 15)], [Stop("s7", 30, 20)], ]) print(route.ids(), route.minutes()) # ['s1', 's2', 's3', 's4', 's7'] 365 ``` ```go title="route.go" package main import ( "fmt" "slices" "strings" ) type Stop struct { ID string DriveMin, ServiceMin int } // RouteSnapshot has only unexported fields, so code outside this package // can hold one but cannot read or build it. type RouteSnapshot struct{ stops []Stop } type RoutePlan struct { ShiftMin int stops []Stop } func (r *RoutePlan) Add(s Stop) { r.stops = append(r.stops, s) } func (r *RoutePlan) Minutes() int { total := 0 for _, s := range r.stops { total += s.DriveMin + s.ServiceMin } return total } func (r *RoutePlan) IDs() []string { ids := make([]string, len(r.stops)) for i, s := range r.stops { ids[i] = s.ID } return ids } // Clone on both sides: append could otherwise write into the snapshot's backing array. func (r *RoutePlan) Save() RouteSnapshot { return RouteSnapshot{stops: slices.Clone(r.stops)} } func (r *RoutePlan) Restore(snap RouteSnapshot) { r.stops = slices.Clone(snap.stops) } // planDay is the caretaker: it decides when to roll back and never reads the snapshot. func planDay(route *RoutePlan, batches [][]Stop) { for _, batch := range batches { checkpoint := route.Save() for _, s := range batch { route.Add(s) } if route.Minutes() > route.ShiftMin { route.Restore(checkpoint) ids := make([]string, len(batch)) for i, s := range batch { ids[i] = s.ID } fmt.Println("rejected:", strings.Join(ids, ",")) } } } func main() { route := &RoutePlan{ShiftMin: 480} planDay(route, [][]Stop{ {{"s1", 60, 15}, {"s2", 40, 20}}, {{"s3", 90, 30}, {"s4", 50, 10}}, {{"s5", 120, 30}, {"s6", 45, 15}}, {{"s7", 30, 20}}, }) fmt.Println(route.IDs(), route.Minutes()) // [s1 s2 s3 s4 s7] 365 } ``` ```java title="Dispatch.java" import java.util.*; import java.util.stream.Collectors; public class Dispatch { record Stop(String id, int driveMin, int serviceMin) {} static final class RoutePlan { // Private constructor and field: only RoutePlan can build or read a Snapshot. static final class Snapshot { private final List stops; private Snapshot(List stops) { this.stops = List.copyOf(stops); } } final int shiftMin; private List stops = new ArrayList<>(); RoutePlan(int shiftMin) { this.shiftMin = shiftMin; } void add(Stop stop) { stops.add(stop); } int minutes() { return stops.stream().mapToInt(s -> s.driveMin() + s.serviceMin()).sum(); } List ids() { return stops.stream().map(Stop::id).toList(); } Snapshot save() { return new Snapshot(stops); } void restore(Snapshot snapshot) { stops = new ArrayList<>(snapshot.stops); } } // Caretaker: decides when to roll back, never looks inside the snapshot. static void planDay(RoutePlan route, List> batches) { for (List batch : batches) { RoutePlan.Snapshot checkpoint = route.save(); batch.forEach(route::add); if (route.minutes() > route.shiftMin) { route.restore(checkpoint); System.out.println("rejected: " + batch.stream().map(Stop::id).collect(Collectors.joining(","))); } } } public static void main(String[] args) { RoutePlan route = new RoutePlan(480); planDay(route, List.of( List.of(new Stop("s1", 60, 15), new Stop("s2", 40, 20)), List.of(new Stop("s3", 90, 30), new Stop("s4", 50, 10)), List.of(new Stop("s5", 120, 30), new Stop("s6", 45, 15)), List.of(new Stop("s7", 30, 20)))); System.out.println(route.ids() + " " + route.minutes()); // [s1, s2, s3, s4, s7] 365 } } ``` In Java, nested classes share private access within their top-level class, so `Dispatch` could technically read `Snapshot.stops`. In real code, put `RoutePlan` in its own file. ## Trade-offs **Gains** - Rollback and undo without exposing the originator's fields. - The caretaker's logic stays simple: save, try, restore. - One restore resets every related field together, so invariants hold. - Snapshots are immutable and safe to keep in a history. **Costs** - Each snapshot copies state. Large state or frequent snapshots cost memory and time. - Deep versus shallow copying is easy to get wrong, and the bug only shows on a later mutation. - Opaqueness is weak in some languages (Python convention, Go package scope). - Caretakers that keep every snapshot need a retention limit. ## Relations - [Command](https://aipatterns.dev/design-patterns/command.md) records the operation and its inverse. Memento records the state. Use a memento inside a command when the operation has no cheap inverse. - [Iterator](https://aipatterns.dev/design-patterns/iterator.md) positions can be saved as mementos so a long traversal can resume. - [Prototype](https://aipatterns.dev/design-patterns/prototype.md) clones the whole object. A memento copies only the state needed to restore it, and keeps it opaque. - [State](https://aipatterns.dev/design-patterns/state.md) objects can be stored in a memento when the current state is part of what must roll back. ## Agent checklist - [ ] You checked that validating before mutating, or keeping an immutable previous value, doesn't already solve the problem. - [ ] Every field that must come back is in the snapshot, or is recomputed after restore. - [ ] Mutable collections are copied on `save` and on `restore`, and a test mutates after saving to prove the snapshot is unaffected. - [ ] Caretakers hold mementos as opaque values, and the history has a size or age limit. - [ ] A round-trip test (save, change, restore) asserts the originator equals its original state. --- # Observer > Let an object announce its changes to a list of subscribers it does not know, so new reactions plug in without editing the source of the change. - kind: pattern - category: behavioral - aliases: Publish-Subscribe, Event Listener, Dependents - canonical: https://aipatterns.dev/design-patterns/observer - inspired-by: https://refactoring.guru/design-patterns/observer ## Intent Give an object (the *subject*) a list of subscribers and notify each of them when its state changes. The subject knows only that its subscribers implement one small interface. It does not know what they do with the news, how many there are, or who they are. ## Forces - **One change** vs **many reactions**: a stock level drops, and the storefront, the purchasing team and the audit log all care, each for its own reason. - **A stable core** vs **reactions that keep arriving**: every new reaction should not mean another line inside the method that made the change. - **Loose coupling** vs **traceable control flow**: removing direct calls also removes the call site a reader would follow. - **Long-lived subjects** vs **short-lived listeners**: a subject that holds references to its listeners keeps them alive until someone detaches them. - **Synchronous delivery** vs **isolation**: a slow or throwing listener runs inside the subject's call and can stall or break it. ## Therefore **Have the subject keep a list of objects that implement a notification interface, and call each of them after every relevant change.** Subscribers register and unregister themselves at runtime. The subject builds one event value that describes the change and passes it to each one. In languages with first-class functions, a subscriber is usually a callback, and `subscribe` returns a function that removes it. Write an interface only when a subscriber reacts to several kinds of events. ## Structure ```mermaid classDiagram class StockLedger { -List observers +subscribe(o) Unsubscribe +adjust(sku, delta) void } class StockObserver { <> +onStockChanged(change) void } class ReorderPlanner { -int reorderPoint +onStockChanged(change) void } class StorefrontAvailability { +onStockChanged(change) void } class AuditTrail { +onStockChanged(change) void } StockLedger o-- StockObserver : notifies StockObserver <|.. ReorderPlanner StockObserver <|.. StorefrontAvailability StockObserver <|.. AuditTrail ``` ### Participants - **Subject** (`StockLedger`): owns the state, keeps the subscriber list, and notifies after each change. It never checks a subscriber's concrete type. - **Observer** (`StockObserver`): the notification interface. Keep it to one method per event kind. - **Concrete observers** (`ReorderPlanner`, `StorefrontAvailability`, `AuditTrail`): each reacts to the event in its own way and knows nothing about the others. - **Event** (`StockChange`): an immutable value that describes what changed, so observers don't need to call back into the subject. - **Client**: wires observers to the subject and detaches them when they go away. ## Use it when - A method ends with a growing list of "also tell X" calls that have nothing to do with its main job. - Code in a lower layer must notify code in a higher layer without importing it, such as a domain model notifying a UI or a cache. - The set of interested parties changes at runtime: plugins, open browser tabs, connected sockets. - You need to add reactions (metrics, audit, cache invalidation) without editing or redeploying the code that causes the change. ## Do not use it when - The events are plain announcements and a platform mechanism already covers them. Use `EventEmitter` or `EventTarget` in JavaScript, signals in a UI framework, C# events, or a channel in Go - a hand-rolled subject duplicates them and gets the edge cases wrong. Write your own subject only when you need filtering, ordering or in-process delivery rules those mechanisms don't have. - There is exactly one reaction and it will not change. Call it directly; the call site documents the dependency. - The order of reactions matters, or one reaction must finish before the next starts. Observers should not depend on each other's order. Write the sequence as plain code, or use [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md). - You need delivery across processes, retries or persistence. Use a message queue or an outbox table. An in-memory observer list loses events on crash. - You can't guarantee that every listener unsubscribes. A long-lived subject, such as a global store, holds a strong reference to each listener and leaks every component that forgot to unsubscribe. If you can't guarantee an unsubscribe path, use scoped subscriptions (framework effects with cleanup, `AbortSignal`, context cancellation) or weak references. ## Application grill **Inspect first:** existing notification mechanisms (emitters, signals, streams, framework subscriptions) and the direct calls they would replace; whether any subscriber must not miss an event - that single requirement changes the infrastructure decision. **Missing pieces:** in-process: the language's built-ins - nothing to buy. Cross-process or durable: a broker - Redis pub/sub for transient fan-out; SQS, SNS or Kafka when a subscriber must not miss events (that is a queue with consumer groups, not pub/sub). **Ask before you build:** - **Scale:** how many subscribers and what event rate - what happens when one subscriber lags (backpressure)? - **Performance:** must delivery be synchronous within the request, or is eventual delivery acceptable per subscriber? - **Reliability:** one failing subscriber must not fail the others - what isolates them (retries, dead letters)? - **Budget:** managed broker versus in-process only - what tips the decision? - **Implementation:** who owns event schemas and their versioning when payloads change? ## Implementation steps 1. Find the method that makes the change, and the calls to other objects at its end that only react to it. 2. Define an immutable event value with everything those reactions read. Don't make observers query the subject. 3. Define the observer interface, or a callback type, with one method per event kind. 4. Give the subject a collection of observers, a `subscribe` method that returns an unsubscribe handle, and a private `notify`. 5. Move each reaction into its own observer and subscribe it at the composition root. 6. Iterate over a copy of the list during `notify`, so an observer can unsubscribe while being notified. 7. Decide what happens when an observer throws: let it propagate, or catch it, log it and continue. Write that decision down in a test. 8. Pair every `subscribe` with an unsubscribe on the subscriber's teardown path. ## Example A warehouse ledger tracks stock per SKU. When a level changes, the purchasing planner may reorder, the storefront hides sold-out items, and an audit trail records the move. ```ts title="stock.ts" interface StockChange { sku: string; previous: number; onHand: number; } interface StockObserver { onStockChanged(change: StockChange): void; } class StockLedger { private readonly levels = new Map(); private readonly observers = new Set(); subscribe(observer: StockObserver): () => void { this.observers.add(observer); return () => this.observers.delete(observer); } adjust(sku: string, delta: number): void { const previous = this.levels.get(sku) ?? 0; const onHand = previous + delta; this.levels.set(sku, onHand); const change: StockChange = { sku, previous, onHand }; // Copy so an observer can unsubscribe during notification. for (const observer of [...this.observers]) observer.onStockChanged(change); } } class ReorderPlanner implements StockObserver { constructor(private readonly reorderPoint: number) {} onStockChanged({ sku, previous, onHand }: StockChange): void { if (previous > this.reorderPoint && onHand <= this.reorderPoint) { console.log(`reorder ${sku}: ${onHand} left`); } } } class StorefrontAvailability implements StockObserver { readonly hidden = new Set(); onStockChanged({ sku, onHand }: StockChange): void { if (onHand <= 0) this.hidden.add(sku); else this.hidden.delete(sku); } } class AuditTrail implements StockObserver { readonly entries: string[] = []; onStockChanged({ sku, previous, onHand }: StockChange): void { this.entries.push(`${sku} ${previous}->${onHand}`); } } const ledger = new StockLedger(); const storefront = new StorefrontAvailability(); const audit = new AuditTrail(); ledger.subscribe(new ReorderPlanner(5)); ledger.subscribe(storefront); const stopAudit = ledger.subscribe(audit); ledger.adjust("KETTLE-01", 8); ledger.adjust("KETTLE-01", -4); // reorder KETTLE-01: 4 left stopAudit(); // detach on teardown, or the ledger keeps the trail alive ledger.adjust("KETTLE-01", -4); console.log([...storefront.hidden]); // [ 'KETTLE-01' ] console.log(audit.entries); // [ 'KETTLE-01 0->8', 'KETTLE-01 8->4' ] ``` ```python title="stock.py" from dataclasses import dataclass from typing import Callable, Protocol @dataclass(frozen=True) class StockChange: sku: str previous: int on_hand: int class StockObserver(Protocol): def on_stock_changed(self, change: StockChange) -> None: ... class StockLedger: def __init__(self) -> None: self._levels: dict[str, int] = {} self._observers: list[StockObserver] = [] def subscribe(self, observer: StockObserver) -> Callable[[], None]: self._observers.append(observer) return lambda: self._observers.remove(observer) def adjust(self, sku: str, delta: int) -> None: previous = self._levels.get(sku, 0) self._levels[sku] = previous + delta change = StockChange(sku, previous, previous + delta) for observer in list(self._observers): # copy: safe to unsubscribe mid-loop observer.on_stock_changed(change) class ReorderPlanner: def __init__(self, reorder_point: int) -> None: self.reorder_point = reorder_point def on_stock_changed(self, change: StockChange) -> None: if change.previous > self.reorder_point >= change.on_hand: print(f"reorder {change.sku}: {change.on_hand} left") class StorefrontAvailability: def __init__(self) -> None: self.hidden: set[str] = set() def on_stock_changed(self, change: StockChange) -> None: if change.on_hand <= 0: self.hidden.add(change.sku) else: self.hidden.discard(change.sku) class AuditTrail: def __init__(self) -> None: self.entries: list[str] = [] def on_stock_changed(self, change: StockChange) -> None: self.entries.append(f"{change.sku} {change.previous}->{change.on_hand}") if __name__ == "__main__": ledger = StockLedger() storefront, audit = StorefrontAvailability(), AuditTrail() ledger.subscribe(ReorderPlanner(5)) ledger.subscribe(storefront) stop_audit = ledger.subscribe(audit) ledger.adjust("KETTLE-01", 8) ledger.adjust("KETTLE-01", -4) # reorder KETTLE-01: 4 left stop_audit() # detach on teardown, or the ledger keeps the trail alive ledger.adjust("KETTLE-01", -4) print(storefront.hidden) # {'KETTLE-01'} print(audit.entries) # ['KETTLE-01 0->8', 'KETTLE-01 8->4'] ``` ```go title="stock.go" package main import "fmt" type StockChange struct { SKU string Previous int OnHand int } type StockObserver interface { OnStockChanged(c StockChange) } type subscription struct { id int observer StockObserver } type StockLedger struct { levels map[string]int subs []subscription nextID int } func NewStockLedger() *StockLedger { return &StockLedger{levels: map[string]int{}} } // Subscribe returns a function that removes the observer again. func (l *StockLedger) Subscribe(o StockObserver) func() { l.nextID++ id := l.nextID l.subs = append(l.subs, subscription{id, o}) return func() { kept := l.subs[:0:0] for _, s := range l.subs { if s.id != id { kept = append(kept, s) } } l.subs = kept } } func (l *StockLedger) Adjust(sku string, delta int) { prev := l.levels[sku] l.levels[sku] = prev + delta change := StockChange{sku, prev, prev + delta} for _, s := range append([]subscription(nil), l.subs...) { s.observer.OnStockChanged(change) } } type ReorderPlanner struct{ ReorderPoint int } func (r ReorderPlanner) OnStockChanged(c StockChange) { if c.Previous > r.ReorderPoint && c.OnHand <= r.ReorderPoint { fmt.Printf("reorder %s: %d left\n", c.SKU, c.OnHand) } } type StorefrontAvailability struct{ Hidden map[string]bool } func (s *StorefrontAvailability) OnStockChanged(c StockChange) { s.Hidden[c.SKU] = c.OnHand <= 0 } type AuditTrail struct{ Entries []string } func (a *AuditTrail) OnStockChanged(c StockChange) { a.Entries = append(a.Entries, fmt.Sprintf("%s %d->%d", c.SKU, c.Previous, c.OnHand)) } func main() { ledger := NewStockLedger() storefront := &StorefrontAvailability{Hidden: map[string]bool{}} audit := &AuditTrail{} ledger.Subscribe(ReorderPlanner{ReorderPoint: 5}) ledger.Subscribe(storefront) stopAudit := ledger.Subscribe(audit) ledger.Adjust("KETTLE-01", 8) ledger.Adjust("KETTLE-01", -4) // reorder KETTLE-01: 4 left stopAudit() // detach on teardown, or the ledger keeps the trail alive ledger.Adjust("KETTLE-01", -4) fmt.Println(storefront.Hidden) // map[KETTLE-01:true] fmt.Println(audit.Entries) // [KETTLE-01 0->8 KETTLE-01 8->4] } ``` ```java title="Stock.java" import java.util.*; public class Stock { record StockChange(String sku, int previous, int onHand) {} @FunctionalInterface interface StockObserver { void onStockChanged(StockChange change); } static final class StockLedger { private final Map levels = new HashMap<>(); private final List observers = new ArrayList<>(); Runnable subscribe(StockObserver observer) { observers.add(observer); return () -> observers.remove(observer); } void adjust(String sku, int delta) { int previous = levels.getOrDefault(sku, 0); levels.put(sku, previous + delta); var change = new StockChange(sku, previous, previous + delta); // Copy so an observer can unsubscribe during notification. for (StockObserver o : List.copyOf(observers)) o.onStockChanged(change); } } record ReorderPlanner(int reorderPoint) implements StockObserver { public void onStockChanged(StockChange c) { if (c.previous() > reorderPoint && c.onHand() <= reorderPoint) { System.out.println("reorder " + c.sku() + ": " + c.onHand() + " left"); } } } static final class StorefrontAvailability implements StockObserver { final Set hidden = new TreeSet<>(); public void onStockChanged(StockChange c) { if (c.onHand() <= 0) hidden.add(c.sku()); else hidden.remove(c.sku()); } } public static void main(String[] args) { var ledger = new StockLedger(); var storefront = new StorefrontAvailability(); List audit = new ArrayList<>(); ledger.subscribe(new ReorderPlanner(5)); ledger.subscribe(storefront); // A lambda is an observer too. Runnable stopAudit = ledger.subscribe( c -> audit.add(c.sku() + " " + c.previous() + "->" + c.onHand())); ledger.adjust("KETTLE-01", 8); ledger.adjust("KETTLE-01", -4); // reorder KETTLE-01: 4 left stopAudit.run(); // detach on teardown, or the ledger keeps the lambda alive ledger.adjust("KETTLE-01", -4); System.out.println(storefront.hidden); // [KETTLE-01] System.out.println(audit); // [KETTLE-01 0->8, KETTLE-01 8->4] } } ``` ## Trade-offs **Gains** - New reactions are new code; the subject does not change. - The subject depends on one interface, not on every consumer, so lower layers stay free of upper-layer imports. - Subscriptions can change at runtime. - Each observer is testable alone by feeding it an event value. **Costs** - Control flow is implicit. A reader of `adjust` can't see what runs after it without finding every `subscribe` call. - Forgotten unsubscribes leak memory and cause "zombie" handlers that act on torn-down objects. - Notification order is usually unspecified, and code that relies on it breaks silently. - A throwing or slow observer runs on the subject's call stack and affects every caller of the subject. - An observer that changes the subject during notification can trigger cascades or infinite loops. ## Relations - [Mediator](https://aipatterns.dev/design-patterns/mediator.md) centralizes how a group of objects talk to each other. It is often implemented with observers, but a mediator knows its participants; a subject does not. - [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) passes a request along until one handler deals with it, in a defined order. Observer delivers to every subscriber, in no order you should rely on. - [Command](https://aipatterns.dev/design-patterns/command.md) turns a request into an object. Combine the two when events must be queued, replayed or logged before observers see them. - [Singleton](https://aipatterns.dev/design-patterns/singleton.md) is how a global event bus usually starts. Avoid it: a global subject hides every dependency and holds every listener for the life of the process. ## Agent checklist - [ ] You checked for an existing event mechanism in the platform or framework before writing a subject. - [ ] Every `subscribe` has a matching unsubscribe on the subscriber's teardown path, verified by a test or a cleanup hook. - [ ] The event is an immutable value with all the data observers need; observers don't call back into the subject. - [ ] No observer depends on running before or after another one. - [ ] Notification iterates over a copy of the list, and the behavior when an observer throws is decided and tested. - [ ] The existing tests still pass, and each observer has a test that feeds it an event directly. --- # State > Move each mode of an object into its own class so behavior and transitions change with the mode instead of branching on a status field. - kind: pattern - category: behavioral - aliases: Objects for States - canonical: https://aipatterns.dev/design-patterns/state - inspired-by: https://refactoring.guru/design-patterns/state ## Intent Let an object change its behavior when its internal mode changes, as if it had switched class. Each mode becomes a separate object that implements the same interface, and the owning object delegates to whichever mode is current. Transitions become the mode objects' job, not a status check repeated in every method. ## Forces - **Several methods** vs **one status field**: every public method starts with the same `switch (status)`, and the branches drift apart. - **Legal transitions** vs **scattered assignments**: `status = "canceled"` can appear anywhere, so nobody can list which moves are allowed. - **Mode-specific data** vs **one flat object**: a retry counter that only means something while a payment is overdue still sits on the object in every mode. - **Adding a mode** vs **editing every method**: a new status should not mean touching each `switch` in the file. ## Therefore **Give each mode its own class behind one interface, have the context forward every mode-dependent call to the current mode object, and let mode objects decide which mode comes next.** The context holds one reference to the current state. A state method does its work and returns (or sets) the next state. Mode-specific data lives in the state object that needs it. If states carry no data, you can share one instance of each. ## Structure ```mermaid classDiagram class Subscription { -SubscriptionState state +paymentSucceeded() void +paymentFailed() void +cancel() void +canUseProduct() bool } class SubscriptionState { <> +paymentSucceeded() SubscriptionState +paymentFailed() SubscriptionState +cancel() SubscriptionState +canUseProduct() bool } class Trialing { +canUseProduct() bool } class Active { +canUseProduct() bool } class PastDue { -int failedCharges +paymentFailed() SubscriptionState } class Canceled { +canUseProduct() bool } Subscription o-- SubscriptionState SubscriptionState <|.. Trialing SubscriptionState <|.. Active SubscriptionState <|.. PastDue SubscriptionState <|.. Canceled PastDue ..> Canceled : third failure ``` The lifecycle the example implements: ```mermaid stateDiagram-v2 [*] --> Trialing Trialing --> Active : paymentSucceeded Trialing --> PastDue : paymentFailed Active --> PastDue : paymentFailed PastDue --> Active : paymentSucceeded PastDue --> Canceled : third failure Trialing --> Canceled : cancel Active --> Canceled : cancel PastDue --> Canceled : cancel Canceled --> Active : paymentSucceeded ``` ### Participants - **Context** (`Subscription`): the object clients use. It holds the current state, forwards calls to it, and stores the state it gets back. - **State** (`SubscriptionState`): the interface for every mode-dependent operation, including the events that cause transitions. - **Concrete states** (`Trialing`, `Active`, `PastDue`, `Canceled`): one mode each. They implement that mode's behavior, hold that mode's data, and pick the next state. ## Use it when - Several methods of one class branch on the same status field, and the branches disagree about edge cases. - You need a reviewable answer to "which transitions are legal", and today the answer is a grep for assignments. - Some fields are meaningful only in one mode, such as an attempt counter, a lock holder or a deadline. - Modes keep being added, such as a new paused or suspended status, and each one touches every method. ## Do not use it when - The machine is small: three or four states, one or two events, little behavior per state. Use an enum and a single `switch` (or `match`) in one `transition` function. In TypeScript, Rust, Kotlin, Swift or Java 21, an exhaustive switch over a closed type makes the compiler flag every unhandled state, which a class hierarchy does not. Past roughly a dozen states, or once states carry behavior that calls back into the context, the switch becomes the maintenance problem and per-state classes pay off. - States differ only in *which state comes next*, not in behavior. Use a table-driven FSM: a map from `(state, event)` to the next state. It is data, so you can print it, validate it and draw it. - The workflow is long-running, persisted and audited, such as orders or approvals. Store the state as a column and drive it with a workflow engine or a transition table. Rebuild state objects from the column, not the other way around. - The behavior varies by a choice made from outside and never changes on its own. That is [Strategy](https://aipatterns.dev/design-patterns/strategy.md). ## Application grill **Inspect first:** the status fields branching across methods and every site that switches on them; the transition rules as they exist scattered in conditionals; where the state lives (a database column, memory) and who guards invalid transitions today. **Missing pieces:** none beyond the language: a transition table or one object per state. If states are long-running and must survive process death (an order lifecycle over days), the infrastructure answer is a state table with a CHECK constraint, or a workflow engine - decide by whether the state outlives the process. **Ask before you build:** - **Scale:** how many states and transitions - past roughly six, a table beats one class per state? - **Performance:** is the state checked per request (both forms are cheap; measure only in tight loops)? - **Reliability:** is an invalid transition a loud failure or a silent coercion, and does the database enforce it too? - **Budget:** N state classes versus one transition table - which does the team maintain? - **Implementation:** how are legacy rows migrated to the new state model, and who owns the transition tests? ## Implementation steps 1. List every state and every event. Draw the transition table or a `stateDiagram-v2` first; it exposes missing and illegal moves. 2. Write the state interface with one method per event plus each query whose answer depends on the mode. 3. Create one class per state. Move the matching branch of each `switch` into it, and move mode-only fields into the state that uses them. 4. Make each event method return the next state. Returning a value keeps states free of a back-reference to the context. 5. Replace the status field in the context with a state reference, and make each public method forward to the state and store the result. 6. Decide what an illegal event does in each state: ignore it, return `this`, or throw. Make it consistent and test it. 7. If you persist the context, map the state to a stable string or enum at the storage boundary. ## Example A SaaS billing service tracks a subscription from trial to paid, through failed card charges, to cancellation. Access to the product depends on where the subscription is in that lifecycle. ```ts title="subscription.ts" const MAX_FAILED_CHARGES = 3; interface SubscriptionState { readonly name: string; canUseProduct(): boolean; paymentSucceeded(): SubscriptionState; paymentFailed(): SubscriptionState; cancel(): SubscriptionState; } class Trialing implements SubscriptionState { readonly name = "trialing"; canUseProduct() { return true; } paymentSucceeded() { return new Active(); } paymentFailed() { return new PastDue(1); } cancel() { return new Canceled(); } } class Active implements SubscriptionState { readonly name = "active"; canUseProduct() { return true; } paymentSucceeded() { return this; } paymentFailed() { return new PastDue(1); } cancel() { return new Canceled(); } } class PastDue implements SubscriptionState { constructor(private readonly failedCharges: number) {} get name() { return `past_due(${this.failedCharges})`; } canUseProduct() { return this.failedCharges < 2; } // one-charge grace period paymentSucceeded() { return new Active(); } paymentFailed(): SubscriptionState { const failed = this.failedCharges + 1; return failed >= MAX_FAILED_CHARGES ? new Canceled() : new PastDue(failed); } cancel() { return new Canceled(); } } class Canceled implements SubscriptionState { readonly name = "canceled"; canUseProduct() { return false; } paymentSucceeded() { return new Active(); } // reactivation paymentFailed() { return this; } cancel() { return this; } } class Subscription { private state: SubscriptionState = new Trialing(); constructor(readonly accountId: string) {} paymentSucceeded(): void { this.moveTo(this.state.paymentSucceeded()); } paymentFailed(): void { this.moveTo(this.state.paymentFailed()); } cancel(): void { this.moveTo(this.state.cancel()); } canUseProduct(): boolean { return this.state.canUseProduct(); } private moveTo(next: SubscriptionState): void { if (next !== this.state) console.log(`${this.accountId}: ${this.state.name} -> ${next.name}`); this.state = next; } } const sub = new Subscription("acct_42"); sub.paymentSucceeded(); // trialing -> active sub.paymentFailed(); // active -> past_due(1) console.log(sub.canUseProduct()); // true sub.paymentFailed(); // past_due(1) -> past_due(2) console.log(sub.canUseProduct()); // false sub.paymentFailed(); // past_due(2) -> canceled sub.paymentSucceeded(); // canceled -> active ``` ```python title="subscription.py" from __future__ import annotations MAX_FAILED_CHARGES = 3 class Trialing: name = "trialing" def can_use_product(self) -> bool: return True def payment_succeeded(self) -> State: return Active() def payment_failed(self) -> State: return PastDue(1) def cancel(self) -> State: return Canceled() class Active: name = "active" def can_use_product(self) -> bool: return True def payment_succeeded(self) -> State: return self def payment_failed(self) -> State: return PastDue(1) def cancel(self) -> State: return Canceled() class PastDue: def __init__(self, failed_charges: int) -> None: self.failed_charges = failed_charges self.name = f"past_due({failed_charges})" def can_use_product(self) -> bool: return self.failed_charges < 2 # one-charge grace period def payment_succeeded(self) -> State: return Active() def payment_failed(self) -> State: failed = self.failed_charges + 1 return Canceled() if failed >= MAX_FAILED_CHARGES else PastDue(failed) def cancel(self) -> State: return Canceled() class Canceled: name = "canceled" def can_use_product(self) -> bool: return False def payment_succeeded(self) -> State: return Active() # reactivation def payment_failed(self) -> State: return self def cancel(self) -> State: return self State = Trialing | Active | PastDue | Canceled class Subscription: def __init__(self, account_id: str) -> None: self.account_id = account_id self._state: State = Trialing() def payment_succeeded(self) -> None: self._move_to(self._state.payment_succeeded()) def payment_failed(self) -> None: self._move_to(self._state.payment_failed()) def cancel(self) -> None: self._move_to(self._state.cancel()) def can_use_product(self) -> bool: return self._state.can_use_product() def _move_to(self, next_state: State) -> None: if next_state is not self._state: print(f"{self.account_id}: {self._state.name} -> {next_state.name}") self._state = next_state if __name__ == "__main__": sub = Subscription("acct_42") sub.payment_succeeded() # trialing -> active sub.payment_failed() # active -> past_due(1) print(sub.can_use_product()) # True sub.payment_failed() # past_due(1) -> past_due(2) print(sub.can_use_product()) # False sub.payment_failed() # past_due(2) -> canceled sub.payment_succeeded() # canceled -> active ``` ```go title="subscription.go" package main import "fmt" const maxFailedCharges = 3 type State interface { Name() string CanUseProduct() bool PaymentSucceeded() State PaymentFailed() State Cancel() State } type Trialing struct{} func (Trialing) Name() string { return "trialing" } func (Trialing) CanUseProduct() bool { return true } func (Trialing) PaymentSucceeded() State { return Active{} } func (Trialing) PaymentFailed() State { return PastDue{FailedCharges: 1} } func (Trialing) Cancel() State { return Canceled{} } type Active struct{} func (Active) Name() string { return "active" } func (Active) CanUseProduct() bool { return true } func (a Active) PaymentSucceeded() State { return a } func (Active) PaymentFailed() State { return PastDue{FailedCharges: 1} } func (Active) Cancel() State { return Canceled{} } type PastDue struct{ FailedCharges int } func (p PastDue) Name() string { return fmt.Sprintf("past_due(%d)", p.FailedCharges) } func (p PastDue) CanUseProduct() bool { return p.FailedCharges < 2 } // one-charge grace period func (PastDue) PaymentSucceeded() State { return Active{} } func (PastDue) Cancel() State { return Canceled{} } func (p PastDue) PaymentFailed() State { if p.FailedCharges+1 >= maxFailedCharges { return Canceled{} } return PastDue{FailedCharges: p.FailedCharges + 1} } type Canceled struct{} func (Canceled) Name() string { return "canceled" } func (Canceled) CanUseProduct() bool { return false } func (Canceled) PaymentSucceeded() State { return Active{} } // reactivation func (c Canceled) PaymentFailed() State { return c } func (c Canceled) Cancel() State { return c } type Subscription struct { AccountID string state State } func NewSubscription(id string) *Subscription { return &Subscription{AccountID: id, state: Trialing{}} } func (s *Subscription) PaymentSucceeded() { s.moveTo(s.state.PaymentSucceeded()) } func (s *Subscription) PaymentFailed() { s.moveTo(s.state.PaymentFailed()) } func (s *Subscription) Cancel() { s.moveTo(s.state.Cancel()) } func (s *Subscription) CanUseProduct() bool { return s.state.CanUseProduct() } func (s *Subscription) moveTo(next State) { if next != s.state { // value structs compare by content fmt.Printf("%s: %s -> %s\n", s.AccountID, s.state.Name(), next.Name()) } s.state = next } func main() { sub := NewSubscription("acct_42") sub.PaymentSucceeded() // trialing -> active sub.PaymentFailed() // active -> past_due(1) fmt.Println(sub.CanUseProduct()) // true sub.PaymentFailed() // past_due(1) -> past_due(2) fmt.Println(sub.CanUseProduct()) // false sub.PaymentFailed() // past_due(2) -> canceled sub.PaymentSucceeded() // canceled -> active } ``` ```java title="Billing.java" public class Billing { static final int MAX_FAILED_CHARGES = 3; sealed interface SubscriptionState permits Trialing, Active, PastDue, Canceled { String name(); boolean canUseProduct(); SubscriptionState paymentSucceeded(); SubscriptionState paymentFailed(); SubscriptionState cancel(); } record Trialing() implements SubscriptionState { public String name() { return "trialing"; } public boolean canUseProduct() { return true; } public SubscriptionState paymentSucceeded() { return new Active(); } public SubscriptionState paymentFailed() { return new PastDue(1); } public SubscriptionState cancel() { return new Canceled(); } } record Active() implements SubscriptionState { public String name() { return "active"; } public boolean canUseProduct() { return true; } public SubscriptionState paymentSucceeded() { return this; } public SubscriptionState paymentFailed() { return new PastDue(1); } public SubscriptionState cancel() { return new Canceled(); } } record PastDue(int failedCharges) implements SubscriptionState { public String name() { return "past_due(" + failedCharges + ")"; } public boolean canUseProduct() { return failedCharges < 2; } // one-charge grace period public SubscriptionState paymentSucceeded() { return new Active(); } public SubscriptionState paymentFailed() { int failed = failedCharges + 1; return failed >= MAX_FAILED_CHARGES ? new Canceled() : new PastDue(failed); } public SubscriptionState cancel() { return new Canceled(); } } record Canceled() implements SubscriptionState { public String name() { return "canceled"; } public boolean canUseProduct() { return false; } public SubscriptionState paymentSucceeded() { return new Active(); } // reactivation public SubscriptionState paymentFailed() { return this; } public SubscriptionState cancel() { return this; } } static final class Subscription { private final String accountId; private SubscriptionState state = new Trialing(); Subscription(String accountId) { this.accountId = accountId; } void paymentSucceeded() { moveTo(state.paymentSucceeded()); } void paymentFailed() { moveTo(state.paymentFailed()); } void cancel() { moveTo(state.cancel()); } boolean canUseProduct() { return state.canUseProduct(); } private void moveTo(SubscriptionState next) { if (!next.equals(state)) { System.out.println(accountId + ": " + state.name() + " -> " + next.name()); } state = next; } } public static void main(String[] args) { var sub = new Subscription("acct_42"); sub.paymentSucceeded(); // trialing -> active sub.paymentFailed(); // active -> past_due(1) System.out.println(sub.canUseProduct()); // true sub.paymentFailed(); // past_due(1) -> past_due(2) System.out.println(sub.canUseProduct()); // false sub.paymentFailed(); // past_due(2) -> canceled sub.paymentSucceeded(); // canceled -> active } } ``` ## Trade-offs **Gains** - Each mode's behavior sits in one place, and the repeated `switch` disappears from every method. - Transitions are explicit return values, so you can list and test every legal move. - Mode-only data lives only in the mode that uses it, so it can't be read in the wrong mode. - A new mode is a new class plus the transitions into it. **Costs** - More types, and the full transition graph is spread across classes. Without a diagram, nobody can see it in one view. - Adding an *event* means touching every state class, the same cost Visitor pays for new element types. - States that need the context's data either receive it as arguments or hold a back-reference, which couples them to the context. - Persisting the context needs a mapping between state objects and a stored value. - For small machines, an enum with an exhaustive switch is shorter and gives better compiler checks. ## Relations - [Strategy](https://aipatterns.dev/design-patterns/strategy.md) has the same delegation shape. Strategies are picked from outside and ignore each other; states pick their successors and know about each other. - [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md) applies when states hold no data: share one instance per state class instead of allocating on every transition. - [Memento](https://aipatterns.dev/design-patterns/memento.md) can snapshot the current state object to support undo or rollback of a transition. - [Singleton](https://aipatterns.dev/design-patterns/singleton.md) is the usual way stateless states get shared, though a module-level constant does the same with less ceremony. ## Agent checklist - [ ] You drew or wrote the transition table before creating classes, and it has no missing `(state, event)` pairs. - [ ] You compared the result against an enum plus exhaustive `switch` and a transition table, and the class version is clearly smaller or clearer. - [ ] Only state objects decide the next state; no code outside them assigns the state field directly. - [ ] Behavior for illegal events is consistent across states and covered by tests. - [ ] Persisted state round-trips through a stable stored value. - [ ] The tests that covered the old `switch` still pass, and each state has its own transition tests. --- # Strategy > Put each interchangeable algorithm behind one interface so the caller picks a behavior at runtime without branching on it. - kind: pattern - category: behavioral - aliases: Policy - canonical: https://aipatterns.dev/design-patterns/strategy - inspired-by: https://refactoring.guru/design-patterns/strategy ## Intent Define a family of algorithms that do the same job in different ways, give them one shared interface, and let the calling code hold a reference to whichever one it needs. The caller stops knowing *how* the work is done and only knows *that* it can be done. ## Forces - **One stable caller** vs **many ways to do its core step**: the surrounding workflow rarely changes, but the step in the middle does. - **Choosing at runtime** vs **readable code**: a growing `switch` over modes works until every new mode touches the same function. - **Testing each variant in isolation** vs **variants tangled in one class**: you can't unit test the third branch of a 200-line method on its own. - **Adding a variant** vs **editing code that already works**: each new case should be new code, not a change to shipped code. ## Therefore **Move each variant into its own object behind a common interface, and have the caller delegate to that object instead of branching.** The caller (the *context*) receives a strategy through its constructor or a setter. To add a behavior, you write a new strategy; the context does not change. In languages with first-class functions, a strategy is often just a function value. Reach for an interface only when a strategy needs state, configuration, or several related methods. ## Structure ```mermaid classDiagram class Checkout { -ShippingStrategy shipping +setShipping(s) void +total(order) Money } class ShippingStrategy { <> +quote(order) Money } class FlatRate { +quote(order) Money } class ByWeight { +quote(order) Money } class FreeOverThreshold { +quote(order) Money } Checkout o-- ShippingStrategy ShippingStrategy <|.. FlatRate ShippingStrategy <|.. ByWeight ShippingStrategy <|.. FreeOverThreshold ``` ### Participants - **Strategy** (`ShippingStrategy`): the interface every variant implements. Keep it to what the context actually calls. - **Concrete strategies** (`FlatRate`, `ByWeight`, …): one algorithm each, with no knowledge of each other. - **Context** (`Checkout`): holds a strategy, calls it at the right moment, and never inspects its concrete type. - **Client**: picks a concrete strategy and hands it to the context, usually from configuration or user input. ## Use it when - A function has a `switch` or `if/else` chain that selects between *algorithms* (not data), and new branches keep arriving. - Several classes differ only in one method's behavior. - You need to swap behavior at runtime, such as by tenant, feature flag, A/B bucket, or user setting. - You want to test each algorithm on its own with no setup for the others. - An algorithm uses data or dependencies the rest of the class shouldn't see. ## Do not use it when - There are only two variants and no realistic third. A boolean parameter or a plain `if` is clearer. - The variant is a single stateless operation. Pass a function (`(order) => Money`); in a language with first-class functions, a one-method interface and three classes is ceremony. Keep the interface when variants carry configuration, mutable state, or a family of related methods. - The variants change *what* happens across many methods together, depending on the object's current situation. That is [State](https://aipatterns.dev/design-patterns/state.md). - The variation is a few steps inside a fixed skeleton. [Template Method](https://aipatterns.dev/design-patterns/template-method.md) or plain helper functions fit better. - The caller would have to know every concrete strategy anyway to choose one, and the choice never changes after construction. A factory function returning the right implementation, or a lookup table, may be all you need. ## Application grill **Inspect first:** the branching on algorithm choice and every site that switches on it; whether the codebase already passes functions (idiomatic in TypeScript, Python, Go); whether the choice comes from code, configuration, or per-tenant data. **Missing pieces:** none beyond the language: pass a function. A strategy registry keyed by name earns its place only when the choice arrives from configuration or user input. **Ask before you build:** - **Scale:** how many real variants exist today - fewer than three with no third planned is a conditional? - **Performance:** is dispatch on a hot path where a registry lookup shows (a direct call is cheapest)? - **Reliability:** when configuration names an unknown strategy, does it fail at startup or at first use? - **Budget:** N files for N strategies - is the set actually changing, or frozen? - **Implementation:** what context does each strategy need - one shared interface, or per-strategy parameters? ## Implementation steps 1. Find the branching code and confirm each branch computes the same kind of result from the same inputs. 2. Write the strategy interface from what the context passes in and needs back. Pass data in as arguments; avoid handing strategies the whole context. 3. Move each branch body into its own strategy class or function. Keep behavior identical and run the tests after each move. 4. Give the context a field for the strategy, set through its constructor (or a setter if runtime swapping is required). 5. Replace the branching with one call to the strategy. 6. Move the *selection* logic (which strategy to use) to the edge: configuration, a factory, or a map from key to strategy. ## Example An online store quotes shipping in three ways, and the marketing team adds a new rule every quarter. ```ts title="shipping.ts" type Money = number; // cents interface Order { subtotal: Money; weightKg: number; } interface ShippingStrategy { quote(order: Order): Money; } class FlatRate implements ShippingStrategy { constructor(private readonly fee: Money) {} quote(): Money { return this.fee; } } class ByWeight implements ShippingStrategy { constructor(private readonly perKg: Money) {} quote(order: Order): Money { return Math.ceil(order.weightKg) * this.perKg; } } class FreeOverThreshold implements ShippingStrategy { constructor( private readonly threshold: Money, private readonly fallback: ShippingStrategy, ) {} quote(order: Order): Money { return order.subtotal >= this.threshold ? 0 : this.fallback.quote(order); } } class Checkout { constructor(private shipping: ShippingStrategy) {} setShipping(shipping: ShippingStrategy): void { this.shipping = shipping; } total(order: Order): Money { return order.subtotal + this.shipping.quote(order); } } // Selection lives at the edge, not inside Checkout. const strategies: Record = { flat: new FlatRate(499), weight: new ByWeight(150), promo: new FreeOverThreshold(5000, new FlatRate(499)), }; const checkout = new Checkout(strategies["promo"]); console.log(checkout.total({ subtotal: 6200, weightKg: 3.2 })); // 6200 console.log(checkout.total({ subtotal: 1800, weightKg: 3.2 })); // 2299 ``` ```python title="shipping.py" from dataclasses import dataclass from typing import Protocol @dataclass(frozen=True) class Order: subtotal: int # cents weight_kg: float class ShippingStrategy(Protocol): def quote(self, order: Order) -> int: ... @dataclass(frozen=True) class FlatRate: fee: int def quote(self, order: Order) -> int: return self.fee @dataclass(frozen=True) class ByWeight: per_kg: int def quote(self, order: Order) -> int: return -(-order.weight_kg // 1) * self.per_kg # ceil without math import @dataclass(frozen=True) class FreeOverThreshold: threshold: int fallback: ShippingStrategy def quote(self, order: Order) -> int: return 0 if order.subtotal >= self.threshold else self.fallback.quote(order) class Checkout: def __init__(self, shipping: ShippingStrategy) -> None: self.shipping = shipping def total(self, order: Order) -> int: return order.subtotal + int(self.shipping.quote(order)) STRATEGIES: dict[str, ShippingStrategy] = { "flat": FlatRate(499), "weight": ByWeight(150), "promo": FreeOverThreshold(5000, FlatRate(499)), } if __name__ == "__main__": checkout = Checkout(STRATEGIES["promo"]) print(checkout.total(Order(6200, 3.2))) # 6200 print(checkout.total(Order(1800, 3.2))) # 2299 ``` ```go title="shipping.go" package main import ( "fmt" "math" ) type Order struct { Subtotal int // cents WeightKg float64 } // ShippingStrategy is satisfied implicitly by any type with Quote. type ShippingStrategy interface { Quote(o Order) int } type FlatRate struct{ Fee int } func (f FlatRate) Quote(Order) int { return f.Fee } type ByWeight struct{ PerKg int } func (b ByWeight) Quote(o Order) int { return int(math.Ceil(o.WeightKg)) * b.PerKg } type FreeOverThreshold struct { Threshold int Fallback ShippingStrategy } func (f FreeOverThreshold) Quote(o Order) int { if o.Subtotal >= f.Threshold { return 0 } return f.Fallback.Quote(o) } // QuoteFunc lets a plain function act as a strategy. type QuoteFunc func(Order) int func (q QuoteFunc) Quote(o Order) int { return q(o) } type Checkout struct{ Shipping ShippingStrategy } func (c Checkout) Total(o Order) int { return o.Subtotal + c.Shipping.Quote(o) } func main() { strategies := map[string]ShippingStrategy{ "flat": FlatRate{Fee: 499}, "weight": ByWeight{PerKg: 150}, "promo": FreeOverThreshold{Threshold: 5000, Fallback: FlatRate{Fee: 499}}, "pickup": QuoteFunc(func(Order) int { return 0 }), } c := Checkout{Shipping: strategies["promo"]} fmt.Println(c.Total(Order{Subtotal: 6200, WeightKg: 3.2})) // 6200 fmt.Println(c.Total(Order{Subtotal: 1800, WeightKg: 3.2})) // 2299 } ``` ```java title="Shipping.java" import java.util.Map; public class Shipping { record Order(long subtotal, double weightKg) {} @FunctionalInterface interface ShippingStrategy { long quote(Order order); } record FlatRate(long fee) implements ShippingStrategy { public long quote(Order order) { return fee; } } record ByWeight(long perKg) implements ShippingStrategy { public long quote(Order order) { return (long) Math.ceil(order.weightKg()) * perKg; } } record FreeOverThreshold(long threshold, ShippingStrategy fallback) implements ShippingStrategy { public long quote(Order order) { return order.subtotal() >= threshold ? 0 : fallback.quote(order); } } static final class Checkout { private ShippingStrategy shipping; Checkout(ShippingStrategy shipping) { this.shipping = shipping; } void setShipping(ShippingStrategy shipping) { this.shipping = shipping; } long total(Order order) { return order.subtotal() + shipping.quote(order); } } public static void main(String[] args) { Map strategies = Map.of( "flat", new FlatRate(499), "weight", new ByWeight(150), "promo", new FreeOverThreshold(5000, new FlatRate(499)), "pickup", order -> 0 // a lambda is a strategy too ); Checkout checkout = new Checkout(strategies.get("promo")); System.out.println(checkout.total(new Order(6200, 3.2))); // 6200 System.out.println(checkout.total(new Order(1800, 3.2))); // 2299 } } ``` ## Trade-offs **Gains** - New behaviors are new code, and the context stays closed to modification. - Each algorithm is unit-testable alone. - Runtime swapping without conditionals. - Strategies compose: `FreeOverThreshold` wraps another strategy. **Costs** - More types, and the reader has to jump to find the active behavior. - Clients must know the strategies exist in order to pick one; the selection logic has to live somewhere. - A wide strategy interface forces every variant to implement methods it doesn't need. - Overkill when a function parameter would do. ## Relations - [State](https://aipatterns.dev/design-patterns/state.md) has the same shape, but states know about each other and trigger transitions. Strategies are independent and chosen from outside. - [Template Method](https://aipatterns.dev/design-patterns/template-method.md) varies steps through inheritance at compile time. Strategy varies the whole algorithm through composition at runtime. - [Command](https://aipatterns.dev/design-patterns/command.md) also wraps behavior in an object, but to queue, log or undo an action, not to pick between ways of computing something. - [Decorator](https://aipatterns.dev/design-patterns/decorator.md) changes an object's skin by wrapping it. Strategy changes its guts by replacing an internal part. - [Bridge](https://aipatterns.dev/design-patterns/bridge.md) uses the same delegation structurally, to keep two whole hierarchies independent. ## Agent checklist - [ ] There are at least three real variants, or a third is concretely planned. Otherwise keep the conditional. - [ ] If the language has first-class functions, you considered a function type before writing an interface. - [ ] The strategy interface takes inputs as arguments rather than a reference to the whole context. - [ ] Selection (which strategy) lives at the composition root, config or a lookup map, not inside the context. - [ ] Each concrete strategy has its own test, and the original behavior is preserved, verified by running the existing tests. --- # Template Method > Fix an algorithm's sequence of steps in a base class and let subclasses fill in or override individual steps without changing the order. - kind: pattern - category: behavioral - aliases: Hook Method - canonical: https://aipatterns.dev/design-patterns/template-method - inspired-by: https://refactoring.guru/design-patterns/template-method ## Intent Write the skeleton of an algorithm once, as a method that calls a sequence of steps. Subclasses supply the steps that differ and may override optional *hooks*, but they can't change the order or skip the invariant parts. The base class owns the "when"; subclasses own some of the "how". ## Forces - **Shared sequence** vs **varying details**: three importers split, parse, validate and summarize in the same order, but each parses differently. - **Duplicate code** vs **coupling through inheritance**: copying the sequence into each variant drifts, but a base class ties every variant to its internals. - **Invariants enforced once** vs **freedom for variants**: validation must run for every format, so no variant should be able to forget it. - **Optional extension points** vs **a readable skeleton**: each hook you add is one more thing a reader has to check for overrides. ## Therefore **Put the fixed sequence in one non-overridable method of a base class, and turn each varying step into an abstract method or a hook with a default.** Abstract steps are mandatory: every subclass must implement them. Hooks have a default (often empty) that a subclass may override. Mark the template method `final` (or its equivalent) so subclasses can't reorder the steps. In languages without implementation inheritance, such as Go, or wherever functions are values, pass the steps in as an interface or as functions instead. ## Structure ```mermaid classDiagram class StatementImporter { <> +importStatement(raw) ImportResult +splitRecords(raw) List +isHeader(record) bool +parseRecord(record) Transaction } class CommaSeparatedImporter { +isHeader(record) bool +parseRecord(record) Transaction } class FixedWidthImporter { +splitRecords(raw) List +parseRecord(record) Transaction } class ImportResult { -List transactions -List rejected } StatementImporter <|-- CommaSeparatedImporter StatementImporter <|-- FixedWidthImporter StatementImporter ..> ImportResult : builds ``` ### Participants - **Abstract class** (`StatementImporter`): defines the template method `importStatement`, the abstract steps, and hooks with default behavior. - **Template method** (`importStatement`): the fixed sequence. It calls the steps in order and enforces the invariants, here date validation and rejection tracking. - **Concrete classes** (`CommaSeparatedImporter`, `FixedWidthImporter`): implement the abstract steps and override only the hooks they need. ## Use it when - Two or more classes run the same steps in the same order and differ in one or two of them. That is the [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) smell with a shared shape. - An invariant step, such as validation, auditing or resource cleanup, must run for every variant and must not be skippable. - A framework needs to call user code at fixed points of its own lifecycle, such as setup and teardown in a test runner. - You are already in an inheritance hierarchy and the variation is small and stable. ## Do not use it when - The skeleton is one function's body and callers choose the steps. Pass the varying steps as function parameters (`importStatement(raw, { parseRecord, isHeader })`) - in a language with first-class functions this covers a one-function skeleton without a class hierarchy, and the caller can mix steps freely. The hierarchy earns its keep when subclasses override hooks between framework calls or share protected state across the skeleton. - Steps vary independently. Two parsing styles times two splitting styles needs four subclasses. Compose the steps instead, as [Strategy](https://aipatterns.dev/design-patterns/strategy.md) objects or functions; that needs two of each. - Variants must change at runtime or per call. Inheritance fixes the choice when the object is created. - The base class and the subclasses live in different packages or teams. Every change to the base class can break subclasses it can't see (the fragile base class problem). Prefer delegation; see [Replace Inheritance with Delegation](https://aipatterns.dev/refactoring/techniques/replace-inheritance-with-delegation.md). - There is only one variant. A plain function with no extension points is enough. ## Application grill **Inspect first:** the workflows duplicated across subclasses or branches, and which steps actually differ; whether the language idiom prefers step callbacks or higher-order functions (it does in TypeScript, Python and Go); the existing inheritance depth. **Missing pieces:** none beyond the language; prefer a function taking step callbacks where the language allows - the class hierarchy form is for when subclasses are a domain concept, not just variation. **Ask before you build:** - **Scale:** how many steps vary per implementation - one varying step is a callback, not a hierarchy? - **Performance:** virtual dispatch cost is negligible outside tight loops - is this one? - **Reliability:** when the template's step order changes, which subclasses silently break - who owns the step contract? - **Budget:** deep hierarchies are the classic maintenance tax - is this one level deep? - **Implementation:** how are hook defaults documented, and is there a test for a subclass that forgets a required step? ## Implementation steps 1. Put the duplicated methods side by side and mark which lines are identical and which differ. 2. Split each method into the same named steps with [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md), so the methods differ only in step bodies. [Form Template Method](https://aipatterns.dev/refactoring/techniques/form-template-method.md) is the refactoring for this. 3. Pull the now-identical sequence up into a base class as the template method, and mark it `final` or document that it must not be overridden. 4. Declare the differing steps abstract. Give optional steps a default and treat them as hooks. 5. Keep the number of abstract steps small. Each one is a method every subclass must write. 6. Delete the duplicated sequences from the subclasses and run the tests. 7. If a subclass overrides almost every step, stop. It doesn't share the algorithm, and it should not inherit it. ## Example A personal finance app imports bank statements. One bank exports CSV with a header row; another sends fixed-width records with a trailer line. Both must reject records with bad dates the same way. ```ts title="importer.ts" interface Transaction { date: string; description: string; amountCents: number } interface ImportResult { transactions: Transaction[]; rejected: string[] } abstract class StatementImporter { // The template method: subclasses must not override it. importStatement(raw: string): ImportResult { const result: ImportResult = { transactions: [], rejected: [] }; for (const record of this.splitRecords(raw)) { if (this.isHeader(record)) continue; const tx = this.parseRecord(record); if (tx && /^\d{4}-\d{2}-\d{2}$/.test(tx.date)) result.transactions.push(tx); else result.rejected.push(record); } return result; } protected abstract parseRecord(record: string): Transaction | null; protected splitRecords(raw: string): string[] { return raw.split("\n").filter((line) => line.trim().length > 0); } protected isHeader(_record: string): boolean { return false; // hook: most formats have no header } } class CommaSeparatedImporter extends StatementImporter { protected isHeader(record: string): boolean { return record.startsWith("date,"); } protected parseRecord(record: string): Transaction | null { const [date, description, amount] = record.split(","); const cents = Math.round(Number(amount) * 100); return Number.isFinite(cents) ? { date, description, amountCents: cents } : null; } } class FixedWidthImporter extends StatementImporter { // Layout: date YYYYMMDD [0,8), description [8,24), signed cents [24,). protected splitRecords(raw: string): string[] { return super.splitRecords(raw).filter((line) => !line.startsWith("TRL")); } protected parseRecord(record: string): Transaction | null { const d = record.slice(0, 8); const cents = Number(record.slice(24).trim()); if (!Number.isInteger(cents)) return null; const date = `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`; return { date, description: record.slice(8, 24).trim(), amountCents: cents }; } } const csv = "date,description,amount\n2026-03-01,Coffee roaster,-18.50\n2026-03-02,Refund,abc\n2026-03-03,Payroll,2400.00\n"; const fixed = "20260301Coffee roaster -1850\n2026-3-3Payroll +240000\nTRL2\n"; const a = new CommaSeparatedImporter().importStatement(csv); console.log(a.transactions.length, a.rejected); // 2 [ '2026-03-02,Refund,abc' ] const b = new FixedWidthImporter().importStatement(fixed); console.log(b.transactions.map((t) => t.amountCents), b.rejected.length); // [ -1850 ] 1 ``` ```python title="importer.py" import re from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import final ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$") @dataclass(frozen=True) class Transaction: date: str description: str amount_cents: int @dataclass class ImportResult: transactions: list[Transaction] = field(default_factory=list) rejected: list[str] = field(default_factory=list) class StatementImporter(ABC): @final def import_statement(self, raw: str) -> ImportResult: result = ImportResult() for record in self.split_records(raw): if self.is_header(record): continue tx = self.parse_record(record) if tx and ISO_DATE.match(tx.date): result.transactions.append(tx) else: result.rejected.append(record) return result @abstractmethod def parse_record(self, record: str) -> Transaction | None: ... def split_records(self, raw: str) -> list[str]: return [line for line in raw.split("\n") if line.strip()] def is_header(self, record: str) -> bool: return False # hook: most formats have no header class CommaSeparatedImporter(StatementImporter): def is_header(self, record: str) -> bool: return record.startswith("date,") def parse_record(self, record: str) -> Transaction | None: try: date, description, amount = record.split(",") return Transaction(date, description, round(float(amount) * 100)) except ValueError: return None class FixedWidthImporter(StatementImporter): # Layout: date YYYYMMDD [0,8), description [8,24), signed cents [24,). def split_records(self, raw: str) -> list[str]: return [r for r in super().split_records(raw) if not r.startswith("TRL")] def parse_record(self, record: str) -> Transaction | None: d, amount = record[:8], record[24:].strip() if not amount.lstrip("+-").isdigit(): return None return Transaction(f"{d[:4]}-{d[4:6]}-{d[6:8]}", record[8:24].strip(), int(amount)) if __name__ == "__main__": csv = "date,description,amount\n2026-03-01,Coffee roaster,-18.50\n2026-03-02,Refund,abc\n2026-03-03,Payroll,2400.00\n" fixed = "20260301Coffee roaster -1850\n2026-3-3Payroll +240000\nTRL2\n" a = CommaSeparatedImporter().import_statement(csv) print(len(a.transactions), a.rejected) # 2 ['2026-03-02,Refund,abc'] b = FixedWidthImporter().import_statement(fixed) print([t.amount_cents for t in b.transactions], len(b.rejected)) # [-1850] 1 ``` ```go title="importer.go" package main import ( "fmt" "math" "regexp" "strconv" "strings" ) type Transaction struct { Date, Description string AmountCents int } type ImportResult struct { Transactions []Transaction Rejected []string } // Go has no inheritance: the required step is an interface, // and optional hooks are smaller interfaces checked at runtime. type RecordParser interface { ParseRecord(record string) (Transaction, bool) } type HeaderSkipper interface{ IsHeader(record string) bool } type RecordFilter interface{ KeepRecord(record string) bool } var isoDate = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) // ImportStatement is the template: the sequence lives here, once. func ImportStatement(p RecordParser, raw string) ImportResult { var result ImportResult for _, record := range strings.Split(raw, "\n") { if strings.TrimSpace(record) == "" { continue } if f, ok := p.(RecordFilter); ok && !f.KeepRecord(record) { continue } if h, ok := p.(HeaderSkipper); ok && h.IsHeader(record) { continue } if tx, ok := p.ParseRecord(record); ok && isoDate.MatchString(tx.Date) { result.Transactions = append(result.Transactions, tx) } else { result.Rejected = append(result.Rejected, record) } } return result } type CommaSeparated struct{} func (CommaSeparated) IsHeader(r string) bool { return strings.HasPrefix(r, "date,") } func (CommaSeparated) ParseRecord(r string) (Transaction, bool) { parts := strings.Split(r, ",") if len(parts) != 3 { return Transaction{}, false } amount, err := strconv.ParseFloat(parts[2], 64) return Transaction{parts[0], parts[1], int(math.Round(amount * 100))}, err == nil } // FixedWidth layout: date YYYYMMDD [0,8), description [8,24), signed cents [24,). type FixedWidth struct{} func (FixedWidth) KeepRecord(r string) bool { return !strings.HasPrefix(r, "TRL") } func (FixedWidth) ParseRecord(r string) (Transaction, bool) { if len(r) < 25 { return Transaction{}, false } cents, err := strconv.Atoi(strings.TrimSpace(r[24:])) date := r[0:4] + "-" + r[4:6] + "-" + r[6:8] return Transaction{date, strings.TrimSpace(r[8:24]), cents}, err == nil } func main() { csv := "date,description,amount\n2026-03-01,Coffee roaster,-18.50\n2026-03-02,Refund,abc\n2026-03-03,Payroll,2400.00\n" fixed := "20260301Coffee roaster -1850\n2026-3-3Payroll +240000\nTRL2\n" a := ImportStatement(CommaSeparated{}, csv) fmt.Println(len(a.Transactions), a.Rejected) // 2 [2026-03-02,Refund,abc] b := ImportStatement(FixedWidth{}, fixed) fmt.Println(b.Transactions[0].AmountCents, len(b.Rejected)) // -1850 1 } ``` ```java title="Importer.java" import java.util.*; import java.util.regex.Pattern; public class Importer { record Transaction(String date, String description, long amountCents) {} record ImportResult(List transactions, List rejected) {} abstract static class StatementImporter { private static final Pattern ISO_DATE = Pattern.compile("\\d{4}-\\d{2}-\\d{2}"); // final: subclasses fill in steps but cannot reorder them. public final ImportResult importStatement(String raw) { var result = new ImportResult(new ArrayList<>(), new ArrayList<>()); for (String record : splitRecords(raw)) { if (isHeader(record)) continue; Optional tx = parseRecord(record); if (tx.isPresent() && ISO_DATE.matcher(tx.get().date()).matches()) { result.transactions().add(tx.get()); } else { result.rejected().add(record); } } return result; } protected abstract Optional parseRecord(String record); protected List splitRecords(String raw) { return raw.lines().filter(l -> !l.isBlank()).toList(); } protected boolean isHeader(String record) { return false; } // hook } static final class CommaSeparatedImporter extends StatementImporter { @Override protected boolean isHeader(String r) { return r.startsWith("date,"); } @Override protected Optional parseRecord(String r) { String[] p = r.split(","); try { return Optional.of(new Transaction(p[0], p[1], Math.round(Double.parseDouble(p[2]) * 100))); } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { return Optional.empty(); } } } // Layout: date YYYYMMDD [0,8), description [8,24), signed cents [24,). static final class FixedWidthImporter extends StatementImporter { @Override protected List splitRecords(String raw) { return super.splitRecords(raw).stream().filter(l -> !l.startsWith("TRL")).toList(); } @Override protected Optional parseRecord(String r) { try { long cents = Long.parseLong(r.substring(24).trim()); String date = r.substring(0, 4) + "-" + r.substring(4, 6) + "-" + r.substring(6, 8); return Optional.of(new Transaction(date, r.substring(8, 24).trim(), cents)); } catch (NumberFormatException | StringIndexOutOfBoundsException e) { return Optional.empty(); } } } public static void main(String[] args) { String csv = "date,description,amount\n2026-03-01,Coffee roaster,-18.50\n2026-03-02,Refund,abc\n2026-03-03,Payroll,2400.00\n"; String fixed = "20260301Coffee roaster -1850\n2026-3-3Payroll +240000\nTRL2\n"; var a = new CommaSeparatedImporter().importStatement(csv); System.out.println(a.transactions().size() + " " + a.rejected()); // 2 [2026-03-02,Refund,abc] var b = new FixedWidthImporter().importStatement(fixed); System.out.println(b.transactions().get(0).amountCents() + " " + b.rejected().size()); // -1850 1 } } ``` ## Trade-offs **Gains** - The sequence and its invariants exist once. No variant can skip validation or reorder steps. - A new variant writes only the steps that differ. - Hooks give controlled extension points instead of open-ended overriding. - It removes duplicated control flow without changing any caller. **Costs** - Inheritance couples subclasses to the base class's internals. Renaming or reordering a step can break every subclass. - The algorithm is split across files. To understand one run, a reader traces the template method and every override. - The choice of variant is fixed at construction and can't be combined with another variant. - Hooks multiply. A base class with many hooks becomes hard to reason about, and subclasses override them in surprising combinations. - In Go, and in any code that passes functions, the same result needs no hierarchy at all. ## Relations - [Strategy](https://aipatterns.dev/design-patterns/strategy.md) varies the whole algorithm through composition at runtime. Template Method varies parts of it through inheritance at compile time. When steps start varying independently, convert each step into a strategy. - [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) is often one step of a template method: the skeleton calls an overridable method to create an object it needs. - [Builder](https://aipatterns.dev/design-patterns/builder.md) with a director also runs a fixed sequence of steps, but to assemble an object, and through composition rather than subclassing. ## Agent checklist - [ ] The variants really share the same steps in the same order; no subclass overrides most of the steps. - [ ] You considered a single function that takes the varying steps as parameters, and the class hierarchy is justified. - [ ] The template method is `final`, `@final`, or documented as not overridable. - [ ] Abstract steps are few, and every optional step is a hook with a safe default. - [ ] Invariant steps such as validation live only in the template method, not in subclasses. - [ ] Existing tests pass, and each concrete class has tests for its own steps. --- # Visitor > Add new operations over a fixed set of node types without editing those types, by routing each node to a type-specific method on a visitor object. - kind: pattern - category: behavioral - aliases: Double Dispatch - canonical: https://aipatterns.dev/design-patterns/visitor - inspired-by: https://refactoring.guru/design-patterns/visitor ## Intent Separate operations from the object structure they run over. Each element type gets one `accept` method that calls back the matching method on a visitor, so a new operation is a new visitor class and the element classes stay untouched. The pattern makes new operations cheap and new element types expensive. ## Forces - **A stable set of node types** vs **a growing list of operations**: an expression tree keeps the same four node kinds while evaluation, pretty-printing, linting and serialization pile up. - **Cohesive operations** vs **cohesive types**: putting `evaluate`, `explain` and `toJson` on every node scatters each operation across many classes. - **Type-specific behavior** vs **no type checks**: without help from the language, picking behavior by concrete type means `instanceof` chains that miss new types silently. - **Encapsulation** vs **access**: a visitor needs to read each element's data, which pushes elements to expose more than they otherwise would. ## Therefore **Give every element an `accept(visitor)` method that calls `visitor.visitX(this)`, and put each operation in its own visitor class with one method per element type.** The two calls make up *double dispatch*: `accept` resolves the element's type, and `visitX` resolves the operation. The visitor interface lists every element type, so the compiler reports any visitor that misses one. In languages with sum types and exhaustive pattern matching, such as Rust, Kotlin, Scala, Swift, TypeScript discriminated unions or Java 21 sealed interfaces, a `switch` over the type gives you the same guarantee with no `accept` methods at all. ## Structure ```mermaid classDiagram class Policy { <> +accept(v) R } class HasRole { -String role +accept(v) R } class AllOf { -List children +accept(v) R } class Not { -Policy inner +accept(v) R } class PolicyVisitor { <> +visitHasRole(p) R +visitAllOf(p) R +visitNot(p) R } class Evaluator { -Set roles +visitHasRole(p) bool } class Explainer { +visitHasRole(p) String } Policy <|.. HasRole Policy <|.. AllOf Policy <|.. Not PolicyVisitor <|.. Evaluator PolicyVisitor <|.. Explainer Policy ..> PolicyVisitor : accept calls visitX ``` ### Participants - **Element** (`Policy`): declares `accept(visitor)`. - **Concrete elements** (`HasRole`, `AllOf`, `AnyOf`, `Not`): each implements `accept` by calling its own `visitX` method. They contain no operation logic. - **Visitor** (`PolicyVisitor`): one method per concrete element type. Generic over the result type where the language allows it. - **Concrete visitors** (`Evaluator`, `Explainer`): one operation each, with a case for every element type. - **Object structure**: the tree or collection being visited. Composite visitors recurse into children by calling `child.accept(this)`. ## Use it when - The element hierarchy is stable and owned by you, and new operations over it arrive often, as with ASTs, query plans, document models and rule trees. - Operations need different behavior per node type, and you want the compiler to flag a missing case. - Several unrelated operations would otherwise bloat every element class with methods that belong to other layers, such as rendering, persistence or metrics. - An operation needs to accumulate state across a whole traversal, such as collecting every referenced role or counting nodes by kind. ## Do not use it when - The element types are a closed set defined in your own code, and each operation visits every type once. Model the elements as a sealed or union type and write one `switch`/`match` per operation: it is shorter, keeps each operation in one function, and the compiler still reports a missing case. This covers TypeScript (discriminated unions with a `never` check), Rust, Kotlin, Swift, Scala and Java 21+. When the classes cannot share a union - different owners, different packages - this bullet does not apply. - New element types arrive more often than new operations. Every new element type forces a new method on the visitor interface and an edit to every visitor. Put the operation on the element instead, with plain polymorphism. - You don't own the element classes and can't add `accept`. Use a type switch, a lookup by type, or an adapter. - There is one operation. A method on each element (plain polymorphism) is simpler. - The structure is a flat list of one type. Use a loop or [Iterator](https://aipatterns.dev/design-patterns/iterator.md). ## Application grill **Inspect first:** the node set's stability (visitors assume it is frozen) and who would add node types; existing pattern matching or exhaustive switches over the same types; whether the code is compiler-shaped (ASTs, documents) where visitors are natural. **Missing pieces:** none beyond the language: in languages with discriminated unions, an exhaustive switch or pattern match is the modern form and gives compiler errors on new nodes for free. **Ask before you build:** - **Scale:** how often are new node types added versus new operations - visitors pay off only when operations change more often than types? - **Performance:** does dispatch overhead matter in tight AST loops - measure before choosing the shape? - **Reliability:** what does a partial visitor do for a node it does not handle - default case or type error? - **Budget:** an exhaustive switch gives compiler-enforced completeness without an interface - is the visitor interface buying anything beyond that? - **Implementation:** does the visitor carry per-node return types, and who keeps the double-dispatch glue honest? ## Implementation steps 1. Confirm the set of element types is stable and that new operations, not new types, are the expected change. 2. Declare the visitor interface with one `visitX` method per concrete element. Make it generic over the return type if the language allows, to avoid mutable result fields. 3. Add `accept(visitor)` to the element interface, and implement it in each element as a single call to its own `visitX`. 4. Move each operation, often a [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md) smell over node types, into a visitor class, one method per case. 5. Decide who traverses children. Usually the visitor recurses by calling `child.accept(this)`, which lets it control order and short-circuit. 6. Delete the old type checks and run the tests. ## Example An internal admin tool stores access rules as small expression trees such as "admin, or billing and not a contractor". The same tree must be evaluated against a user and explained in plain text on the settings page. ```ts title="policy.ts" interface PolicyVisitor { visitHasRole(p: HasRole): R; visitAllOf(p: AllOf): R; visitAnyOf(p: AnyOf): R; visitNot(p: Not): R; } interface Policy { accept(v: PolicyVisitor): R; } class HasRole implements Policy { constructor(readonly role: string) {} accept(v: PolicyVisitor): R { return v.visitHasRole(this); } } class AllOf implements Policy { constructor(readonly children: Policy[]) {} accept(v: PolicyVisitor): R { return v.visitAllOf(this); } } class AnyOf implements Policy { constructor(readonly children: Policy[]) {} accept(v: PolicyVisitor): R { return v.visitAnyOf(this); } } class Not implements Policy { constructor(readonly inner: Policy) {} accept(v: PolicyVisitor): R { return v.visitNot(this); } } class Evaluator implements PolicyVisitor { constructor(private readonly roles: ReadonlySet) {} visitHasRole(p: HasRole): boolean { return this.roles.has(p.role); } visitAllOf(p: AllOf): boolean { return p.children.every((c) => c.accept(this)); } visitAnyOf(p: AnyOf): boolean { return p.children.some((c) => c.accept(this)); } visitNot(p: Not): boolean { return !p.inner.accept(this); } } class Explainer implements PolicyVisitor { visitHasRole(p: HasRole): string { return p.role; } visitAllOf(p: AllOf): string { return `(${p.children.map((c) => c.accept(this)).join(" AND ")})`; } visitAnyOf(p: AnyOf): string { return p.children.map((c) => c.accept(this)).join(" OR "); } visitNot(p: Not): string { return `NOT ${p.inner.accept(this)}`; } } const canIssueRefunds = new AnyOf([ new HasRole("admin"), new AllOf([new HasRole("billing"), new Not(new HasRole("contractor"))]), ]); console.log(canIssueRefunds.accept(new Explainer())); // admin OR (billing AND NOT contractor) console.log(canIssueRefunds.accept(new Evaluator(new Set(["billing"])))); // true console.log(canIssueRefunds.accept(new Evaluator(new Set(["billing", "contractor"])))); // false ``` ```python title="policy.py" from __future__ import annotations from dataclasses import dataclass from typing import Generic, Protocol, TypeVar R = TypeVar("R", covariant=True) class PolicyVisitor(Protocol[R]): def visit_has_role(self, p: HasRole) -> R: ... def visit_all_of(self, p: AllOf) -> R: ... def visit_any_of(self, p: AnyOf) -> R: ... def visit_not(self, p: Not) -> R: ... @dataclass(frozen=True) class HasRole: role: str def accept(self, v: PolicyVisitor[R]) -> R: return v.visit_has_role(self) @dataclass(frozen=True) class AllOf: children: tuple[Policy, ...] def accept(self, v: PolicyVisitor[R]) -> R: return v.visit_all_of(self) @dataclass(frozen=True) class AnyOf: children: tuple[Policy, ...] def accept(self, v: PolicyVisitor[R]) -> R: return v.visit_any_of(self) @dataclass(frozen=True) class Not: inner: Policy def accept(self, v: PolicyVisitor[R]) -> R: return v.visit_not(self) Policy = HasRole | AllOf | AnyOf | Not class Evaluator: def __init__(self, roles: set[str]) -> None: self.roles = roles def visit_has_role(self, p: HasRole) -> bool: return p.role in self.roles def visit_all_of(self, p: AllOf) -> bool: return all(c.accept(self) for c in p.children) def visit_any_of(self, p: AnyOf) -> bool: return any(c.accept(self) for c in p.children) def visit_not(self, p: Not) -> bool: return not p.inner.accept(self) class Explainer: def visit_has_role(self, p: HasRole) -> str: return p.role def visit_all_of(self, p: AllOf) -> str: return "(" + " AND ".join(c.accept(self) for c in p.children) + ")" def visit_any_of(self, p: AnyOf) -> str: return " OR ".join(c.accept(self) for c in p.children) def visit_not(self, p: Not) -> str: return f"NOT {p.inner.accept(self)}" if __name__ == "__main__": can_issue_refunds = AnyOf(( HasRole("admin"), AllOf((HasRole("billing"), Not(HasRole("contractor")))), )) print(can_issue_refunds.accept(Explainer())) # admin OR (billing AND NOT contractor) print(can_issue_refunds.accept(Evaluator({"billing"}))) # True print(can_issue_refunds.accept(Evaluator({"billing", "contractor"}))) # False ``` ```go title="policy.go" package main import ( "fmt" "strings" ) // Go methods can't be generic, so visitors store their result in a field. // Idiomatic Go often skips Visitor and uses a type switch over Policy instead. type PolicyVisitor interface { VisitHasRole(p HasRole) VisitAllOf(p AllOf) VisitAnyOf(p AnyOf) VisitNot(p Not) } type Policy interface{ Accept(v PolicyVisitor) } type HasRole struct{ Role string } type AllOf struct{ Children []Policy } type AnyOf struct{ Children []Policy } type Not struct{ Inner Policy } func (p HasRole) Accept(v PolicyVisitor) { v.VisitHasRole(p) } func (p AllOf) Accept(v PolicyVisitor) { v.VisitAllOf(p) } func (p AnyOf) Accept(v PolicyVisitor) { v.VisitAnyOf(p) } func (p Not) Accept(v PolicyVisitor) { v.VisitNot(p) } type Evaluator struct { Roles map[string]bool result bool } func (e *Evaluator) eval(p Policy) bool { p.Accept(e); return e.result } func (e *Evaluator) VisitHasRole(p HasRole) { e.result = e.Roles[p.Role] } func (e *Evaluator) VisitNot(p Not) { e.result = !e.eval(p.Inner) } func (e *Evaluator) VisitAllOf(p AllOf) { ok := true for _, c := range p.Children { ok = ok && e.eval(c) } e.result = ok } func (e *Evaluator) VisitAnyOf(p AnyOf) { ok := false for _, c := range p.Children { ok = ok || e.eval(c) } e.result = ok } type Explainer struct{ out string } func (x *Explainer) explain(p Policy) string { p.Accept(x); return x.out } func (x *Explainer) join(ps []Policy, sep string) string { parts := make([]string, len(ps)) for i, c := range ps { parts[i] = x.explain(c) } return strings.Join(parts, sep) } func (x *Explainer) VisitHasRole(p HasRole) { x.out = p.Role } func (x *Explainer) VisitAllOf(p AllOf) { x.out = "(" + x.join(p.Children, " AND ") + ")" } func (x *Explainer) VisitAnyOf(p AnyOf) { x.out = x.join(p.Children, " OR ") } func (x *Explainer) VisitNot(p Not) { x.out = "NOT " + x.explain(p.Inner) } func main() { canIssueRefunds := AnyOf{[]Policy{ HasRole{"admin"}, AllOf{[]Policy{HasRole{"billing"}, Not{HasRole{"contractor"}}}}, }} fmt.Println((&Explainer{}).explain(canIssueRefunds)) // admin OR (billing AND NOT contractor) billing := &Evaluator{Roles: map[string]bool{"billing": true}} fmt.Println(billing.eval(canIssueRefunds)) // true contractor := &Evaluator{Roles: map[string]bool{"billing": true, "contractor": true}} fmt.Println(contractor.eval(canIssueRefunds)) // false } ``` ```java title="Policies.java" import java.util.List; import java.util.Set; import java.util.stream.Collectors; public class Policies { interface PolicyVisitor { R visitHasRole(HasRole p); R visitAllOf(AllOf p); R visitAnyOf(AnyOf p); R visitNot(Not p); } interface Policy { R accept(PolicyVisitor v); } record HasRole(String role) implements Policy { public R accept(PolicyVisitor v) { return v.visitHasRole(this); } } record AllOf(List children) implements Policy { public R accept(PolicyVisitor v) { return v.visitAllOf(this); } } record AnyOf(List children) implements Policy { public R accept(PolicyVisitor v) { return v.visitAnyOf(this); } } record Not(Policy inner) implements Policy { public R accept(PolicyVisitor v) { return v.visitNot(this); } } record Evaluator(Set roles) implements PolicyVisitor { public Boolean visitHasRole(HasRole p) { return roles.contains(p.role()); } public Boolean visitAllOf(AllOf p) { return p.children().stream().allMatch(c -> c.accept(this)); } public Boolean visitAnyOf(AnyOf p) { return p.children().stream().anyMatch(c -> c.accept(this)); } public Boolean visitNot(Not p) { return !p.inner().accept(this); } } static final class Explainer implements PolicyVisitor { public String visitHasRole(HasRole p) { return p.role(); } public String visitAllOf(AllOf p) { return "(" + join(p.children(), " AND ") + ")"; } public String visitAnyOf(AnyOf p) { return join(p.children(), " OR "); } public String visitNot(Not p) { return "NOT " + p.inner().accept(this); } private String join(List ps, String sep) { return ps.stream().map(c -> c.accept(this)).collect(Collectors.joining(sep)); } } public static void main(String[] args) { Policy canIssueRefunds = new AnyOf(List.of( new HasRole("admin"), new AllOf(List.of(new HasRole("billing"), new Not(new HasRole("contractor")))))); System.out.println(canIssueRefunds.accept(new Explainer())); // admin OR (billing AND NOT contractor) System.out.println(canIssueRefunds.accept(new Evaluator(Set.of("billing")))); // true System.out.println(canIssueRefunds.accept(new Evaluator(Set.of("billing", "contractor")))); // false } } ``` ## Trade-offs **Gains** - A new operation is one new class; element classes don't change. - Each operation's logic sits in one place instead of being spread across every element. - The compiler reports any visitor that misses an element type. - Visitors can carry state across a traversal, such as a symbol table, counters or an output buffer. **Costs** - Adding an element type means a new method on the visitor interface and an edit to every visitor. This is the price, and it grows with each visitor you write. - Elements must expose the data visitors read, which weakens encapsulation. - The `accept`/`visitX` round trip is indirect and confuses readers who haven't seen double dispatch. - In languages without generic methods, such as Go, results travel through mutable fields, which is error-prone. - Pattern matching over a closed type does the same job with less code in most modern languages. ## Relations - [Composite](https://aipatterns.dev/design-patterns/composite.md) is the usual structure a visitor walks; composite elements pass the visitor to their children. - [Iterator](https://aipatterns.dev/design-patterns/iterator.md) walks a collection without caring about element types. Visitor exists for structures whose elements have different types and need different handling. - [Command](https://aipatterns.dev/design-patterns/command.md) wraps one action as an object. A visitor is a family of related actions, one per element type, bundled into one object. - [Strategy](https://aipatterns.dev/design-patterns/strategy.md) swaps one algorithm for a single type. Visitor adds an algorithm across a whole hierarchy of types. ## Agent checklist - [ ] You checked whether the language offers sealed/union types with exhaustive matching, and Visitor is still the better fit. - [ ] The element hierarchy is stable and owned by you; new element types are rare. - [ ] Each `accept` is a single call to its own `visitX`, with no logic. - [ ] Every visitor handles every element type; no default case swallows new types silently. - [ ] Traversal order is decided in one place (usually the visitor) and covered by a test. - [ ] Old type checks (`instanceof` chains, type switches) for the moved operations are deleted, and existing tests pass. --- # Factory Method > Let a subclass or a supplied function decide which concrete object to create, so shared workflow code depends only on an interface. - kind: pattern - category: creational - aliases: Virtual Constructor - canonical: https://aipatterns.dev/design-patterns/factory-method - inspired-by: https://refactoring.guru/design-patterns/factory-method ## Intent Declare a method that returns an object of some interface type, and let subclasses (or a passed-in function) override it to choose the concrete class. The surrounding workflow is written once against the interface and never names the concrete type it works with. ## Forces - **Shared workflow** vs **varying collaborator**: the steps around an object stay the same, but which object is used differs per variant. - **Calling `new` directly** vs **depending on abstractions**: a hard-coded constructor ties reusable code to one implementation. - **Adding a variant** vs **editing working code**: a new kind of product should not mean another branch in the shared method. - **Testability** vs **construction buried inside logic**: you can't swap in a fake when the real object is built halfway through a method. ## Therefore **Move the `new` call into an overridable creation method, and write the rest of the workflow against the interface it returns.** The base class (the *creator*) owns the algorithm and calls `createX()` when it needs a product. Each subclass overrides only that method. In languages without inheritance, or when you prefer composition, the factory method becomes a function field or constructor parameter: same idea, fewer classes. ## Structure ```mermaid classDiagram class ContactImport { <> +run(raw) ImportSummary +createParser() RecordParser } class CsvImport { +createParser() RecordParser } class AddressBookImport { +createParser() RecordParser } class RecordParser { <> +parse(raw) Contact[] } class CsvParser { +parse(raw) Contact[] } class BlockParser { +parse(raw) Contact[] } ContactImport <|-- CsvImport ContactImport <|-- AddressBookImport ContactImport ..> RecordParser : uses RecordParser <|.. CsvParser RecordParser <|.. BlockParser CsvImport ..> CsvParser : creates AddressBookImport ..> BlockParser : creates ``` ### Participants - **Product** (`RecordParser`): the interface the workflow depends on. - **Concrete products** (`CsvParser`, `BlockParser`): the implementations that actually get built. - **Creator** (`ContactImport`): runs the shared workflow and declares the factory method `createParser()`. - **Concrete creators** (`CsvImport`, `AddressBookImport`): override the factory method to return one concrete product. ## Use it when - A base class or framework runs a fixed workflow but must let extensions decide which helper object it uses. - You find the same method duplicated across classes except for the one `new SomethingSpecific()` line. - A library needs users to plug in their own implementation of an internal component without forking it. - You need a seam for tests: overriding the creation method is the smallest change that lets a test inject a fake. ## Do not use it when - There is one concrete product and no realistic second. Call the constructor; add the seam when the second variant shows up. - The only reason for subclassing is to pick the product. Pass the product - or a factory function such as `() => RecordParser` - into the constructor instead; in a language with first-class functions this is plain dependency injection and avoids a class per variant. Keep the overridable method when subclasses must weave creation into their own setup logic. - You need to create several related objects that must match each other. Use [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md). - The selection is a lookup from a config string to a class. A map of key to constructor is clearer than a creator hierarchy. ## Application grill **Inspect first:** the runtime choices between concrete classes and who makes them (callers, config, environment); existing createX() helpers - many codebases already have this pattern under a plain name; a DI container's presence. **Missing pieces:** none beyond the language: pass a creator function. A DI container helps only when construction graphs are wide and wired in many entry points; do not introduce one for a single product choice. **Ask before you build:** - **Scale:** how many products exist and how often does the set change - a stable pair is a conditional, not a factory? - **Performance:** is creation on a per-request path where the indirection shows? - **Reliability:** must an unknown product fail at startup (validate the registry) or at first use? - **Budget:** the indirection's review cost versus the branch it removes - does the set actually change? - **Implementation:** where does the product registry live - code, configuration, or plugins - and who extends it? ## Implementation steps 1. Find the place where shared code calls a concrete constructor, and extract an interface for what the code actually uses from that object. 2. Make the shared code depend on the interface type only. 3. Replace the `new` call with a call to a creation method (`createParser()`). Start with a default implementation that returns the existing class, and keep tests green. 4. Make the creation method abstract (or keep the default) and move each variant into a subclass that overrides it. 5. If subclasses exist only to override that one method, switch to a constructor parameter holding a factory function and delete the subclasses. 6. Keep any parameters needed for construction (`delimiter`) on the concrete creator, not on the shared workflow. ## Example A CRM imports contacts from CSV exports and from address-book exports. The import workflow (parse, validate, count) is the same; only the parser differs. ```ts title="contact-import.ts" interface Contact { email: string; name: string; } interface RecordParser { parse(raw: string): Contact[]; } class CsvParser implements RecordParser { constructor(private readonly delimiter: string) {} parse(raw: string): Contact[] { const [header, ...rows] = raw.trim().split("\n"); const cols = header.split(this.delimiter); return rows.map((row) => { const f = row.split(this.delimiter); return { email: f[cols.indexOf("email")] ?? "", name: f[cols.indexOf("name")] ?? "" }; }); } } // Address-book export: "key: value" lines, a blank line between contacts. class BlockParser implements RecordParser { parse(raw: string): Contact[] { return raw.trim().split("\n\n").map((block) => { const fields = new Map(block.split("\n").map((l) => l.split(": ", 2) as [string, string])); return { email: fields.get("email") ?? "", name: fields.get("name") ?? "" }; }); } } abstract class ContactImport { // The factory method: subclasses decide which parser to build. protected abstract createParser(): RecordParser; run(raw: string): { imported: number; rejected: number } { const contacts = this.createParser().parse(raw); const valid = contacts.filter((c) => c.email.includes("@")); return { imported: valid.length, rejected: contacts.length - valid.length }; } } class CsvImport extends ContactImport { constructor(private readonly delimiter: string) { super(); } protected createParser(): RecordParser { return new CsvParser(this.delimiter); } } class AddressBookImport extends ContactImport { protected createParser(): RecordParser { return new BlockParser(); } } console.log(new CsvImport(";").run("email;name\nana@example.com;Ana\nnot-an-email;Bo")); // { imported: 1, rejected: 1 } const book = "email: li@example.com\nname: Li\n\nemail: kim@example.com\nname: Kim"; console.log(new AddressBookImport().run(book)); // { imported: 2, rejected: 0 } ``` ```python title="contact_import.py" from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Protocol @dataclass(frozen=True) class Contact: email: str name: str class RecordParser(Protocol): def parse(self, raw: str) -> list[Contact]: ... class CsvParser: def __init__(self, delimiter: str) -> None: self.delimiter = delimiter def parse(self, raw: str) -> list[Contact]: header, *rows = raw.strip().splitlines() cols = header.split(self.delimiter) records = [dict(zip(cols, row.split(self.delimiter))) for row in rows] return [Contact(r.get("email", ""), r.get("name", "")) for r in records] class BlockParser: """Address-book export: 'key: value' lines, a blank line between contacts.""" def parse(self, raw: str) -> list[Contact]: contacts = [] for block in raw.strip().split("\n\n"): fields = dict(line.split(": ", 1) for line in block.splitlines()) contacts.append(Contact(fields.get("email", ""), fields.get("name", ""))) return contacts class ContactImport(ABC): @abstractmethod def create_parser(self) -> RecordParser: """Factory method: each subclass picks its parser.""" def run(self, raw: str) -> dict[str, int]: contacts = self.create_parser().parse(raw) valid = [c for c in contacts if "@" in c.email] return {"imported": len(valid), "rejected": len(contacts) - len(valid)} class CsvImport(ContactImport): def __init__(self, delimiter: str) -> None: self.delimiter = delimiter def create_parser(self) -> RecordParser: return CsvParser(self.delimiter) class AddressBookImport(ContactImport): def create_parser(self) -> RecordParser: return BlockParser() if __name__ == "__main__": print(CsvImport(";").run("email;name\nana@example.com;Ana\nnot-an-email;Bo")) # {'imported': 1, 'rejected': 1} book = "email: li@example.com\nname: Li\n\nemail: kim@example.com\nname: Kim" print(AddressBookImport().run(book)) # {'imported': 2, 'rejected': 0} ``` ```go title="contact_import.go" package main import ( "fmt" "strings" ) type Contact struct{ Email, Name string } type RecordParser interface { Parse(raw string) []Contact } type CSVParser struct{ Delimiter string } func (p CSVParser) Parse(raw string) []Contact { lines := strings.Split(strings.TrimSpace(raw), "\n") col := map[string]int{} for i, name := range strings.Split(lines[0], p.Delimiter) { col[name] = i } var out []Contact for _, row := range lines[1:] { f := strings.Split(row, p.Delimiter) out = append(out, Contact{Email: f[col["email"]], Name: f[col["name"]]}) } return out } // BlockParser reads "key: value" lines, a blank line between contacts. type BlockParser struct{} func (BlockParser) Parse(raw string) []Contact { var out []Contact for _, block := range strings.Split(strings.TrimSpace(raw), "\n\n") { fields := map[string]string{} for _, line := range strings.Split(block, "\n") { if k, v, ok := strings.Cut(line, ": "); ok { fields[k] = v } } out = append(out, Contact{Email: fields["email"], Name: fields["name"]}) } return out } // Go has no subclassing, so the factory method is a function field. type ContactImport struct { NewParser func() RecordParser } func (ci ContactImport) Run(raw string) (imported, rejected int) { for _, c := range ci.NewParser().Parse(raw) { if strings.Contains(c.Email, "@") { imported++ } else { rejected++ } } return imported, rejected } func main() { csvImport := ContactImport{NewParser: func() RecordParser { return CSVParser{Delimiter: ";"} }} fmt.Println(csvImport.Run("email;name\nana@example.com;Ana\nnot-an-email;Bo")) // 1 1 bookImport := ContactImport{NewParser: func() RecordParser { return BlockParser{} }} fmt.Println(bookImport.Run("email: li@example.com\nname: Li\n\nemail: kim@example.com\nname: Kim")) // 2 0 } ``` ```java title="ContactImports.java" import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; public class ContactImports { record Contact(String email, String name) {} interface RecordParser { List parse(String raw); } record CsvParser(String delimiter) implements RecordParser { public List parse(String raw) { String[] lines = raw.strip().split("\n"); List cols = List.of(lines[0].split(Pattern.quote(delimiter))); List out = new ArrayList<>(); for (int i = 1; i < lines.length; i++) { String[] f = lines[i].split(Pattern.quote(delimiter)); out.add(new Contact(f[cols.indexOf("email")], f[cols.indexOf("name")])); } return out; } } // Address-book export: "key: value" lines, a blank line between contacts. static final class BlockParser implements RecordParser { public List parse(String raw) { List out = new ArrayList<>(); for (String block : raw.strip().split("\n\n")) { Map fields = new HashMap<>(); for (String line : block.split("\n")) { String[] kv = line.split(": ", 2); if (kv.length == 2) fields.put(kv[0], kv[1]); } out.add(new Contact(fields.getOrDefault("email", ""), fields.getOrDefault("name", ""))); } return out; } } abstract static class ContactImport { protected abstract RecordParser createParser(); // the factory method String run(String raw) { List contacts = createParser().parse(raw); long valid = contacts.stream().filter(c -> c.email().contains("@")).count(); return "imported=" + valid + " rejected=" + (contacts.size() - valid); } } static final class CsvImport extends ContactImport { private final String delimiter; CsvImport(String delimiter) { this.delimiter = delimiter; } protected RecordParser createParser() { return new CsvParser(delimiter); } } static final class AddressBookImport extends ContactImport { protected RecordParser createParser() { return new BlockParser(); } } public static void main(String[] args) { System.out.println(new CsvImport(";").run("email;name\nana@example.com;Ana\nnot-an-email;Bo")); // imported=1 rejected=1 String book = "email: li@example.com\nname: Li\n\nemail: kim@example.com\nname: Kim"; System.out.println(new AddressBookImport().run(book)); // imported=2 rejected=0 } } ``` ## Trade-offs **Gains** - The shared workflow depends on an interface, not on any concrete class. - A new input format is a new creator and product; `run()` is untouched. - Construction is a single, overridable seam that tests can replace. **Costs** - One creator subclass per product is a parallel hierarchy that grows with every variant. - Inheritance couples the variant to the base class's internals. A constructor parameter avoids that. - Readers must look in the subclass to learn what the workflow actually uses. ## Relations - [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md) is often a set of factory methods grouped on one object, producing a matched family instead of one product. - [Template Method](https://aipatterns.dev/design-patterns/template-method.md) is the larger shape a factory method usually lives in: a fixed algorithm with an overridable step, where the step is "create the collaborator". - [Prototype](https://aipatterns.dev/design-patterns/prototype.md) creates by copying a configured instance, so it needs no creator subclass per product. - [Builder](https://aipatterns.dev/design-patterns/builder.md) concerns how one complex object is assembled step by step. Factory Method only decides which class to instantiate. - [Strategy](https://aipatterns.dev/design-patterns/strategy.md) is what you often get after refactoring: pass the parser in, and the creator hierarchy disappears. ## Agent checklist - [ ] There are at least two concrete products today, or the code is a library extension point. Otherwise keep the constructor call. - [ ] You considered a constructor parameter (an object or a factory function) before adding subclasses. - [ ] The shared workflow references only the product interface, with no `instanceof` checks on the result. - [ ] Construction parameters live on the concrete creator, not on the shared workflow. - [ ] Existing tests pass unchanged, and each concrete creator has a test that checks which product it returns. --- # Abstract Factory > Create families of related objects through one interface so client code always gets a matching set and never names the concrete classes. - kind: pattern - category: creational - aliases: Kit - canonical: https://aipatterns.dev/design-patterns/abstract-factory - inspired-by: https://refactoring.guru/design-patterns/abstract-factory ## Intent Provide one interface with a creation method for each kind of object in a family, and implement it once per family. Client code asks the factory for its parts and gets a set that is guaranteed to fit together, without ever naming a concrete class. ## Forces - **Objects that must match** vs **code that builds them separately**: mixing an EU tax calculator with a US invoice layout is a bug that compiles fine. - **One switch per object** vs **one decision per family**: when every constructor call checks the same `market` flag, the flag leaks everywhere. - **Adding a family** vs **touching every client**: a new market should be one new class, not edits across the codebase. - **Adding a product kind** vs **a stable factory interface**: every new method on the factory must be implemented by every family. ## Therefore **Group the creation of each family's objects behind one factory interface, pick the concrete factory once at startup, and pass it to the code that needs the products.** The decision "which family?" is made exactly once, at the composition root. Everything downstream receives a factory (or the products it made) and works only with the product interfaces. Each product interface still matters on its own; the factory's job is only to guarantee consistency between them. ## Structure ```mermaid classDiagram class InvoicingKit { <> +taxCalculator() TaxCalculator +formatter() InvoiceFormatter } class EuInvoicing { +taxCalculator() TaxCalculator +formatter() InvoiceFormatter } class UsInvoicing { +taxCalculator() TaxCalculator +formatter() InvoiceFormatter } class TaxCalculator { <> +taxCents(net, region) int } class VatCalculator { +taxCents(net, region) int } class SalesTaxCalculator { +taxCents(net, region) int } class InvoiceFormatter { <> +render(seq, net, tax) string } class Billing { -TaxCalculator tax -InvoiceFormatter fmt +issue(netCents, region) string } InvoicingKit <|.. EuInvoicing InvoicingKit <|.. UsInvoicing TaxCalculator <|.. VatCalculator TaxCalculator <|.. SalesTaxCalculator EuInvoicing ..> VatCalculator : creates UsInvoicing ..> SalesTaxCalculator : creates Billing ..> InvoicingKit : uses Billing --> InvoiceFormatter ``` ### Participants - **Abstract factory** (`InvoicingKit`): one creation method per product kind. - **Concrete factories** (`EuInvoicing`, `UsInvoicing`): one per family; each returns products that belong together. - **Abstract products** (`TaxCalculator`, `InvoiceFormatter`): the interfaces clients use. - **Concrete products** (`VatCalculator`, `SalesTaxCalculator`, and the two formatters): one implementation per family per kind. In the example both calculators are one `RateTable` type configured with different rates; a product only needs its own class when its behavior differs. - **Client** (`Billing`): receives a factory, asks for products, and never sees a concrete class. ## Use it when - Several objects vary together along one axis (market, cloud provider, database vendor, test vs production), and mixing them is a bug. - The same `if (market === "eu")` check appears next to several different constructor calls. - You ship a library that must run against several backends, and each backend needs a matching set of adapters. - Tests need to swap a whole set of collaborators (in-memory store, fake clock, fake queue) in one move. ## Do not use it when - You have one family today and only imagine a second. That is [speculative generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md). Use plain constructors and extract the factory when the second family arrives with real requirements. - Only one product kind varies. Use [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md), or pass that one object in. - The products don't actually constrain each other. Independent choices deserve independent parameters, not a factory that bundles them. - A dependency injection container or a composition-root function already wires the right implementations per environment. A hand-written factory on top duplicates that wiring. - Product kinds change often. Every new creation method must be added to every concrete factory, which turns a small feature into a wide change. ## Application grill **Inspect first:** the concrete classes that would form each family, and how callers choose among them today (constructor calls at the use site, a config value, an environment variable); whether a module or DI container already centralizes construction. **Missing pieces:** none beyond the language: one factory module with one function per family. A DI container (Awilix, tsyringe, Spring, Google Wire) earns its place only when wiring is repeated across many entry points; do not introduce one for a single family. **Ask before you build:** - **Scale:** how many real families exist, and is a third actually planned (a second cloud, a second database vendor)? Two families with no third planned is a conditional, not a factory. - **Performance:** is construction on a per-request path where the indirection must stay allocation-cheap? - **Reliability:** must an unknown family fail at startup (validate the factory map) or at first use? - **Budget:** who maintains the wiring when a family gains or loses a member? - **Implementation:** how do tests obtain a family - through the factory like production, or by constructing test doubles directly? ## Implementation steps 1. List the product kinds and the families. Write them as a grid; every cell must be a real class you need, or the grid is speculative. 2. Extract an interface for each product kind from what clients actually call. 3. Declare the abstract factory with one creation method per product kind, returning the product interfaces. 4. Implement one concrete factory per family. Keep family-specific configuration (rates, prefixes) inside it. 5. Choose the concrete factory once, at the composition root, from configuration. 6. Change clients to take the factory (or the products) through their constructor, and delete the scattered family checks. ## Example A billing service sells in the EU and the US. EU invoices need VAT by country and EU-style numbering; US invoices need state sales tax and a different layout. Using the EU calculator with the US layout must be impossible. ```ts title="invoicing.ts" interface TaxCalculator { taxCents(netCents: number, region: string): number; } interface InvoiceFormatter { render(seq: number, netCents: number, taxCents: number): string; } interface InvoicingKit { taxCalculator(): TaxCalculator; formatter(): InvoiceFormatter; } const amount = (cents: number) => (cents / 100).toFixed(2); function rateFor(rates: Record, region: string): number { const rate = rates[region]; if (rate === undefined) throw new Error(`no tax rate for ${region}`); return rate; } class EuInvoicing implements InvoicingKit { taxCalculator(): TaxCalculator { const vat = { DE: 0.19, FR: 0.2, IE: 0.23 }; return { taxCents: (net, country) => Math.round(net * rateFor(vat, country)) }; } formatter(): InvoiceFormatter { return { render: (seq, net, tax) => `EU-${String(seq).padStart(6, "0")} net ${amount(net)} EUR, VAT ${amount(tax)} EUR, gross ${amount(net + tax)} EUR`, }; } } class UsInvoicing implements InvoicingKit { taxCalculator(): TaxCalculator { const salesTax = { CA: 0.0725, NY: 0.04, TX: 0.0625 }; return { taxCents: (net, state) => Math.round(net * rateFor(salesTax, state)) }; } formatter(): InvoiceFormatter { return { render: (seq, net, tax) => `Invoice #${seq}: subtotal $${amount(net)}, sales tax $${amount(tax)}, total $${amount(net + tax)}`, }; } } class Billing { private readonly tax: TaxCalculator; private readonly fmt: InvoiceFormatter; private seq = 0; constructor(kit: InvoicingKit) { this.tax = kit.taxCalculator(); this.fmt = kit.formatter(); } issue(netCents: number, region: string): string { return this.fmt.render(++this.seq, netCents, this.tax.taxCents(netCents, region)); } } // Composition root: the family is chosen once. const kits: Record = { eu: new EuInvoicing(), us: new UsInvoicing() }; console.log(new Billing(kits["eu"]).issue(12000, "DE")); // EU-000001 net 120.00 EUR, VAT 22.80 EUR, gross 142.80 EUR console.log(new Billing(kits["us"]).issue(12000, "CA")); // Invoice #1: subtotal $120.00, sales tax $8.70, total $128.70 ``` ```python title="invoicing.py" from dataclasses import dataclass from typing import Protocol class TaxCalculator(Protocol): def tax_cents(self, net_cents: int, region: str) -> int: ... class InvoiceFormatter(Protocol): def render(self, seq: int, net_cents: int, tax_cents: int) -> str: ... class InvoicingKit(Protocol): def tax_calculator(self) -> TaxCalculator: ... def formatter(self) -> InvoiceFormatter: ... @dataclass(frozen=True) class RateTable: rates: dict[str, float] def tax_cents(self, net_cents: int, region: str) -> int: if region not in self.rates: raise KeyError(f"no tax rate for {region}") return round(net_cents * self.rates[region]) class EuFormatter: def render(self, seq: int, net: int, tax: int) -> str: return f"EU-{seq:06d} net {net / 100:.2f} EUR, VAT {tax / 100:.2f} EUR, gross {(net + tax) / 100:.2f} EUR" class UsFormatter: def render(self, seq: int, net: int, tax: int) -> str: return f"Invoice #{seq}: subtotal ${net / 100:.2f}, sales tax ${tax / 100:.2f}, total ${(net + tax) / 100:.2f}" class EuInvoicing: def tax_calculator(self) -> TaxCalculator: return RateTable({"DE": 0.19, "FR": 0.20, "IE": 0.23}) # VAT by country def formatter(self) -> InvoiceFormatter: return EuFormatter() class UsInvoicing: def tax_calculator(self) -> TaxCalculator: return RateTable({"CA": 0.0725, "NY": 0.04, "TX": 0.0625}) # sales tax by state def formatter(self) -> InvoiceFormatter: return UsFormatter() class Billing: def __init__(self, kit: InvoicingKit) -> None: self._tax = kit.tax_calculator() self._fmt = kit.formatter() self._seq = 0 def issue(self, net_cents: int, region: str) -> str: self._seq += 1 return self._fmt.render(self._seq, net_cents, self._tax.tax_cents(net_cents, region)) KITS: dict[str, InvoicingKit] = {"eu": EuInvoicing(), "us": UsInvoicing()} if __name__ == "__main__": print(Billing(KITS["eu"]).issue(12000, "DE")) # EU-000001 net 120.00 EUR, VAT 22.80 EUR, gross 142.80 EUR print(Billing(KITS["us"]).issue(12000, "CA")) # Invoice #1: subtotal $120.00, sales tax $8.70, total $128.70 ``` ```go title="invoicing.go" package main import ( "fmt" "math" ) type TaxCalculator interface { TaxCents(netCents int, region string) (int, error) } type InvoiceFormatter interface { Render(seq, netCents, taxCents int) string } type InvoicingKit interface { TaxCalculator() TaxCalculator Formatter() InvoiceFormatter } type RateTable map[string]float64 func (r RateTable) TaxCents(net int, region string) (int, error) { rate, ok := r[region] if !ok { return 0, fmt.Errorf("no tax rate for %s", region) } return int(math.Round(float64(net) * rate)), nil } type euFormatter struct{} func (euFormatter) Render(seq, net, tax int) string { return fmt.Sprintf("EU-%06d net %.2f EUR, VAT %.2f EUR, gross %.2f EUR", seq, float64(net)/100, float64(tax)/100, float64(net+tax)/100) } type usFormatter struct{} func (usFormatter) Render(seq, net, tax int) string { return fmt.Sprintf("Invoice #%d: subtotal $%.2f, sales tax $%.2f, total $%.2f", seq, float64(net)/100, float64(tax)/100, float64(net+tax)/100) } type EuInvoicing struct{} func (EuInvoicing) TaxCalculator() TaxCalculator { return RateTable{"DE": 0.19, "FR": 0.20, "IE": 0.23} } func (EuInvoicing) Formatter() InvoiceFormatter { return euFormatter{} } type UsInvoicing struct{} func (UsInvoicing) TaxCalculator() TaxCalculator { return RateTable{"CA": 0.0725, "NY": 0.04, "TX": 0.0625} } func (UsInvoicing) Formatter() InvoiceFormatter { return usFormatter{} } type Billing struct { tax TaxCalculator fmt InvoiceFormatter seq int } func NewBilling(kit InvoicingKit) *Billing { return &Billing{tax: kit.TaxCalculator(), fmt: kit.Formatter()} } func (b *Billing) Issue(netCents int, region string) (string, error) { tax, err := b.tax.TaxCents(netCents, region) if err != nil { return "", err } b.seq++ return b.fmt.Render(b.seq, netCents, tax), nil } func main() { kits := map[string]InvoicingKit{"eu": EuInvoicing{}, "us": UsInvoicing{}} fmt.Println(NewBilling(kits["eu"]).Issue(12000, "DE")) // EU-000001 net 120.00 EUR, VAT 22.80 EUR, gross 142.80 EUR fmt.Println(NewBilling(kits["us"]).Issue(12000, "CA")) // Invoice #1: subtotal $120.00, sales tax $8.70, total $128.70 } ``` ```java title="Invoicing.java" import java.util.Map; public class Invoicing { interface TaxCalculator { long taxCents(long netCents, String region); } interface InvoiceFormatter { String render(int seq, long netCents, long taxCents); } interface InvoicingKit { TaxCalculator taxCalculator(); InvoiceFormatter formatter(); } record RateTable(Map rates) implements TaxCalculator { public long taxCents(long net, String region) { Double rate = rates.get(region); if (rate == null) throw new IllegalArgumentException("no tax rate for " + region); return Math.round(net * rate); } } static String amount(long cents) { return String.format("%.2f", cents / 100.0); } static final class EuInvoicing implements InvoicingKit { public TaxCalculator taxCalculator() { return new RateTable(Map.of("DE", 0.19, "FR", 0.20, "IE", 0.23)); } public InvoiceFormatter formatter() { return (seq, net, tax) -> String.format("EU-%06d net %s EUR, VAT %s EUR, gross %s EUR", seq, amount(net), amount(tax), amount(net + tax)); } } static final class UsInvoicing implements InvoicingKit { public TaxCalculator taxCalculator() { return new RateTable(Map.of("CA", 0.0725, "NY", 0.04, "TX", 0.0625)); } public InvoiceFormatter formatter() { return (seq, net, tax) -> String.format("Invoice #%d: subtotal $%s, sales tax $%s, total $%s", seq, amount(net), amount(tax), amount(net + tax)); } } static final class Billing { private final TaxCalculator tax; private final InvoiceFormatter fmt; private int seq = 0; Billing(InvoicingKit kit) { this.tax = kit.taxCalculator(); this.fmt = kit.formatter(); } String issue(long netCents, String region) { return fmt.render(++seq, netCents, tax.taxCents(netCents, region)); } } public static void main(String[] args) { Map kits = Map.of("eu", new EuInvoicing(), "us", new UsInvoicing()); System.out.println(new Billing(kits.get("eu")).issue(12000, "DE")); // EU-000001 net 120.00 EUR, VAT 22.80 EUR, gross 142.80 EUR System.out.println(new Billing(kits.get("us")).issue(12000, "CA")); // Invoice #1: subtotal $120.00, sales tax $8.70, total $128.70 } } ``` ## Trade-offs **Gains** - Products from different families can't be mixed by accident. - The family decision lives in one place, and clients depend only on interfaces. - A new family is one new factory plus its products, with no client changes. - Tests can swap an entire environment by passing a different factory. **Costs** - A new product kind changes the factory interface and every implementation. - Many small types: factories times products. With one family, all of it is overhead. - An extra layer of indirection between the client and the object it uses. ## Relations - [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) creates one product. An abstract factory is often a bundle of factory methods, one per product kind. - [Builder](https://aipatterns.dev/design-patterns/builder.md) assembles one complex object step by step. Abstract Factory returns finished, related objects immediately. - [Prototype](https://aipatterns.dev/design-patterns/prototype.md) can implement a concrete factory: store one configured instance per product and clone it, instead of writing a subclass per family. - [Singleton](https://aipatterns.dev/design-patterns/singleton.md) is often (and usually needlessly) applied to concrete factories. A factory with no state can be a plain module-level value or injected once. - [Bridge](https://aipatterns.dev/design-patterns/bridge.md) can use an abstract factory to create and configure the implementation side that matches an abstraction. - [Facade](https://aipatterns.dev/design-patterns/facade.md) can replace an abstract factory when clients only need to hide how a subsystem's objects are created, not swap families. ## Agent checklist - [ ] At least two families exist in code today, each with a real implementation of every product kind. - [ ] Mixing products across families is a genuine bug, not just a style preference. - [ ] The concrete factory is chosen exactly once, at the composition root, not inside business logic. - [ ] Clients reference only product and factory interfaces. Search for leftover concrete class names and family flags. - [ ] You checked whether the existing DI setup already covers this wiring before adding a factory layer. --- # Builder > Assemble a complex object through named, step-by-step calls and validate it once in build(), instead of a constructor with many parameters. - kind: pattern - category: creational - canonical: https://aipatterns.dev/design-patterns/builder - inspired-by: https://refactoring.guru/design-patterns/builder ## Intent Separate the construction of a complex object from the object itself. A builder collects the parts through small, named calls, then checks that the combination is valid and returns a finished object, usually immutable, in a single `build()` step. ## Forces - **Many optional parts** vs **readable call sites**: `new Email(a, b, null, null, c, [], true)` hides which argument is which. - **Immutable result** vs **piecemeal assembly**: the parts arrive across loops and conditionals, but the finished object should never change. - **Validation of the whole** vs **validation per field**: some rules span fields ("at least one recipient", "total attachment size"). - **One construction process** vs **several representations**: sometimes the same steps should produce different outputs. ## Therefore **Give the object a companion builder with one method per part, and make `build()` the only way to get a finished, validated instance.** The builder is mutable and short-lived. The product is complete and, ideally, immutable. Call sites read as a list of named steps, and invalid combinations fail in one place. When the same sequence of steps is reused, put it in a *director*: a function or class that drives the builder. ## Structure ```mermaid classDiagram class EmailBuilder { -string[] to -string subject -Attachment[] attachments +addTo(address) EmailBuilder +subject(text) EmailBuilder +attach(file) EmailBuilder +build() EmailMessage } class EmailMessage { -string from -string[] to -string subject +totalBytes() int } class Attachment { -string filename -int bytes } class DigestJob { +compose(team, report) EmailMessage } DigestJob ..> EmailBuilder : drives EmailBuilder ..> EmailMessage : builds EmailMessage *-- Attachment ``` ### Participants - **Builder** (`EmailBuilder`): mutable; exposes one method per part and returns itself for chaining. - **Product** (`EmailMessage`): the finished object. Has no setters and is only created by `build()`. - **Director** (`DigestJob`, here a function `weeklyDigest`): optional; encodes a reusable sequence of builder calls. - **Client**: creates a builder, calls steps (directly or through a director), and calls `build()`. ## Use it when - A constructor has four or more parameters, several optional, and call sites pass `null` or `undefined` to skip them. - Parts are collected incrementally, in loops or conditionals, before the object is usable. - The finished object must be immutable, but constructing it in one expression is impractical. - Validation spans several fields and must happen before the object exists. - The same assembly steps must produce different representations (a MIME message and a plain-text preview, for example). ## Do not use it when - The parameters are independent and all known at the call site. The language's native optional arguments cover it: named or keyword parameters with defaults in Python, Kotlin, C# and Swift (`EmailMessage(to=[...], subject="...")`), a typed options object in TypeScript (`new EmailMessage({ to, subject })`), a struct literal or functional options in Go (`New(WithCC(...))`). Reserve Builder for step-wise accumulation or validation that spans fields. - The object has two or three required fields. A constructor is shorter and enforces them at compile time, which a builder does not. - The builder would mirror every field one to one with no validation and no accumulation. That is a second class doing the constructor's job. ## Application grill **Inspect first:** the constructor or parameter sprawl the pattern would fix, and every construction site that would have to migrate; whether a validation library (zod, class-validator, pydantic) already exists that build() should call. **Missing pieces:** validation at the trust boundary: if the object's invariants are checked nowhere, put them in build(). A validation library is justified when the same rules are enforced in several places, not for one type. **Ask before you build:** - **Scale:** how many optional parameters are real today - an options object covers modest cases without builder ceremony? - **Performance:** is the object built per element in a loop where extra allocations show? - **Reliability:** which invariants must fail loudly in build(), and which may surface later? - **Budget:** builder plus target roughly doubles the surface for one type; how many construction sites pay that back? - **Implementation:** does build() return the plain type or a validated one, and can callers still construct the target directly? ## Implementation steps 1. List the product's parts and mark which are required, which are optional, and which rules span several fields. 2. Make the product's constructor private or package-private (where the language allows) and its fields read-only. 3. Write the builder with one method per part. Collection parts get `add`-style methods; scalar parts get setters. Return the builder for chaining. 4. Put all cross-field validation in `build()`, and copy collections into the product so later builder calls can't change it. 5. Replace call sites one at a time, running tests after each. 6. If several call sites repeat the same sequence of steps, extract it into a director function. ## Example A reporting service sends weekly digests. Recipients come from a team list, the attachment is optional, and the mail provider rejects messages over 10 MB, so the size rule must be checked before sending. ```ts title="email-builder.ts" interface Attachment { filename: string; bytes: number; } const MAX_BYTES = 10 * 1024 * 1024; class EmailMessage { constructor( readonly from: string, readonly to: readonly string[], readonly subject: string, readonly body: string, readonly attachments: readonly Attachment[], ) {} totalBytes(): number { return this.attachments.reduce((sum, a) => sum + a.bytes, 0); } } class EmailBuilder { private readonly to: string[] = []; private readonly attachments: Attachment[] = []; private subjectText = ""; private bodyText = ""; constructor(private readonly from: string) {} addTo(address: string): this { this.to.push(address); return this; } subject(text: string): this { this.subjectText = text; return this; } body(text: string): this { this.bodyText = text; return this; } attach(file: Attachment): this { this.attachments.push(file); return this; } build(): EmailMessage { if (this.to.length === 0) throw new Error("at least one recipient is required"); if (!this.subjectText.trim()) throw new Error("subject is required"); const msg = new EmailMessage(this.from, [...this.to], this.subjectText, this.bodyText, [...this.attachments]); if (msg.totalBytes() > MAX_BYTES) throw new Error("attachments exceed 10 MB"); return msg; } } // Director: one reusable sequence of steps, including loops and conditionals. function weeklyDigest(team: string[], reportBytes: number | null): EmailMessage { const b = new EmailBuilder("reports@example.com").subject("Weekly usage digest").body("Numbers attached."); team.forEach((member) => b.addTo(member)); if (reportBytes !== null) b.attach({ filename: "usage.csv", bytes: reportBytes }); return b.build(); } const msg = weeklyDigest(["ana@example.com", "li@example.com"], 48_000); console.log(msg.to.length, msg.totalBytes()); // 2 48000 try { weeklyDigest([], null); } catch (e) { console.log((e as Error).message); // at least one recipient is required } ``` ```python title="email_builder.py" from dataclasses import dataclass, field MAX_BYTES = 10 * 1024 * 1024 @dataclass(frozen=True) class Attachment: filename: str bytes: int @dataclass(frozen=True) class EmailMessage: sender: str to: tuple[str, ...] subject: str body: str = "" attachments: tuple[Attachment, ...] = () def total_bytes(self) -> int: return sum(a.bytes for a in self.attachments) # Keyword arguments already name every field; the builder earns its place # only by accumulating parts step by step and validating the whole. @dataclass class EmailBuilder: sender: str _to: list[str] = field(default_factory=list) _attachments: list[Attachment] = field(default_factory=list) _subject: str = "" _body: str = "" def add_to(self, address: str) -> "EmailBuilder": self._to.append(address) return self def subject(self, text: str) -> "EmailBuilder": self._subject = text return self def body(self, text: str) -> "EmailBuilder": self._body = text return self def attach(self, file: Attachment) -> "EmailBuilder": self._attachments.append(file) return self def build(self) -> EmailMessage: if not self._to: raise ValueError("at least one recipient is required") if not self._subject.strip(): raise ValueError("subject is required") msg = EmailMessage(self.sender, tuple(self._to), self._subject, self._body, tuple(self._attachments)) if msg.total_bytes() > MAX_BYTES: raise ValueError("attachments exceed 10 MB") return msg def weekly_digest(team: list[str], report_bytes: int | None) -> EmailMessage: b = EmailBuilder("reports@example.com").subject("Weekly usage digest").body("Numbers attached.") for member in team: b.add_to(member) if report_bytes is not None: b.attach(Attachment("usage.csv", report_bytes)) return b.build() if __name__ == "__main__": msg = weekly_digest(["ana@example.com", "li@example.com"], 48_000) print(len(msg.to), msg.total_bytes()) # 2 48000 try: weekly_digest([], None) except ValueError as e: print(e) # at least one recipient is required ``` ```go title="email_builder.go" package main import ( "errors" "fmt" ) const maxBytes = 10 * 1024 * 1024 type Attachment struct { Filename string Bytes int } type EmailMessage struct { From, Subject, Body string To []string Attachments []Attachment } func (m EmailMessage) TotalBytes() int { total := 0 for _, a := range m.Attachments { total += a.Bytes } return total } type EmailBuilder struct{ msg EmailMessage } func NewEmailBuilder(from string) *EmailBuilder { return &EmailBuilder{msg: EmailMessage{From: from}} } func (b *EmailBuilder) AddTo(addr string) *EmailBuilder { b.msg.To = append(b.msg.To, addr) return b } func (b *EmailBuilder) Subject(s string) *EmailBuilder { b.msg.Subject = s return b } func (b *EmailBuilder) Body(s string) *EmailBuilder { b.msg.Body = s return b } func (b *EmailBuilder) Attach(a Attachment) *EmailBuilder { b.msg.Attachments = append(b.msg.Attachments, a) return b } // Build validates the whole message and returns a copy the builder can't mutate. func (b *EmailBuilder) Build() (EmailMessage, error) { switch { case len(b.msg.To) == 0: return EmailMessage{}, errors.New("at least one recipient is required") case b.msg.Subject == "": return EmailMessage{}, errors.New("subject is required") case b.msg.TotalBytes() > maxBytes: return EmailMessage{}, errors.New("attachments exceed 10 MB") } m := b.msg m.To = append([]string(nil), b.msg.To...) m.Attachments = append([]Attachment(nil), b.msg.Attachments...) return m, nil } func weeklyDigest(team []string, reportBytes int) (EmailMessage, error) { b := NewEmailBuilder("reports@example.com").Subject("Weekly usage digest").Body("Numbers attached.") for _, member := range team { b.AddTo(member) } if reportBytes > 0 { b.Attach(Attachment{Filename: "usage.csv", Bytes: reportBytes}) } return b.Build() } func main() { msg, _ := weeklyDigest([]string{"ana@example.com", "li@example.com"}, 48_000) fmt.Println(len(msg.To), msg.TotalBytes()) // 2 48000 _, err := weeklyDigest(nil, 0) fmt.Println(err) // at least one recipient is required } ``` ```java title="Email.java" import java.util.ArrayList; import java.util.List; public class Email { static final long MAX_BYTES = 10L * 1024 * 1024; record Attachment(String filename, long bytes) {} record EmailMessage(String from, List to, String subject, String body, List attachments) { long totalBytes() { return attachments.stream().mapToLong(Attachment::bytes).sum(); } } static final class EmailBuilder { private final String from; private final List to = new ArrayList<>(); private final List attachments = new ArrayList<>(); private String subject = ""; private String body = ""; EmailBuilder(String from) { this.from = from; } EmailBuilder addTo(String address) { to.add(address); return this; } EmailBuilder subject(String text) { subject = text; return this; } EmailBuilder body(String text) { body = text; return this; } EmailBuilder attach(Attachment file) { attachments.add(file); return this; } EmailMessage build() { if (to.isEmpty()) throw new IllegalStateException("at least one recipient is required"); if (subject.isBlank()) throw new IllegalStateException("subject is required"); // List.copyOf makes the product immune to later builder calls. var msg = new EmailMessage(from, List.copyOf(to), subject, body, List.copyOf(attachments)); if (msg.totalBytes() > MAX_BYTES) throw new IllegalStateException("attachments exceed 10 MB"); return msg; } } static EmailMessage weeklyDigest(List team, Long reportBytes) { var b = new EmailBuilder("reports@example.com").subject("Weekly usage digest").body("Numbers attached."); team.forEach(b::addTo); if (reportBytes != null) b.attach(new Attachment("usage.csv", reportBytes)); return b.build(); } public static void main(String[] args) { var msg = weeklyDigest(List.of("ana@example.com", "li@example.com"), 48_000L); System.out.println(msg.to().size() + " " + msg.totalBytes()); // 2 48000 try { weeklyDigest(List.of(), null); } catch (IllegalStateException e) { System.out.println(e.getMessage()); // at least one recipient is required } } } ``` ## Trade-offs **Gains** - Call sites name each part, so there are no positional `null` arguments. - The product can be immutable even though it is assembled piece by piece. - Cross-field validation runs once, before an invalid object can exist. - A director captures a common recipe so call sites don't repeat it. **Costs** - A second class that must stay in sync with every field of the product. - Required fields are checked at runtime in `build()`, not by the compiler. A constructor catches a missing argument earlier. - Duplicates what named arguments, options objects or functional options already give you in many languages. ## Relations - [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md) returns finished, related objects in one call. Builder assembles one object over several calls. - [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) decides which class to create. Builder decides how one instance is put together. A factory method can return a preconfigured builder. - [Composite](https://aipatterns.dev/design-patterns/composite.md) trees are a common product of builders, since nested structures are tedious to construct by hand. - [Prototype](https://aipatterns.dev/design-patterns/prototype.md) starts from a copy of an existing object. Builder starts from nothing. You can seed a builder from a prototype ("same as this message, but to a different team"). ## Agent checklist - [ ] The product has many optional parts, step-wise accumulation, or cross-field rules. Otherwise use a constructor, options object or keyword arguments. - [ ] Required parts that the compiler could enforce are constructor parameters of the builder, not optional setters. - [ ] `build()` validates the whole object and copies collections, so the product never shares mutable state with the builder. - [ ] The product has no public setters and no way to be created except through `build()`, where the language allows it. - [ ] Every rewritten call site produces an object equal to the one it produced before. Run the existing tests. --- # Prototype > Create new objects by copying a configured existing instance, so callers get a ready-made variant without knowing its class or setup. - kind: pattern - category: creational - aliases: Clone - canonical: https://aipatterns.dev/design-patterns/prototype - inspired-by: https://refactoring.guru/design-patterns/prototype ## Intent Make an object responsible for copying itself. Clients keep a set of preconfigured instances, the prototypes, and create new objects by cloning one and adjusting the copy, instead of calling a constructor and repeating the setup. ## Forces - **Expensive or detailed setup** vs **many similar objects**: rebuilding the same configuration by hand for every instance is slow and error-prone. - **Copying from outside** vs **encapsulation**: a client can't copy private fields it can't see, and shouldn't need to know them. - **Shallow copy** vs **independent copies**: copying references to nested lists and maps makes the clone and the original share state. - **Configuration known at runtime** vs **classes known at compile time**: templates loaded from config files don't map to one subclass each. ## Therefore **Give the object a `clone()` method that returns an independent copy of itself, and create variants by cloning a registered template and changing the copy.** The object knows its own fields, so it is the right place to copy them, including private ones. A small registry maps names to prototypes. Before writing `clone()` by hand, check what the language already gives you: `copy.deepcopy` in Python, copy constructors or records in Java, struct assignment plus `maps.Clone` and `slices.Clone` in Go, spread syntax or `structuredClone` in JavaScript. The pattern's value is the *template registry* idea and the guarantee of a correct deep copy, not the method name. ## Structure ```mermaid classDiagram class Prototype { <> +clone() Prototype } class AlertRule { -string name -int threshold -Map labels -string[] notify +clone() AlertRule } class RuleCatalog { -Map templates +register(key, rule) void +create(key) AlertRule } class Client { +provision(service) AlertRule } Prototype <|.. AlertRule RuleCatalog o-- Prototype : templates Client ..> RuleCatalog : asks for copies ``` ### Participants - **Prototype** (`Prototype`): declares `clone()`. Often unnecessary as a separate type when the language has a copy convention. - **Concrete prototype** (`AlertRule`): copies itself, including deep copies of mutable collections. - **Registry** (`RuleCatalog`): optional; stores named prototypes and hands out clones, never the originals. - **Client**: asks for a clone by name and customizes the copy. ## Use it when - Many objects differ from a few templates only in a handful of fields, and the templates come from configuration or user input. - Creating an object from scratch is expensive (parsing, network calls, computed defaults) but copying a ready one is cheap. - Client code must copy objects whose concrete class it doesn't know, for example items behind an interface in an editor or a workflow designer. - You want "duplicate" or "save as new" behavior and the object owns private state the caller can't reach. ## Do not use it when - The state is plain data and a copy has no rules of its own. The built-in copiers cover it: Python's `copy.copy` / `copy.deepcopy` and `dataclasses.replace`, Java records with wither methods, Go struct assignment, and JavaScript spread or `structuredClone`. Prototype earns its `clone()` only when copying needs rules they don't know - redacting fields, regenerating identity, deep-copying selected references. - The object is immutable. Share the instance; there is nothing to copy. To get a variant, create a new value with the changed field (`replace(rule, threshold=1500)`). - The object holds resources that can't be duplicated meaningfully: open connections, file handles, locks, or identity such as database primary keys. Cloning these causes subtle bugs. Use a factory that builds fresh ones. - There are only a few fixed variants known at compile time. A factory function per variant is clearer than a registry of instances. - In Java, don't reach for `Cloneable` and `Object.clone()`. They skip constructors, copy shallowly by default and need casts. Write a copy constructor or a static `copyOf`. ## Application grill **Inspect first:** the expensive setups that would be copied (configured clients, parsed config, game objects); the language's cloning support (`structuredClone`, copy constructors, clone traits); which fields hold non-copyable resources (sockets, connections, timers) today. **Missing pieces:** none beyond the language: `structuredClone` or a copy constructor. A deep-copy library only when the graph is cyclic or copying must skip typed fields. **Ask before you build:** - **Scale:** what does a copy cost versus a rebuild, measured? - **Performance:** do copies run per request or per element where allocation shows? - **Reliability:** which fields must be excluded or reset in copies (connections, caches, clock handles), and who owns that list? - **Budget:** is a prototype pool actually cheaper than constructing fresh? - **Implementation:** is the copy shallow or deep per field, and where is that documented? ## Implementation steps 1. Decide what "independent copy" means for each field: immutable values can be shared, mutable collections and nested objects must be copied, and identity fields (IDs, handles) must be reset or excluded. 2. Implement `clone()` on the class, or a copy constructor, using the language's copy facilities where they are correct. 3. Write a test that mutates every collection on the clone and asserts the original is unchanged. 4. If templates are named, add a registry that stores prototypes and always returns clones from `create()`. 5. Load or register the templates once at startup, and replace hand-repeated setup code with `create(key)` plus the field changes. ## Example A monitoring platform ships alert rule templates ("high latency", "error spike"). Teams provision rules for each service by copying a template and adjusting labels, thresholds and notification targets. Templates must never change when one team edits its copy. ```ts title="alert-rules.ts" interface Prototype { clone(): T; } class AlertRule implements Prototype { constructor( public name: string, public metric: string, public threshold: number, public forMinutes: number, public labels: Map, public notify: string[], ) {} // structuredClone would drop the class (and its methods); copy explicitly. clone(): AlertRule { const { name, metric, threshold, forMinutes } = this; return new AlertRule(name, metric, threshold, forMinutes, new Map(this.labels), [...this.notify]); } } class RuleCatalog { private readonly templates = new Map(); register(key: string, rule: AlertRule): void { this.templates.set(key, rule); } create(key: string): AlertRule { const template = this.templates.get(key); if (!template) throw new Error(`unknown template: ${key}`); return template.clone(); } } // Templates usually come from config at startup. const catalog = new RuleCatalog(); const labels = new Map([["severity", "page"], ["team", "platform"]]); catalog.register("high-latency", new AlertRule("high-latency", "http_p99_ms", 800, 5, labels, ["oncall:platform"])); const checkout = catalog.create("high-latency"); checkout.name = "checkout-latency"; checkout.labels.set("service", "checkout"); checkout.notify.push("chat:#checkout"); const search = catalog.create("high-latency"); search.name = "search-latency"; search.threshold = 1500; console.log(checkout.labels.size, checkout.notify.length); // 3 2 console.log(search.labels.size, search.notify.length, search.threshold); // 2 1 1500 ``` ```python title="alert_rules.py" import copy from dataclasses import dataclass, field @dataclass class AlertRule: name: str metric: str threshold: float for_minutes: int labels: dict[str, str] = field(default_factory=dict) notify: list[str] = field(default_factory=list) def clone(self) -> "AlertRule": # The standard library already knows how to deep-copy plain data. return copy.deepcopy(self) class RuleCatalog: def __init__(self) -> None: self._templates: dict[str, AlertRule] = {} def register(self, key: str, rule: AlertRule) -> None: self._templates[key] = rule def create(self, key: str) -> AlertRule: if key not in self._templates: raise KeyError(f"unknown template: {key}") return self._templates[key].clone() if __name__ == "__main__": catalog = RuleCatalog() catalog.register( "high-latency", AlertRule("high-latency", "http_p99_ms", 800, 5, {"severity": "page", "team": "platform"}, ["oncall:platform"]), ) checkout = catalog.create("high-latency") checkout.name = "checkout-latency" checkout.labels["service"] = "checkout" checkout.notify.append("chat:#checkout") search = catalog.create("high-latency") search.name = "search-latency" search.threshold = 1500 print(len(checkout.labels), len(checkout.notify)) # 3 2 print(len(search.labels), len(search.notify), search.threshold) # 2 1 1500 ``` ```go title="alert_rules.go" package main import ( "fmt" "maps" "slices" ) type AlertRule struct { Name, Metric string Threshold float64 ForMinutes int Labels map[string]string Notify []string } // Clone copies the struct by value, then copies the map and slice, // which would otherwise be shared with the original. func (r AlertRule) Clone() AlertRule { c := r c.Labels = maps.Clone(r.Labels) c.Notify = slices.Clone(r.Notify) return c } type RuleCatalog struct{ templates map[string]AlertRule } func NewRuleCatalog() *RuleCatalog { return &RuleCatalog{templates: map[string]AlertRule{}} } func (c *RuleCatalog) Register(key string, r AlertRule) { c.templates[key] = r } func (c *RuleCatalog) Create(key string) (AlertRule, error) { t, ok := c.templates[key] if !ok { return AlertRule{}, fmt.Errorf("unknown template: %s", key) } return t.Clone(), nil } func main() { catalog := NewRuleCatalog() catalog.Register("high-latency", AlertRule{ Name: "high-latency", Metric: "http_p99_ms", Threshold: 800, ForMinutes: 5, Labels: map[string]string{"severity": "page", "team": "platform"}, Notify: []string{"oncall:platform"}, }) checkout, _ := catalog.Create("high-latency") checkout.Name = "checkout-latency" checkout.Labels["service"] = "checkout" checkout.Notify = append(checkout.Notify, "chat:#checkout") search, _ := catalog.Create("high-latency") search.Name = "search-latency" search.Threshold = 1500 fmt.Println(len(checkout.Labels), len(checkout.Notify)) // 3 2 fmt.Println(len(search.Labels), len(search.Notify), search.Threshold) // 2 1 1500 } ``` ```java title="AlertRules.java" import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class AlertRules { interface Prototype { T copy(); } static final class AlertRule implements Prototype { String name; final String metric; double threshold; final int forMinutes; final Map labels; final List notify; AlertRule(String name, String metric, double threshold, int forMinutes, Map labels, List notify) { this.name = name; this.metric = metric; this.threshold = threshold; this.forMinutes = forMinutes; this.labels = new HashMap<>(labels); this.notify = new ArrayList<>(notify); } // A copy constructor in disguise; avoids Cloneable and its shallow default. public AlertRule copy() { return new AlertRule(name, metric, threshold, forMinutes, labels, notify); } } static final class RuleCatalog { private final Map templates = new HashMap<>(); void register(String key, AlertRule rule) { templates.put(key, rule); } AlertRule create(String key) { AlertRule t = templates.get(key); if (t == null) throw new IllegalArgumentException("unknown template: " + key); return t.copy(); } } public static void main(String[] args) { var catalog = new RuleCatalog(); catalog.register("high-latency", new AlertRule("high-latency", "http_p99_ms", 800, 5, Map.of("severity", "page", "team", "platform"), List.of("oncall:platform"))); AlertRule checkout = catalog.create("high-latency"); checkout.name = "checkout-latency"; checkout.labels.put("service", "checkout"); checkout.notify.add("chat:#checkout"); AlertRule search = catalog.create("high-latency"); search.name = "search-latency"; search.threshold = 1500; System.out.println(checkout.labels.size() + " " + checkout.notify.size()); // 3 2 System.out.println(search.labels.size() + " " + search.notify.size() + " " + search.threshold); // 2 1 1500.0 } } ``` ## Trade-offs **Gains** - New variants come from data (registered templates), not from new subclasses. - Clients copy objects without knowing their concrete class or private fields. - Expensive setup runs once per template, not once per instance. **Costs** - Correct deep copying is the whole job, and it is easy to get wrong: one forgotten collection and clones share state. - Cyclic references, shared sub-objects and identity fields need explicit rules that generic copy helpers can't infer. - Every new mutable field must be added to `clone()`. A test that mutates the copy is the only reliable guard. ## Relations - [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) needs a creator subclass per product. Prototype replaces that hierarchy with registered instances. - [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md) can store one prototype per product kind and clone them, so a new family is new data rather than a new class. - [Builder](https://aipatterns.dev/design-patterns/builder.md) assembles an object from nothing. A builder seeded from a prototype gives "like this one, but" construction with validation. - [Composite](https://aipatterns.dev/design-patterns/composite.md) trees are often cloned as a whole, which forces a recursive deep copy through every child. - [Memento](https://aipatterns.dev/design-patterns/memento.md) snapshots state to restore it later. A clone can serve as a simple memento when the object has no external resources. - [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md) is the opposite move: share one instance instead of copying it, which only works for immutable state. ## Agent checklist - [ ] You checked the language's built-in copy facilities, and used them unless they copy too shallowly or too much. - [ ] Every mutable collection and nested object is copied; immutable values and true shared services are not. - [ ] Identity fields, locks, connections and handles are reset or excluded from the copy. - [ ] A test mutates each collection on a clone and asserts the original and the registered template are unchanged. - [ ] The registry returns clones only and never exposes the stored prototypes. --- # Singleton > Guarantee one instance of a class per process with a global access point. Usually better replaced by one instance created at startup and injected. - kind: pattern - category: creational - canonical: https://aipatterns.dev/design-patterns/singleton - inspired-by: https://refactoring.guru/design-patterns/singleton ## Intent Ensure a class has exactly one instance in the process and give code a well-known way to reach it. The class itself controls its creation, so no caller can make a second copy by accident. ## Forces - **One shared resource** vs **many callers**: a metrics registry, connection pool or cache must be shared, or its data splits across copies. - **Global access** vs **visible dependencies**: reaching for `Registry.instance()` anywhere is convenient, but hides what a function depends on. - **Lazy creation** vs **thread safety**: creating on first use is cheap until two threads race to do it. - **One instance in production** vs **fresh instances in tests**: state that leaks between tests makes them order-dependent. ## Therefore **Create the one instance in one place, and hand it to the code that needs it. Use the class-enforced global accessor only at the edge, if at all.** The classic form hides the constructor and exposes a static accessor that creates the instance lazily and safely. That guarantees uniqueness, but it also makes the instance a global variable. The modern form keeps the "one instance" part and drops the "global access" part: the composition root creates the object once (or asks the accessor once) and passes it through constructors. Most languages also have a cheaper way to get one instance: a module-level value (TypeScript, Python), `sync.OnceValue` (Go), or a holder class or `enum` (Java). ## Structure ```mermaid classDiagram class MetricsSink { <> +increment(name) void } class MetricsRegistry { -MetricsRegistry instance -Map counters +instance() MetricsRegistry +increment(name) void +snapshot() Map } class CheckoutService { -MetricsSink metrics +placeOrder(id) void } class CompositionRoot { +main() void } MetricsSink <|.. MetricsRegistry CheckoutService --> MetricsSink CompositionRoot ..> MetricsRegistry : gets the instance once CompositionRoot ..> CheckoutService : injects it ``` ### Participants - **Singleton** (`MetricsRegistry`): holds the single instance and controls its creation, lazily and thread-safely. - **Abstraction** (`MetricsSink`): the narrow interface consumers depend on, so tests can pass a fake. - **Consumers** (`CheckoutService`): receive the instance through their constructor and never call the static accessor. - **Composition root** (`main`): the only place that touches the global accessor or module-level instance. ## Use it when - Exactly one instance must exist for correctness, not convenience: one process-wide metrics registry, one connection pool per database, one in-process cache whose entries must be shared. - The instance is expensive to create and must be created lazily, on first use. - A framework or plugin system offers no injection point, and the shared object has to be reachable from callbacks you don't construct. - You are wrapping a process-wide resource that is already unique: stdout, the environment, a native library handle. ## Do not use it when - You want "only one" merely because the app creates only one. Create it in `main` and inject it. Uniqueness enforced by convention at the composition root is enough, and it keeps the dependency visible. - One instance per process is enough and load-time creation is acceptable. A module-level constant (`export const metrics = new MetricsRegistry()` in TypeScript or Python) is already a per-process single instance, with no private constructor or static accessor. Singleton's controlled construction matters only when creation is expensive, must wait for runtime configuration, or a second instance would be a bug. - You use a DI container. Register the class with singleton scope (Spring beans, .NET `AddSingleton`, NestJS providers) and let the container own its lifetime. - The object holds per-request or per-user state. A singleton turns that into cross-request leakage, and in multi-tenant code into a data exposure bug. - Tests need isolation. Every `Foo.instance()` call inside business logic is a hidden global that tests can't replace without reset hooks. Pass the instance as a parameter instead. - Configuration is the "shared" thing. Parse config once and pass the values; a global config object makes every function implicitly depend on every setting. ## Application grill **Inspect first:** the shared instances today (module scope in Node is already one per process; DI containers; global mutable state); how tests reset or fake the instance now; whether "one" means one per process or one across the fleet. **Missing pieces:** none: module scope plus constructor injection replaces it. If "one" really means fleet-wide (one scheduler, one leader), that is a different problem - a database advisory lock or leader election - and this pattern does not provide it. **Ask before you build:** - **Scale:** does "one" mean per process, per instance, or across the fleet? - **Performance:** does lazy initialization stall the first request that pays for setup? - **Reliability:** when the single instance holds state, what happens on redeploy or restart - who rebuilds it? - **Budget:** what does the global state cost in test isolation (order-dependent suites, flaky CI)? - **Implementation:** how is the instance provided - import side effect or injection - and how do tests substitute it? ## Implementation steps 1. Confirm a second instance would be a bug, not just a waste. If not, stop and inject a normal instance. 2. Extract a narrow interface for what consumers actually call (`increment`), and change consumers to take it through their constructor. 3. Create the instance in one place, preferring the language's idiom: a module-level value, `sync.OnceValue`, a holder class or an `enum`. 4. If you need class-enforced uniqueness, hide the constructor and add a static accessor that creates the instance lazily and thread-safely. 5. Call the accessor only at the composition root. Search for remaining `instance()` calls in business code and replace them with injected parameters. 6. In tests, construct consumers with a fake or a fresh instance. Don't add `resetForTests()` hooks to the singleton. ## Example A web service counts business events (orders placed, payments failed) in one in-process metrics registry that a `/metrics` endpoint reads. Two registries would silently split the counts. Each version below uses its language's idiom for one instance, and `CheckoutService` receives it through its constructor. ```ts title="metrics.ts" interface MetricsSink { increment(name: string): void; } class MetricsRegistry implements MetricsSink { private static current: MetricsRegistry | undefined; private readonly counters = new Map(); private constructor() {} // Classic accessor. A module-level `export const metrics = ...` gives the same // guarantee in a module system; JavaScript is single-threaded, so no lock is needed. static instance(): MetricsRegistry { return (MetricsRegistry.current ??= new MetricsRegistry()); } increment(name: string): void { this.counters.set(name, (this.counters.get(name) ?? 0) + 1); } snapshot(): Record { return Object.fromEntries(this.counters); } } class CheckoutService { // Depends on the interface, so tests pass a fake instead of the global. constructor(private readonly metrics: MetricsSink) {} placeOrder(orderId: string): void { if (!orderId) { this.metrics.increment("orders.rejected"); return; } this.metrics.increment("orders.placed"); } } // Composition root: the only place that touches the accessor. const registry = MetricsRegistry.instance(); const checkout = new CheckoutService(registry); checkout.placeOrder("A-1001"); checkout.placeOrder("A-1002"); checkout.placeOrder(""); console.log(registry === MetricsRegistry.instance()); // true console.log(registry.snapshot()); // { 'orders.placed': 2, 'orders.rejected': 1 } // In a test: no global involved. const seen: string[] = []; new CheckoutService({ increment: (n) => seen.push(n) }).placeOrder("T-1"); console.log(seen); // [ 'orders.placed' ] ``` ```python title="metrics.py" import threading from collections import Counter from typing import Protocol class MetricsSink(Protocol): def increment(self, name: str) -> None: ... class MetricsRegistry: def __init__(self) -> None: self._lock = threading.Lock() self._counters: Counter[str] = Counter() def increment(self, name: str) -> None: with self._lock: self._counters[name] += 1 def snapshot(self) -> dict[str, int]: with self._lock: return dict(self._counters) # A module is imported once per process, so this is the single instance. # No __new__ tricks or metaclasses needed. metrics = MetricsRegistry() class CheckoutService: def __init__(self, sink: MetricsSink) -> None: self._metrics = sink def place_order(self, order_id: str) -> None: if not order_id: self._metrics.increment("orders.rejected") return self._metrics.increment("orders.placed") class RecordingSink: def __init__(self) -> None: self.seen: list[str] = [] def increment(self, name: str) -> None: self.seen.append(name) if __name__ == "__main__": checkout = CheckoutService(metrics) # composition root injects the module instance for order_id in ("A-1001", "A-1002", ""): checkout.place_order(order_id) print(metrics.snapshot()) # {'orders.placed': 2, 'orders.rejected': 1} fake = RecordingSink() # tests never touch the global CheckoutService(fake).place_order("T-1") print(fake.seen) # ['orders.placed'] ``` ```go title="metrics.go" package main import ( "fmt" "sync" ) type MetricsSink interface { Increment(name string) } type Registry struct { mu sync.Mutex counters map[string]int } func (r *Registry) Increment(name string) { r.mu.Lock() defer r.mu.Unlock() r.counters[name]++ } func (r *Registry) Snapshot() map[string]int { r.mu.Lock() defer r.mu.Unlock() out := make(map[string]int, len(r.counters)) for k, v := range r.counters { out[k] = v } return out } // DefaultRegistry is created once, on first call, safely across goroutines. var DefaultRegistry = sync.OnceValue(func() *Registry { return &Registry{counters: map[string]int{}} }) type CheckoutService struct{ metrics MetricsSink } func (s CheckoutService) PlaceOrder(orderID string) { if orderID == "" { s.metrics.Increment("orders.rejected") return } s.metrics.Increment("orders.placed") } type recordingSink struct{ seen []string } func (r *recordingSink) Increment(name string) { r.seen = append(r.seen, name) } func main() { reg := DefaultRegistry() // composition root: the only call to the accessor checkout := CheckoutService{metrics: reg} for _, id := range []string{"A-1001", "A-1002", ""} { checkout.PlaceOrder(id) } fmt.Println(reg == DefaultRegistry()) // true fmt.Println(reg.Snapshot()) // map[orders.placed:2 orders.rejected:1] fake := &recordingSink{} CheckoutService{metrics: fake}.PlaceOrder("T-1") fmt.Println(fake.seen) // [orders.placed] } ``` ```java title="Metrics.java" import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; public class Metrics { interface MetricsSink { void increment(String name); } static final class MetricsRegistry implements MetricsSink { private final ConcurrentHashMap counters = new ConcurrentHashMap<>(); private MetricsRegistry() {} // Holder idiom: the JVM loads Holder lazily and initializes it exactly once, // so this is thread-safe without locks or double-checked locking. private static final class Holder { static final MetricsRegistry INSTANCE = new MetricsRegistry(); } static MetricsRegistry instance() { return Holder.INSTANCE; } public void increment(String name) { counters.merge(name, 1L, Long::sum); } Map snapshot() { return new TreeMap<>(counters); } } static final class CheckoutService { private final MetricsSink metrics; CheckoutService(MetricsSink metrics) { this.metrics = metrics; } void placeOrder(String orderId) { metrics.increment(orderId.isEmpty() ? "orders.rejected" : "orders.placed"); } } public static void main(String[] args) { // Composition root: the only call to instance(). With Spring, a @Component // bean (singleton scope by default) replaces all of the above. MetricsRegistry registry = MetricsRegistry.instance(); var checkout = new CheckoutService(registry); for (String id : List.of("A-1001", "A-1002", "")) checkout.placeOrder(id); System.out.println(registry == MetricsRegistry.instance()); // true System.out.println(registry.snapshot()); // {orders.placed=2, orders.rejected=1} List seen = new ArrayList<>(); new CheckoutService(seen::add).placeOrder("T-1"); // tests never touch the global System.out.println(seen); // [orders.placed] } } ``` ## Trade-offs **Gains** - A second instance can't be created by mistake. - Lazy, thread-safe creation is solved once, in one place. - Every part of the process sees the same shared state. **Costs** - The accessor is a global variable: any function can depend on it without saying so in its signature. - Tests share state across cases unless every consumer takes the instance as a parameter. - It mixes two jobs, the object's real work and lifetime management, in one class. - "One per process" breaks down across worker processes, serverless instances or clustered nodes, where each gets its own copy. - Lazy initialization in languages with threads is easy to get wrong when written by hand. ## Relations - [Facade](https://aipatterns.dev/design-patterns/facade.md) objects are often needed only once, but that is a reason to create one, not to make the class a singleton. - [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md) also shares instances, but many of them, one per distinct intrinsic state. A singleton is exactly one, with mutable state allowed. - [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md) implementations, builders and prototype registries are often made singletons. Usually a module-level value or a single injected instance is enough. - [Builder](https://aipatterns.dev/design-patterns/builder.md) and [Prototype](https://aipatterns.dev/design-patterns/prototype.md) registries are similar candidates: one shared catalog, created at startup and passed around. ## Agent checklist - [ ] A second instance would cause incorrect behavior, not only extra memory. Otherwise create one instance at startup and inject it. - [ ] You used the language idiom (module-level value, `sync.OnceValue`, holder class, `enum`, or DI singleton scope) rather than hand-rolled locking. - [ ] Business code receives the instance through a constructor or parameter; the static accessor is called only at the composition root. - [ ] The singleton holds no per-request, per-user or per-tenant state. - [ ] Tests construct consumers with a fake or fresh instance and need no reset hook on the singleton. --- # Cache-Aside > Read through a cache: on a miss, load from the store and fill it; on a write, save to the store and evict the entry - so reads speed up while the store stays the only source of truth. - kind: pattern - category: data - aliases: Lazy Loading, Look-Aside Cache - canonical: https://aipatterns.dev/design-patterns/cache-aside - inspired-by: https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside ## Intent Keep a cache beside the store of record, not inside it. On a read, check the cache first; on a miss, load from the store, put the value in the cache, and return it. On a write, save to the store and drop the cache entry, so the next read reloads the fresh value. The cache is an accelerator that can be emptied at any time; the store remains the only source of truth. ## Forces - **Read speed** vs **write cost**: every cached read saves a query; every write now has a second thing to keep honest. - **Staleness** vs **consistency**: a cached answer can be out of date until its entry expires or is evicted. - **Eager filling** vs **wasted work**: pre-warming the cache with everything costs memory and effort for values that may never be read again. - **Two copies** vs **one truth**: once the cache holds data, someone will eventually ask which copy is right. ## Therefore **On a miss, load from the store and fill the cache; on a write, update the store and evict - never update both and hope they stay in step.** The application code owns the coordination: check the cache, fall back to the store, fill on the way out. The cache is disposable by construction - empty it and the system is slower, never wrong. Writes go to the store alone and invalidate the entry, because the alternative (writing to both) turns every failure between the two into silent disagreement. ## Structure ```mermaid classDiagram class PricingService { -Cache cache -PricingStore store +priceFor(sku) Price +reprice(sku, cents) } class Cache { +get(key) Price +put(key, price) +evict(key) } class PricingStore { +load(sku) Price +save(sku, cents) } PricingService --> Cache : reads, fills, evicts PricingService --> PricingStore : loads on miss, saves on write ``` ### Participants - **Coordinator** (`PricingService`): the application code the pattern lives in. Implements the read path (cache, then store, then fill) and the write path (store, then evict). - **Cache** (`Cache`): a keyed, expirable store - Redis, Memcached, or an in-process map. Holds entries with a TTL; losing everything is acceptable. - **Store** (`PricingStore`): the source of truth - the database. Every fact in the cache can be regenerated from it. - **Clock** (`now`): injected so TTL expiry is testable without sleeping. ## Use it when - Reads dominate writes on data that is expensive to compute or far away: product pages, user profiles, configuration, an LLM response keyed by prompt hash. - Slightly stale answers are acceptable within a bounded window - a price that is at most sixty seconds old. - The working set is bigger than the cache, so pre-warming everything would waste memory; entries should earn their place by being read. - You already have a cache (Redis, a CDN, an in-process map) and a store, and need the rule that keeps them from drifting apart. ## Do not use it when - The read must reflect a write made moments ago in the same session - showing a user their just-saved settings from a stale cache is a bug. Bypass the cache for that read, or key the cache per session. - The value must always be current for correctness - stock levels at checkout, an account balance before a debit. Read the store; cache only immutable or slowly changing derivatives. - Writes are as frequent as reads. Every write pays an eviction and the hit rate never covers it; write to the store alone. - The underlying query is already fast, measured. A cache adds a second source of truth and an invalidation surface to save milliseconds you have not proven you need. - You are in one process with the data already in memory. A map beside your map is not a cache layer, it is a bug incubator; reach for this when the store is remote or the computation is expensive. ## Application grill **Inspect first:** the read paths the pattern would accelerate and what they cost today (a database hit per request, N+1 loops, remote calls); whether a cache already exists - search for `redis`, `ioredis`, `memcached`, `lru-cache`, `node-cache`, framework caches (`unstable_cache`), `Cache-Control` headers, and in-process maps near the store; the deploy target and instance count, which decide whether one per-process cache is enough. **Missing pieces:** with no cache in the codebase, the pattern needs one. An in-process LRU (a size-capped map) is free to run but per-instance: two instances cache twice and invalidate independently. Managed Redis (Upstash, ElastiCache, Memorystore, or the platform's Redis/KV binding) is shared across instances and adds a service to pay for, secure and monitor. A CDN may already cache whole responses for anonymous traffic - check before adding an application cache. **Ask before you build:** - **Scale:** how many reads per second land on the hot path, and how large is the working set that would live in the cache? - **Performance:** what latency is being bought, and how stale may an answer be (the TTL)? - **Reliability:** is "slower, never wrong" the agreed failure mode when the cache is down, and can the store survive a hot-key stampede on expiry? - **Budget:** pay for a managed cache now, or start in-process and accept per-instance scope? - **Implementation:** who owns invalidation on writes, and which single read path becomes the coordinator? ## Implementation steps 1. Name the cache key precisely: the query and its parameters (`price:{sku}`), not the entity (`product:{id}`), when different queries need different shapes. 2. Write the read path in one place: get from cache; on miss, load from the store, put with a TTL, return. Every reader goes through it - a second read path is an uncacheable read path. 3. Write the write path in the same place: save to the store, then evict the key. Evict, do not overwrite - the store's write is the fact; the cache entry is only a speedup. 4. Give every entry a TTL even though writes evict, so bugs, rollbacks and manual edits age out instead of living forever. 5. Decide the miss storm policy before you need it: when a hot key expires, many readers miss at once. Fill under a per-key lock, or accept the thundering herd because the store can take it. 6. Measure the hit rate. A cache below roughly half hits is cost without payoff; reconsider the key or the TTL. 7. Log evictions and misses at the key level while integrating, then drop to metrics - the first production bug in every cache is a wrong key. ## Example A storefront reads prices on every page view and reprices a few times a day. The first read misses and loads; the second is served from cache; a reprice saves to the store and evicts, so the next read reloads the fresh value; after the TTL expires, even an unrepriced entry reloads. ```ts title="pricing.ts" type Price = { cents: number }; interface PricingStore { load(sku: string): Price; save(sku: string, cents: number): void; } /** The store of record: counts loads so the demo can prove caching happened. */ class SlowDatabase implements PricingStore { loads = 0; private prices = new Map([["tea-1", 450]]); load(sku: string): Price { this.loads++; return { cents: this.prices.get(sku) ?? 0 }; } save(sku: string, cents: number): void { this.prices.set(sku, cents); } } class Cache { private entries = new Map(); constructor( private readonly ttlMs: number, private readonly now: () => number, ) {} get(key: string): Price | undefined { const e = this.entries.get(key); if (!e) return undefined; if (this.now() - e.at >= this.ttlMs) { this.entries.delete(key); // expired: a miss, not a stale answer return undefined; } return e.value; } put(key: string, value: Price): void { this.entries.set(key, { value, at: this.now() }); } evict(key: string): void { this.entries.delete(key); } } class PricingService { constructor( private readonly store: PricingStore, private readonly cache: Cache, ) {} priceFor(sku: string): Price { const key = `price:${sku}`; const hit = this.cache.get(key); if (hit) return hit; // fast path const price = this.store.load(sku); // miss: the store is the truth this.cache.put(key, price); // fill on the way out return price; } reprice(sku: string, cents: number): void { this.store.save(sku, cents); // write the fact this.cache.evict(`price:${sku}`); // drop the stale copy, never overwrite it } } const store = new SlowDatabase(); let clock = 0; const service = new PricingService(store, new Cache(1_000, () => clock)); service.priceFor("tea-1"); // miss -> load 1 service.priceFor("tea-1"); // hit console.log(`loads after two reads: ${store.loads}`); // 1 service.reprice("tea-1", 475); // save + evict console.log(service.priceFor("tea-1").cents); // 475, load 2: fresh, not overwritten clock += 2_000; // TTL (1 s) has passed service.priceFor("tea-1"); // expired -> load 3 console.log(`loads after expiry: ${store.loads}`); // 3 ``` ```python title="pricing.py" from dataclasses import dataclass from typing import Callable, Protocol @dataclass class Price: cents: int class PricingStore(Protocol): def load(self, sku: str) -> Price: ... def save(self, sku: str, cents: int) -> None: ... class SlowDatabase: """The store of record: counts loads so the demo can prove caching happened.""" def __init__(self) -> None: self.loads = 0 self._prices = {"tea-1": 450} def load(self, sku: str) -> Price: self.loads += 1 return Price(cents=self._prices.get(sku, 0)) def save(self, sku: str, cents: int) -> None: self._prices[sku] = cents class Cache: def __init__(self, ttl_ms: int, now: Callable[[], int]) -> None: self._ttl, self._now = ttl_ms, now self._entries: dict[str, tuple[Price, int]] = {} def get(self, key: str) -> Price | None: entry = self._entries.get(key) if entry is None: return None price, at = entry if self._now() - at >= self._ttl: del self._entries[key] # expired: a miss, not a stale answer return None return price def put(self, key: str, value: Price) -> None: self._entries[key] = (value, self._now()) def evict(self, key: str) -> None: self._entries.pop(key, None) class PricingService: def __init__(self, store: PricingStore, cache: Cache) -> None: self._store, self._cache = store, cache def price_for(self, sku: str) -> Price: key = f"price:{sku}" hit = self._cache.get(key) if hit is not None: return hit # fast path price = self._store.load(sku) # miss: the store is the truth self._cache.put(key, price) # fill on the way out return price def reprice(self, sku: str, cents: int) -> None: self._store.save(sku, cents) # write the fact self._cache.evict(f"price:{sku}") # drop the stale copy, never overwrite it store = SlowDatabase() clock = 0 now = lambda: clock # noqa: E731 - injected clock; reassigning moves time for both service = PricingService(store, Cache(ttl_ms=1_000, now=now)) service.price_for("tea-1") # miss -> load 1 service.price_for("tea-1") # hit print(f"loads after two reads: {store.loads}") # 1 service.reprice("tea-1", 475) # save + evict print(service.price_for("tea-1").cents) # 475, load 2: fresh, not overwritten clock += 2_000 # TTL (1 s) has passed service.price_for("tea-1") # expired -> load 3 print(f"loads after expiry: {store.loads}") # 3 ``` ```go title="pricing.go" package main import "fmt" type Price struct{ Cents int } // PricingStore is the store of record. type PricingStore interface { Load(sku string) Price Save(sku string, cents int) } // SlowDatabase counts loads so the demo can prove caching happened. type SlowDatabase struct { loads int prices map[string]int } func NewSlowDatabase() *SlowDatabase { return &SlowDatabase{prices: map[string]int{"tea-1": 450}} } func (d *SlowDatabase) Load(sku string) Price { d.loads++ return Price{Cents: d.prices[sku]} } func (d *SlowDatabase) Save(sku string, cents int) { d.prices[sku] = cents } type cacheEntry struct { value Price at int } type Cache struct { ttlMs int now func() int entries map[string]cacheEntry } func NewCache(ttlMs int, now func() int) *Cache { return &Cache{ttlMs: ttlMs, now: now, entries: map[string]cacheEntry{}} } func (c *Cache) Get(key string) (Price, bool) { e, ok := c.entries[key] if !ok { return Price{}, false } if c.now()-e.at >= c.ttlMs { delete(c.entries, key) // expired: a miss, not a stale answer return Price{}, false } return e.value, true } func (c *Cache) Put(key string, value Price) { c.entries[key] = cacheEntry{value: value, at: c.now()} } func (c *Cache) Evict(key string) { delete(c.entries, key) } type PricingService struct { store PricingStore cache *Cache } func NewPricingService(store PricingStore, cache *Cache) *PricingService { return &PricingService{store: store, cache: cache} } func (s *PricingService) PriceFor(sku string) Price { key := "price:" + sku if hit, ok := s.cache.Get(key); ok { return hit // fast path } price := s.store.Load(sku) // miss: the store is the truth s.cache.Put(key, price) // fill on the way out return price } func (s *PricingService) Reprice(sku string, cents int) { s.store.Save(sku, cents) // write the fact s.cache.Evict("price:" + sku) // drop the stale copy, never overwrite it } func main() { store := NewSlowDatabase() clock := 0 now := func() int { return clock } service := NewPricingService(store, NewCache(1_000, now)) service.PriceFor("tea-1") // miss -> load 1 service.PriceFor("tea-1") // hit fmt.Printf("loads after two reads: %d\n", store.loads) // 1 service.Reprice("tea-1", 475) // save + evict fmt.Println(service.PriceFor("tea-1").Cents) // 475, load 2: fresh, not overwritten clock += 2_000 // TTL (1 s) has passed service.PriceFor("tea-1") // expired -> load 3 fmt.Printf("loads after expiry: %d\n", store.loads) // 3 } ``` ```java title="Pricing.java" import java.util.HashMap; import java.util.Map; import java.util.function.IntSupplier; public class Pricing { record Price(int cents) {} interface PricingStore { Price load(String sku); void save(String sku, int cents); } /** The store of record: counts loads so the demo can prove caching happened. */ static final class SlowDatabase implements PricingStore { int loads = 0; private final Map prices = new HashMap<>(Map.of("tea-1", 450)); public Price load(String sku) { loads++; return new Price(prices.getOrDefault(sku, 0)); } public void save(String sku, int cents) { prices.put(sku, cents); } } static final class Cache { private record Entry(Price value, long at) {} private final long ttlMs; private final IntSupplier now; private final Map entries = new HashMap<>(); Cache(long ttlMs, IntSupplier now) { this.ttlMs = ttlMs; this.now = now; } Price get(String key) { Entry e = entries.get(key); if (e == null) return null; if (now.getAsLong() - e.at() >= ttlMs) { entries.remove(key); // expired: a miss, not a stale answer return null; } return e.value(); } void put(String key, Price value) { entries.put(key, new Entry(value, now.getAsLong())); } void evict(String key) { entries.remove(key); } } static final class PricingService { private final PricingStore store; private final Cache cache; PricingService(PricingStore store, Cache cache) { this.store = store; this.cache = cache; } Price priceFor(String sku) { String key = "price:" + sku; Price hit = cache.get(key); if (hit != null) return hit; // fast path Price price = store.load(sku); // miss: the store is the truth cache.put(key, price); // fill on the way out return price; } void reprice(String sku, int cents) { store.save(sku, cents); // write the fact cache.evict("price:" + sku); // drop the stale copy, never overwrite it } } public static void main(String[] args) { SlowDatabase store = new SlowDatabase(); long[] clock = {0}; PricingService service = new PricingService(store, new Cache(1_000, () -> clock[0])); service.priceFor("tea-1"); // miss -> load 1 service.priceFor("tea-1"); // hit System.out.println("loads after two reads: " + store.loads); // 1 service.reprice("tea-1", 475); // save + evict System.out.println(service.priceFor("tea-1").cents()); // 475, load 2: fresh clock[0] += 2_000; // TTL (1 s) has passed service.priceFor("tea-1"); // expired -> load 3 System.out.println("loads after expiry: " + store.loads); // 3 } } ``` Under concurrency the same rule holds with one addition: the write path saves and evicts as one logical step, and a read that filled the cache before the eviction landed may briefly serve the old value - that is the staleness window the TTL bounds, not a bug to chase. ## Trade-offs **Gains** - Reads stop paying the store's cost; the cache absorbs the hot path. - The store stays the only source of truth - empty the cache and nothing is wrong, just slow. - Failures degrade in the safe direction: a cache outage means slower reads, never wrong data. **Costs** - Answers are stale by up to the TTL; read-your-writes needs a bypass or per-session keys. - Every write now has two steps to keep ordered, and the invalidation is the part everyone forgets. - Hot-key expiry can stampede the store; per-key fill locking adds real complexity. - A second datastore to operate, monitor and evict. ## Relations - [Proxy](https://aipatterns.dev/design-patterns/proxy.md): the coordinator exposes the store's read interface and adds caching in front - the same shape, but the pattern's point is the invalidation rule, not the wrapping. - [Fallback](https://aipatterns.dev/design-patterns/fallback.md): a stale cached answer served during a store outage is the most useful fallback there is - the cache you already have doubles as the degraded mode. - [Strategy](https://aipatterns.dev/design-patterns/strategy.md): eviction and TTL policies vary (LRU, LFU, per-key TTL); swap them without touching the read path. - [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md): both avoid repeating expensive work by sharing, but flyweight shares objects inside one process to save memory, while cache-aside shares computed answers across processes to save loads. ## Agent checklist - [ ] Reads and writes go through one coordinator each; no second read path skips the cache. - [ ] Writes save to the store and evict; nothing overwrites a cache entry directly. - [ ] Every entry has a TTL, and expiry is a miss rather than a stale serve. - [ ] The cache key covers everything the value depends on. - [ ] The hot-key expiry path is decided (lock or accept), and the hit rate is measured. --- # Transactional Outbox > Write the outgoing message in the same transaction as the business change; a relay publishes and marks it sent - the commit and the message become one fact, not two hopes. - kind: pattern - category: data - aliases: Outbox - canonical: https://aipatterns.dev/design-patterns/transactional-outbox - inspired-by: https://microservices.io/patterns/data/transactional-outbox.html ## Intent A service must change its database and tell the world - publish an event, send an email, call a webhook. Doing both as separate operations is the dual-write bug: send mid-transaction and you may announce data that never committed; send after commit and a crash in between loses the message forever. The fix is to write the message into an outbox table inside the same transaction as the business change, and let a separate relay publish it afterwards. ## Forces - **Atomicity** vs **two systems**: the database commits atomically; the message broker does not participate, so the message must become database data first. - **Deliver once** vs **deliver at least once**: the relay cannot atomically publish and mark-sent, so it chooses between risking duplicates or risking loss - and duplicates are the only survivable choice. - **Immediate publish** vs **orderly publish**: polling adds latency; reacting to the transaction log is fast but is infrastructure you now operate. - **Service simplicity** vs **relay machinery**: the pattern removes the bug from business code and concentrates it in one relay everyone can inspect. ## Therefore **In the same transaction, write the business row and an outbox row; afterwards, a relay reads unmarked outbox rows, publishes each, and marks it sent - and every consumer tolerates redelivery.** The outbox row is the message: its insertion commits with the business fact, so "database committed" and "message exists" become one event. The relay publishes and then marks - if it dies between the two, the message is published again on restart. That makes publishing at-least-once by construction, which is why this pattern drags in its sibling: consumers must be idempotent ([Idempotent Consumer](https://aipatterns.dev/design-patterns/idempotent-consumer.md)). ## Structure ```mermaid flowchart TD A[Business transaction] --> B[Write business row] A --> C[Write outbox row] B --> D[Commit] C --> D D --> E[Relay polls unmarked rows] E --> F[Publish message] F --> G[Mark row sent] F -- relay dies here --> E ``` ### Participants - **Service transaction** (`placeOrder`): writes the order and the outbox row, commits once. Never calls the broker directly. - **Outbox** (`outbox` table): rows with the message payload, the destination, and a `sent` marker. Read by the relay, written only inside business transactions. - **Relay** (`Relay`): polls unmarked rows in order, publishes each, marks it sent. Small, stateless apart from its position, and the only component that talks to the broker. - **Consumer**: receives messages at least once and deduplicates on the message id. ## Use it when - A state change must reliably produce a message: `user.created` after the insert, `order.shipped` after the status update, a webhook after a subscription change. - The alternative is sending inside or right after the transaction - the first announces uncommitted data, the second loses messages on crash. - You own the writing service and can add a table; the fix is one table and one loop, not new infrastructure. - Consumers can deduplicate, or the message can carry an idempotency key ([Idempotent Consumer](https://aipatterns.dev/design-patterns/idempotent-consumer.md)). ## Do not use it when - The write and the notification live in the same database transaction - a trigger or an in-transaction listener already gives you atomicity; a relay would be latency for nothing. - The message must go out within milliseconds, synchronously with the commit. Polling lag is part of the contract; if that is unacceptable, buy or operate log-tailing CDC and say so explicitly. - There is no message: if "notify" just means updating another table in the same database, do it in the same transaction and skip the pattern. - The business change is itself a saga step - the step's result message should go through this pattern, but the saga owns the ordering decisions ([Saga](https://aipatterns.dev/design-patterns/saga.md)). ## Application grill **Inspect first:** the dual-write sites (database commit plus publish or external call) and today's loss window between them; the database's capabilities (Postgres and MySQL support SKIP LOCKED; DynamoDB has streams); existing queues and any CDC tooling (Debezium) already running in the organization. **Missing pieces:** an outbox table in the existing database - no new infrastructure on the write side. A relay to publish: cron polling with SELECT ... FOR UPDATE SKIP LOCKED is the smallest reliable relay; a queue service receives the events; Debezium or streams is the high-throughput option with a Kafka-grade operational bill. Consumers receive at-least-once and must deduplicate (idempotent-consumer). **Ask before you build:** - **Scale:** what event rate and what publish lag is acceptable - is a one-second poll enough, or does CDC earn its complexity? - **Performance:** what does relay polling cost the primary database at peak? - **Reliability:** the relay crashes mid-publish - is at-least-once downstream accepted, and is per-aggregate ordering preserved? - **Budget:** cron plus queue versus a CDC platform - whose team operates each? - **Implementation:** who guarantees consumer idempotency, and where do poison messages land (dead letters)? ## Implementation steps 1. Add the outbox table: id, destination, payload, `created_at`, `sent` marker (or a `sent_at` null). One table per service, not one shared outbox. 2. In every code path that changes business state and must notify, write the outbox row inside the same transaction. This is the whole pattern - one code review rule: no broker calls from business transactions. 3. Write the message payload at write time, not "at send time" - the message must reflect exactly what committed, or the relay becomes a second brain re-deriving facts. 4. Implement the relay: select unmarked rows in `created_at` order (batch, e.g. 100), publish each, mark sent. One relay instance at a time per destination, or partition rows so two relays cannot race. 5. Accept at-least-once: if the relay dies after publishing but before marking, the row publishes again. Put the message id in every payload and require consumers to deduplicate. 6. Order within reason: per-entity ordering only. Global ordering across all entities is a queueing system, not an outbox. 7. Monitor the outbox age - oldest unsent row - not just the relay's liveness. A stuck relay is silent until someone asks why the downstream never heard about yesterday's orders. ## Example Placing an order writes the order and its `order.placed` outbox row in one commit. The relay then publishes and marks. The demo also shows the failure this exists for: a crash between commit and publish loses nothing, because the message is already a committed row. ```ts title="orders.ts" type OutboxRow = { id: number; event: string; payload: string; sent: boolean }; // The database: orders and outbox commit together or not at all. // begin()/commit() stand in for a real transaction's BEGIN/COMMIT. class Database { orders: string[] = []; outbox: OutboxRow[] = []; private nextId = 1; begin() {} commit() {} } class OrderService { constructor(private readonly db: Database) {} placeOrder(sku: string): string { this.db.begin(); const orderId = `o-${this.db.orders.length + 1}`; this.db.orders.push(orderId); // Same transaction: the message is data, not a side effect. this.db.outbox.push({ id: this.db.nextId++, event: "order.placed", payload: JSON.stringify({ orderId, sku }), sent: false, }); this.db.commit(); return orderId; } } const published: string[] = []; class Relay { constructor(private readonly db: Database) {} tick(): void { for (const row of this.db.outbox) { if (row.sent) continue; published.push(`${row.event}: ${row.payload}`); row.sent = true; // die here and it publishes again - consumers dedupe } } } const db = new Database(); const orders = new OrderService(db); const relay = new Relay(db); const id = orders.placeOrder("tea-1"); console.log(`placed ${id}; outbox rows: ${db.outbox.length}`); // 1 - committed together relay.tick(); console.log(`published: ${published}`); // ["order.placed: {\"orderId\":\"o-1\",...}"] relay.tick(); // marked rows are skipped - no duplicate from a healthy relay // The bug this pattern removes: a crash after commit, before any publish. const crashed = new Database(); const crashedOrders = new OrderService(crashed); crashedOrders.placeOrder("tea-2"); // committed, relay never ran new Relay(crashed).tick(); // restart: the message is still there, and goes out console.log(`recovered publish count: ${published.length + 1}`); // nothing lost ``` ```python title="orders.py" import json from dataclasses import dataclass, field @dataclass class OutboxRow: id: int event: str payload: str sent: bool = False @dataclass class Database: """Orders and outbox commit together or not at all.""" orders: list[str] = field(default_factory=list) outbox: list[OutboxRow] = field(default_factory=list) _next_id: int = 1 _tx_rows: list[OutboxRow] = field(default_factory=list) class OrderService: def __init__(self, db: Database) -> None: self._db = db def place_order(self, sku: str) -> str: order_id = f"o-{len(self._db.orders) + 1}" self._db.orders.append(order_id) # Same transaction: the message is data, not a side effect. self._db.outbox.append( OutboxRow( id=self._db._next_id, event="order.placed", payload=json.dumps({"orderId": order_id, "sku": sku}), ) ) self._db._next_id += 1 return order_id published: list[str] = [] class Relay: def __init__(self, db: Database) -> None: self._db = db def tick(self) -> None: for row in self._db.outbox: if row.sent: continue published.append(f"{row.event}: {row.payload}") row.sent = True # die here and it publishes again - consumers dedupe db = Database() orders = OrderService(db) relay = Relay(db) order_id = orders.place_order("tea-1") print(f"placed {order_id}; outbox rows: {len(db.outbox)}") # 1 - committed together relay.tick() print(f"published: {published}") # ['order.placed: {"orderId": "o-1", ...}'] relay.tick() # marked rows are skipped - no duplicate from a healthy relay # The bug this pattern removes: a crash after commit, before any publish. crashed_db = Database() crashed_orders = OrderService(crashed_db) crashed_orders.place_order("tea-2") # committed, relay never ran Relay(crashed_db).tick() # restart: the message is still there, and goes out print(f"recovered publish count: {len(published) + 1}") # nothing lost ``` ```go title="orders.go" package main import ( "fmt" ) type OutboxRow struct { ID int Event string Payload string Sent bool } // Database: orders and outbox commit together or not at all. type Database struct { Orders []string Outbox []OutboxRow nextID int } type OrderService struct{ db *Database } func (s OrderService) PlaceOrder(sku string) string { id := fmt.Sprintf("o-%d", len(s.db.Orders)+1) s.db.Orders = append(s.db.Orders, id) // Same transaction: the message is data, not a side effect. s.db.nextID++ s.db.Outbox = append(s.db.Outbox, OutboxRow{ ID: s.db.nextID, Event: "order.placed", Payload: fmt.Sprintf(`{"orderId":%q,"sku":%q}`, id, sku), }) return id } var published []string type Relay struct{ db *Database } func (r Relay) Tick() { for i := range r.db.Outbox { row := &r.db.Outbox[i] if row.Sent { continue } published = append(published, row.Event+": "+row.Payload) row.Sent = true // die here and it publishes again - consumers dedupe } } func main() { db := &Database{} orders := OrderService{db} relay := Relay{db} id := orders.PlaceOrder("tea-1") fmt.Printf("placed %s; outbox rows: %d\n", id, len(db.Outbox)) // 1 - committed together relay.Tick() fmt.Printf("published: %v\n", published) relay.Tick() // marked rows are skipped - no duplicate from a healthy relay // The bug this pattern removes: a crash after commit, before any publish. crashed := &Database{} crashedOrders := OrderService{crashed} crashedOrders.PlaceOrder("tea-2") // committed, relay never ran Relay{crashed}.Tick() // restart: the message is still there, and goes out fmt.Printf("recovered publish count: %d\n", len(published)+1) // nothing lost } ``` ```java title="Orders.java" import java.util.ArrayList; import java.util.List; public class Orders { record OutboxRow(int id, String event, String payload, boolean sent) {} // The database: orders and outbox commit together or not at all. static final class Database { final List orders = new ArrayList<>(); final List outbox = new ArrayList<>(); private int nextId = 1; } record OrderService(Database db) { String placeOrder(String sku) { String orderId = "o-" + (db.orders.size() + 1); db.orders.add(orderId); // Same transaction: the message is data, not a side effect. db.outbox.add(new OutboxRow(db.nextId++, "order.placed", "{\"orderId\":\"" + orderId + "\",\"sku\":\"" + sku + "\"}", false)); return orderId; } } static final List published = new ArrayList<>(); record Relay(Database db) { void tick() { for (int i = 0; i < db.outbox.size(); i++) { OutboxRow row = db.outbox.get(i); if (row.sent()) continue; published.add(row.event() + ": " + row.payload()); db.outbox.set(i, new OutboxRow(row.id(), row.event(), row.payload(), true)); // die here and it publishes again - consumers dedupe } } } public static void main(String[] args) { Database db = new Database(); OrderService orders = new OrderService(db); Relay relay = new Relay(db); String id = orders.placeOrder("tea-1"); System.out.printf("placed %s; outbox rows: %d%n", id, db.outbox.size()); // 1 relay.tick(); System.out.println("published: " + published); relay.tick(); // marked rows are skipped - no duplicate from a healthy relay // The bug this pattern removes: a crash after commit, before any publish. Database crashed = new Database(); new OrderService(crashed).placeOrder("tea-2"); // committed, relay never ran new Relay(crashed).tick(); // restart: the message is still there, and goes out System.out.println("recovered publish count: " + (published.size() + 1)); } } ``` The demo database collapses the transaction into "rows are there or they are not"; in a real store the order insert and outbox insert sit inside one `BEGIN … COMMIT`, which is what makes the guarantee real. ## Trade-offs **Gains** - The database commit and the message become one atomic fact - the dual-write bug is gone by construction. - Business code never touches the broker; failure to publish is one inspectable relay, not scattered `try/except`. - The outbox is an audit log for free: every message the service owes the world is a row. **Costs** - Delivery is at-least-once; every consumer must deduplicate or every message must carry an idempotency key. - Polling adds latency between commit and publish; log-tailing CDC removes it at the price of operating CDC. - The relay needs its own discipline: one instance or partitioned rows, ordered reads, age monitoring. - Message payloads are written at business time - schema evolution means old outbox rows with old payloads. ## Relations - [Idempotent Consumer](https://aipatterns.dev/design-patterns/idempotent-consumer.md): the mandatory sibling - the relay's at-least-once delivery is only safe onto consumers that deduplicate. - [Saga](https://aipatterns.dev/design-patterns/saga.md): saga steps announce their results through this pattern, so a step's state change and its message commit together. - [Retry](https://aipatterns.dev/design-patterns/retry.md): the relay is a retry loop with a persistent cursor; transient publish failures re-publish the same row later, which is exactly the contract. - [Command](https://aipatterns.dev/design-patterns/command.md): the outbox row is a command to the outside world - a persisted unit of work someone else will execute. ## Agent checklist - [ ] The outbox row is written inside the same transaction as the business change - no broker calls from business code. - [ ] The payload is computed at write time from committed data. - [ ] The relay publishes then marks, tolerates its own crash, and runs single-instance or partitioned. - [ ] Every message carries an id, and consumers are known to deduplicate. - [ ] Outbox age (oldest unsent row) is monitored, not just relay liveness. --- # Saga > Run a multi-step transaction as local steps, each with a compensation; on failure, compensate the completed steps in reverse - consistency by explicit undo, not locks. - kind: pattern - category: data - aliases: Compensating Transaction - canonical: https://aipatterns.dev/design-patterns/saga - inspired-by: https://microservices.io/patterns/data/saga.html ## Intent A workflow that spans services or non-transactional systems cannot share one database transaction. A saga splits it into a sequence of steps, each committing locally, and gives every step a compensation - the operation that semantically undoes it. If a step fails, the saga runs the compensations of all completed steps in reverse order. The outcome is either every step done, or every effect undone: consistency by construction, with no locks held across services. ## Forces - **Atomicity** vs **autonomy**: each service owns its data and commits immediately, so intermediate states are visible to everyone until the saga finishes. - **Rollback** vs **compensation**: databases undo; the world does not - a sent email has no `unsend`, so compensations must be designed per step, not assumed. - **Isolation** vs **concurrency**: concurrent sagas can see each other's half-done work; without countermeasures, one saga reads a total another saga will later refund. - **Coordination** vs **coupling**: a central orchestrator sees the whole flow and its failures; pure choreography couples every service to every event. ## Therefore **Write each step as an action plus a compensation, execute steps in order, and on the first failure run the compensations of the completed steps in reverse - then record the outcome so the saga's result is queryable.** Two coordination styles exist: orchestrated, where one component drives the steps and owns the failure path (the shape below), and choreographed, where services react to each other's events. Start orchestrated - the failure path is the hard part, and one place that owns it is easier to make correct than five. Persist the saga's state with [Command](https://aipatterns.dev/design-patterns/command.md)-style records so a crashed process resumes instead of re-running side effects. ## Structure ```mermaid sequenceDiagram participant Saga participant Stock participant Card participant Email Saga->>Stock: reserve(order) Stock-->>Saga: reservation Saga->>Card: charge(order) Card-->>Saga: receipt Saga->>Email: schedule confirmation Email-->>Saga: delivery failed Saga->>Card: refund(receipt) Saga->>Stock: release(reservation) ``` ### Participants - **Saga** (`CheckoutSaga`): owns the step list and the failure path. Runs actions in order; on failure, runs compensations in reverse and records the outcome. - **Step** (`Step`): one action plus its compensation. The compensation takes the action's result - refunding needs the receipt, releasing needs the reservation. - **Local services** (`Stock`, `Card`, `Email`): each commits its own step immediately. They know nothing about the saga. - **Saga log** (in the demo, `outcome`): the persisted record of what completed and what was compensated, so the result is auditable and resumable. ## Use it when - A workflow spans services or systems that cannot share a transaction: reserve stock, charge a card, send a confirmation. - You can define a compensation for every step - refund a charge, release a reservation. If any step cannot be undone, handle it last or make it retriable instead. - The flow is long or slow enough that holding a distributed lock would be worse than brief visibility of intermediate states. - Steps are idempotent or can carry an idempotency key ([Idempotent Consumer](https://aipatterns.dev/design-patterns/idempotent-consumer.md)), because retries after crashes will re-run them. ## Do not use it when - All the data lives in one database. A local ACID transaction is strictly simpler and gives real isolation; a saga re-implements it badly. - A step has no compensation - filing a legal record, dispatching a one-way physical shipment. Put non-compensable steps last, make them retriable until they succeed, or take the failure to a human workflow. - Strict isolation is required - concurrent sagas would double-spend a balance. Countermeasures (semantic locks, commutative updates) add complexity that a transaction gives you for free. - The workflow is two steps and rarely fails. An `if` with an explicit undo is a saga with none of the machinery; earn the structure first. ## Application grill **Inspect first:** the multi-step flows, their failure points, and whether each step can run twice (they must survive the idempotent-consumer bar); where saga progress would live (a table in the existing database is the default); existing queues, schedulers or workflow engines (Temporal, Step Functions) already in the organization. **Missing pieces:** saga state: one table (saga id, step, status) - no new infrastructure. Execution: a worker or cron to advance and time out sagas - platform cron or the existing queue. An orchestration engine (Temporal, AWS Step Functions) buys durable retries and visibility at the cost of a platform to operate; it earns its keep past a handful of long-running flows. **Ask before you build:** - **Scale:** how many concurrent sagas, and what timeout window precedes compensation? - **Performance:** must steps run strictly in order, or can independent steps parallelize? - **Reliability:** when compensation itself fails, what happens - alert plus a manual runbook, or automatic retry? - **Budget:** a workflow platform versus a hand-rolled state machine - how many flows justify the platform? - **Implementation:** how is every step made idempotent, and who owns the compensation tests? ## Implementation steps 1. List the steps in order and write the compensation next to each. If a compensation cannot be named, that step moves last or becomes a retriable action - decide before writing code. 2. Make every action idempotent (or keyed), because crashes and retries re-run steps; a compensated-then-retried charge must not double-charge. 3. Model a step as the pair `{ action, compensate }` where compensation consumes the action's result. Type it once; every step obeys it. 4. Drive the saga from persisted state, not from a live stack: write "step N completed with result R" before running step N+1, so a restart resumes instead of repeating. 5. On failure, run compensations in strict reverse order, each with its step's recorded result, and continue compensating even if a compensation itself fails - log the failure and keep going. 6. Record the terminal outcome (completed, compensated at step N, and why). A saga whose outcome cannot be queried is a rumor. 7. Choose orchestration first; move to choreography only when independent workflows need to react to the same events anyway. ## Example Checkout across three services. Stock and card succeed; the confirmation email fails. The saga refunds the charge, then releases the stock - compensations in reverse - and records the outcome. ```ts title="checkout.ts" type Step = { name: string; action: () => string; // returns the result its compensation consumes compensate: (result: string) => string; }; class CheckoutSaga { private executed: { step: Step; result: string }[] = []; outcome = ""; constructor(private readonly steps: Step[]) {} run(): string { for (const step of this.steps) { try { const result = step.action(); this.executed.push({ step, result }); console.log(` ok: ${step.name} -> ${result}`); } catch (e) { console.log(` FAILED: ${step.name} (${(e as Error).message})`); this.compensate(); return (this.outcome = `compensated at "${step.name}"`); } } return (this.outcome = "completed"); } private compensate(): void { while (this.executed.length) { const { step, result } = this.executed.pop()!; console.log(` undo: ${step.name} -> ${step.compensate(result)}`); } } } // Local services: each commits immediately, none knows the saga exists. const stock: { reserved: string[] } = { reserved: [] }; const card: { charged: string[] } = { charged: [] }; const email = { failing: true }; const saga = new CheckoutSaga([ { name: "reserve stock", action: () => { stock.reserved.push("sku-1"); return "reservation r-1"; }, compensate: (r) => { stock.reserved.pop(); return `${r} released`; }, }, { name: "charge card", action: () => { card.charged.push("c-1"); return "receipt rc-1"; }, compensate: (r) => { card.charged.pop(); return `${r} refunded`; }, }, { name: "send confirmation", action: () => { if (email.failing) throw new Error("mail queue down"); return "sent"; }, compensate: () => "n/a", }, ]); console.log(saga.run()); // compensated at "send confirmation" console.log(`stock: ${stock.reserved.length}, charges: ${card.charged.length}`); // stock: 0, charges: 0 - every effect undone, in reverse order ``` ```python title="checkout.py" from dataclasses import dataclass from typing import Callable @dataclass class Step: name: str action: Callable[[], str] compensate: Callable[[str], str] class CheckoutSaga: def __init__(self, steps: list[Step]) -> None: self._steps = steps self._executed: list[tuple[Step, str]] = [] self.outcome = "" def run(self) -> str: for step in self._steps: try: result = step.action() self._executed.append((step, result)) print(f" ok: {step.name} -> {result}") except Exception as e: print(f" FAILED: {step.name} ({e})") self._compensate() self.outcome = f'compensated at "{step.name}"' return self.outcome self.outcome = "completed" return self.outcome def _compensate(self) -> None: while self._executed: step, result = self._executed.pop() print(f" undo: {step.name} -> {step.compensate(result)}") # Local services: each commits immediately, none knows the saga exists. stock: list[str] = [] card: list[str] = [] email_failing = True def reserve() -> str: stock.append("sku-1") return "reservation r-1" def release(reservation: str) -> str: stock.pop() return f"{reservation} released" def charge() -> str: card.append("c-1") return "receipt rc-1" def refund(receipt: str) -> str: card.pop() return f"{receipt} refunded" def send() -> str: if email_failing: raise RuntimeError("mail queue down") return "sent" saga = CheckoutSaga( [ Step("reserve stock", reserve, release), Step("charge card", charge, refund), Step("send confirmation", send, lambda _: "n/a"), ] ) print(saga.run()) # compensated at "send confirmation" print(f"stock: {len(stock)}, charges: {len(card)}") # stock: 0, charges: 0 - every effect undone, in reverse order ``` ```go title="checkout.go" package main import "fmt" type Step struct { Name string Action func() string Compensate func(result string) string } type executedStep struct { step Step result string } type CheckoutSaga struct { steps []Step executed []executedStep Outcome string } func (s *CheckoutSaga) run() string { for _, step := range s.steps { result, err := safeCall(step.Action) if err != nil { fmt.Printf(" FAILED: %s (%v)\n", step.Name, err) s.compensate() s.Outcome = fmt.Sprintf("compensated at %q", step.Name) return s.Outcome } s.executed = append(s.executed, executedStep{step, result}) fmt.Printf(" ok: %s -> %s\n", step.Name, result) } s.Outcome = "completed" return s.Outcome } func (s *CheckoutSaga) compensate() { for len(s.executed) > 0 { last := s.executed[len(s.executed)-1] s.executed = s.executed[:len(s.executed)-1] fmt.Printf(" undo: %s -> %s\n", last.step.Name, last.step.Compensate(last.result)) } } func safeCall(action func() string) (result string, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("%v", r) } }() return action(), nil } func main() { var stock, card []string // local services: each commits immediately reserve := func() string { stock = append(stock, "sku-1") return "reservation r-1" } release := func(r string) string { stock = stock[:len(stock)-1] return r + " released" } charge := func() string { card = append(card, "c-1") return "receipt rc-1" } refund := func(r string) string { card = card[:len(card)-1] return r + " refunded" } emailFailing := true send := func() string { if emailFailing { panic("mail queue down") } return "sent" } saga := &CheckoutSaga{steps: []Step{ {"reserve stock", reserve, release}, {"charge card", charge, refund}, {"send confirmation", send, func(string) string { return "n/a" }}, }} fmt.Println(saga.run()) // compensated at "send confirmation" fmt.Printf("stock: %d, charges: %d\n", len(stock), len(card)) // stock: 0, charges: 0 - every effect undone, in reverse order } ``` ```java title="Checkout.java" import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.List; public class Checkout { record Step(String name, Action action, Compensation compensate) {} interface Action { String run(); } interface Compensation { String run(String result); } record Executed(Step step, String result) {} static final class CheckoutSaga { private final List steps; private final Deque executed = new ArrayDeque<>(); String outcome = ""; CheckoutSaga(List steps) { this.steps = steps; } String run() { for (Step step : steps) { try { String result = step.action().run(); executed.push(new Executed(step, result)); System.out.printf(" ok: %s -> %s%n", step.name(), result); } catch (RuntimeException e) { System.out.printf(" FAILED: %s (%s)%n", step.name(), e.getMessage()); compensate(); outcome = "compensated at \"" + step.name() + "\""; return outcome; } } outcome = "completed"; return outcome; } private void compensate() { while (!executed.isEmpty()) { Executed done = executed.pop(); System.out.printf(" undo: %s -> %s%n", done.step().name(), done.step().compensate().run(done.result())); } } } public static void main(String[] args) { // Local services: each commits immediately, none knows the saga exists. List stock = new ArrayList<>(); List card = new ArrayList<>(); boolean emailFailing = true; Step reserve = new Step("reserve stock", () -> { stock.add("sku-1"); return "reservation r-1"; }, r -> { stock.remove(stock.size() - 1); return r + " released"; }); Step charge = new Step("charge card", () -> { card.add("c-1"); return "receipt rc-1"; }, r -> { card.remove(card.size() - 1); return r + " refunded"; }); Step send = new Step("send confirmation", () -> { if (emailFailing) throw new IllegalStateException("mail queue down"); return "sent"; }, r -> "n/a"); CheckoutSaga saga = new CheckoutSaga(List.of(reserve, charge, send)); System.out.println(saga.run()); // compensated at "send confirmation" System.out.printf("stock: %d, charges: %d%n", stock.size(), card.size()); // stock: 0, charges: 0 - every effect undone, in reverse order } } ``` In production the steps are remote calls: the compensations must be retried until they succeed ([Retry](https://aipatterns.dev/design-patterns/retry.md)), and each action carries an idempotency key so a compensation followed by a retry cannot double-charge ([Idempotent Consumer](https://aipatterns.dev/design-patterns/idempotent-consumer.md)). ## Trade-offs **Gains** - Multi-service workflows get all-or-nothing semantics without distributed locks. - Each service stays autonomous: it commits locally and exposes nothing but its own operations. - The compensation path is explicit, testable code - not an error message. **Costs** - No isolation: concurrent sagas see intermediate states, so countermeasures (semantic locks, commutative updates) must be designed in. - Every step costs two implementations - the action and its compensation. - The failure path is the product: a saga whose compensations were never tested has not been written. - Orchestration centralizes logic; choreography scatters it - both are ongoing design decisions. ## Relations - [Transactional Outbox](https://aipatterns.dev/design-patterns/transactional-outbox.md): how a step's own two writes - its local state change and the "tell the saga" message - commit atomically; the saga's reliability rests on it. - [Idempotent Consumer](https://aipatterns.dev/design-patterns/idempotent-consumer.md): makes actions and compensations safe to re-run after crashes and at-least-once redelivery. - [Retry](https://aipatterns.dev/design-patterns/retry.md): compensations must ultimately succeed, so they retry until they do - a compensation that gives up leaves the saga inconsistent. - [Command](https://aipatterns.dev/design-patterns/command.md): a step is a command with an undo; persisting commands as objects is what makes a crashed saga resumable. ## Agent checklist - [ ] Every step has a named compensation that consumes the action's result; non-compensable steps are last or retriable. - [ ] Actions are idempotent or carry idempotency keys. - [ ] Saga state is persisted per step; a restart resumes, never repeats a side effect. - [ ] Compensations run in strict reverse order and continue even when one fails. - [ ] The terminal outcome is recorded and queryable. - [ ] Concurrent-saga interference has been considered, with a countermeasure or a written reason none is needed. --- # Idempotent Consumer > Record the ids of processed messages and skip ids already seen, so a redelivered webhook or queue event processes once no matter how many times it arrives. - kind: pattern - category: data - aliases: Deduplication, Message Dedup - canonical: https://aipatterns.dev/design-patterns/idempotent-consumer - inspired-by: https://learn.microsoft.com/en-us/azure/architecture/patterns/idempotent-consumer ## Intent Treat every incoming message as a possible repeat. Before processing, check a store of already-processed message ids; process only unseen messages, and record the id in the same transaction as the work. A message delivered five times then produces exactly one effect, because only the first delivery finds an unseen id. ## Forces - **At-least-once delivery** vs **once-only effects**: queues, relays and webhooks guarantee at-least-once at best; effects like charges and inserts happen in a world that counts. - **Check** vs **process**: the dedup check and the business work must commit together, or a crash between them turns the id record into a lie - either a lost message or a repeated one. - **Retention** vs **safety**: keeping every id forever is perfect and unbounded; keeping a window risks reprocessing an ancient redelivery. - **Sender cooperation** vs **receiver discipline**: an idempotency key from the sender is ideal, but the receiver must assume keys can repeat or be absent and dedupe on its own ids. ## Therefore **Give every message an id the sender keeps stable across retries; before processing, atomically claim the id - insert it into a processed-ids store keyed uniquely - and process only if the claim is the first.** The claim, not the check-then-act, is the pattern: two concurrent deliveries race to insert the same id, the unique key lets exactly one win, and the loser skips. Process the message inside the claim's transaction so a crash either leaves both the work and the id uncommitted (redelivery retries) or both committed (redelivery skips). ## Structure ```mermaid flowchart TD A[Message arrives] --> B{Claim the message id} B -- claimed --> C[Process the work in the claim's transaction] C --> D[Commit work + id] B -- already claimed --> E[Skip: acknowledge as duplicate] ``` ### Participants - **Message** (`Event`): payload plus a stable id - the provider's event id or a sender-generated idempotency key that survives retries. - **Processed-ids store** (`Processed`): rows keyed uniquely by message id, written in the same transaction as the work. Holds ids for a retention window. - **Consumer** (`WebhookReceiver`): claims, processes, commits - or recognizes the duplicate and acknowledges without work. - **Handler** (`grantCredit`): the business effect. Runs zero times or once per message id. ## Use it when - Delivery is at-least-once by contract: webhook providers redeliver on missing `2xx`, queues redeliver on unacknowledged messages, and the [Transactional Outbox](https://aipatterns.dev/design-patterns/transactional-outbox.md) relay publishes again after a crash. - The effect counts: charges, credits, inserts, sends. Read-only handlers need none of this. - Retries exist anywhere in the path - the sender retries on timeout, or a [Saga](https://aipatterns.dev/design-patterns/saga.md) re-runs a step after compensation ([Retry](https://aipatterns.dev/design-patterns/retry.md)). - You receive webhooks from a provider that documents redelivery (Stripe, GitHub) and you must never double-apply an event. ## Do not use it when - The handler is read-only or naturally idempotent - setting a field to a fixed value, deleting by id. Re-delivery changes nothing; the store is ceremony. - The transport contractually delivers exactly once and you believe it for your risk tolerance - rare, expensive, and usually configured per-flow. - The same message legitimately arrives under different ids for different business events. Dedupe on the business operation, not the transport id - or you will drop real work. - You control both ends and the sender deduplicates already. One honest dedup layer per path is enough; two is two things to debug. ## Application grill **Inspect first:** the ingestion points (webhooks, queue consumers) and whether redelivery is already a fact (provider retries, at-least-once queues); an existing store of processed ids (outbox table, dedupe table, Redis keys); whether handlers are safe to run twice today (blind inserts versus upserts). **Missing pieces:** a durable record of seen ids. The default is a dedupe table with a unique constraint in the existing database, checked in the same transaction as the work. Redis with a TTL suits high volume where a duplicate after expiry is harmless. Whatever the store, the handler must still tolerate a second run (upsert, not insert). **Ask before you build:** - **Scale:** how many messages per second arrive, and how long can a duplicate arrive late (the retention window for seen ids)? - **Performance:** can the dedupe check share the business transaction (one round trip), or does it add one? - **Reliability:** is the id recorded only after the work commits, so a crash mid-handler redelivers safely? - **Budget:** who prunes the dedupe store, and what does its growth cost? - **Implementation:** which id is the dedupe key (provider event id versus entity plus operation), and is there a test that replays a redelivery? ## Implementation steps 1. Choose the id: the provider's event id when it exists; otherwise require an idempotency key from the sender, or derive a stable id from the operation (order id + operation name). 2. Make the store of processed ids part of the processing transaction: insert the id with a unique constraint and do the work, then commit. Never check-then-insert in separate transactions. 3. On a unique-violation, treat the message as a duplicate: acknowledge it and move on. Log at info level - duplicates are the contract working, not errors. 4. Set a retention window longer than the sender's maximum redelivery horizon, and say it in days, not vibes. Purge expired ids on a schedule; a dedup store that grows forever is its own incident. 5. Handle the concurrent case explicitly: a unique constraint settles the race; an upsert-and-report-rows-inserted achieves the same without catching exceptions. 6. Keep processing inside the claim even when the handler calls other services - if the remote call fails after the claim commits, redelivery is skipped, so the handler itself must be retryable or the claim must roll back on failure. 7. Alert on duplicate rates per sender: a sudden rise means their retry policy changed or your acknowledgments stopped arriving. ## Example A credit webhook that providers redeliver on any non-2xx. The receiver claims the event id in the same transaction as the credit; the first delivery grants and records, a redelivery is acknowledged as a duplicate, and a crash mid-handler rolls the claim back so the redelivery succeeds. ```ts title="webhooks.ts" type Event = { id: string; account: string; cents: number }; // The database: the claim and the credit commit together or not at all. class Store { private claimed = new Set(); private balances = new Map(); begin() {} commit() {} rollback() {} // a real store reverts uncommitted writes here claim(id: string): boolean { if (this.claimed.has(id)) return false; // unique key says: duplicate this.claimed.add(id); return true; } grantCredit(account: string, cents: number): void { this.balances.set(account, (this.balances.get(account) ?? 0) + cents); } balance(account: string): number { return this.balances.get(account) ?? 0; } } class WebhookReceiver { constructor(private readonly store: Store) {} /** Returns "processed" or "duplicate"; providers only need a 2xx. */ handle(event: Event): string { this.store.begin(); try { if (!this.store.claim(event.id)) { this.store.commit(); return `duplicate: ${event.id} skipped`; } this.store.grantCredit(event.account, event.cents); this.store.commit(); return `processed: ${event.id} (+${event.cents}c)`; } catch (e) { this.store.rollback(); // claim rolls back too: redelivery will process throw e; } } } const store = new Store(); const receiver = new WebhookReceiver(store); const event: Event = { id: "evt_001", account: "acc-9", cents: 500 }; console.log(receiver.handle(event)); // processed: evt_001 (+500c) console.log(receiver.handle(event)); // duplicate: evt_001 skipped (redelivery) console.log(receiver.handle(event)); // duplicate: evt_001 skipped console.log(`balance: ${store.balance("acc-9")}`); // 500 - granted exactly once ``` ```python title="webhooks.py" from dataclasses import dataclass @dataclass class Event: id: str account: str cents: int class Store: """The database: the claim and the credit commit together or not at all.""" def __init__(self) -> None: self._claimed: set[str] = set() self._balances: dict[str, int] = {} def begin(self) -> None: ... def commit(self) -> None: ... def rollback(self) -> None: # a real store reverts uncommitted writes here ... def claim(self, id: str) -> bool: if id in self._claimed: return False # unique key says: duplicate self._claimed.add(id) return True def grant_credit(self, account: str, cents: int) -> None: self._balances[account] = self._balances.get(account, 0) + cents def balance(self, account: str) -> int: return self._balances.get(account, 0) class WebhookReceiver: def __init__(self, store: Store) -> None: self._store = store def handle(self, event: Event) -> str: """Returns 'processed' or 'duplicate'; providers only need a 2xx.""" self._store.begin() try: if not self._store.claim(event.id): self._store.commit() return f"duplicate: {event.id} skipped" self._store.grant_credit(event.account, event.cents) self._store.commit() return f"processed: {event.id} (+{event.cents}c)" except Exception: self._store.rollback() # claim rolls back too: redelivery will process raise store = Store() receiver = WebhookReceiver(store) event = Event(id="evt_001", account="acc-9", cents=500) print(receiver.handle(event)) # processed: evt_001 (+500c) print(receiver.handle(event)) # duplicate: evt_001 skipped (redelivery) print(receiver.handle(event)) # duplicate: evt_001 skipped print(f"balance: {store.balance('acc-9')}") # 500 - granted exactly once ``` ```go title="webhooks.go" package main import "fmt" type Event struct { ID string Account string Cents int } // Store: the claim and the credit commit together or not at all. type Store struct { claimed map[string]bool balances map[string]int inTx bool } func NewStore() *Store { return &Store{claimed: map[string]bool{}, balances: map[string]int{}} } func (s *Store) begin() { s.inTx = true } func (s *Store) commit() { s.inTx = false } func (s *Store) rollback() { s.inTx = false } // a real store reverts uncommitted writes func (s *Store) claim(id string) bool { if s.claimed[id] { return false // unique key says: duplicate } s.claimed[id] = true return true } func (s *Store) grantCredit(account string, cents int) { s.balances[account] += cents } func (s *Store) Balance(account string) int { return s.balances[account] } type WebhookReceiver struct{ store *Store } // Handle returns "processed" or "duplicate"; providers only need a 2xx. func (r WebhookReceiver) Handle(e Event) (string, error) { s := r.store s.begin() defer func() { if s.inTx { s.rollback() // panic path: claim rolls back too, redelivery processes } }() if !s.claim(e.ID) { s.commit() return "duplicate: " + e.ID + " skipped", nil } s.grantCredit(e.Account, e.Cents) s.commit() return fmt.Sprintf("processed: %s (+%dc)", e.ID, e.Cents), nil } func main() { store := NewStore() receiver := WebhookReceiver{store} event := Event{ID: "evt_001", Account: "acc-9", Cents: 500} fmt.Println(must(receiver.Handle(event))) // processed: evt_001 (+500c) fmt.Println(must(receiver.Handle(event))) // duplicate: evt_001 skipped (redelivery) fmt.Println(must(receiver.Handle(event))) // duplicate: evt_001 skipped fmt.Printf("balance: %d\n", store.Balance("acc-9")) // 500 - granted exactly once } func must[T any](v T, err error) T { if err != nil { panic(err) } return v } ``` ```java title="Webhooks.java" import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; public class Webhooks { record Event(String id, String account, int cents) {} // The database: the claim and the credit commit together or not at all. static final class Store { private final Set claimed = new HashSet<>(); private final Map balances = new HashMap<>(); void begin() {} void commit() {} void rollback() {} // a real store reverts uncommitted writes here boolean claim(String id) { if (claimed.contains(id)) return false; // unique key says: duplicate claimed.add(id); return true; } void grantCredit(String account, int cents) { balances.merge(account, cents, Integer::sum); } int balance(String account) { return balances.getOrDefault(account, 0); } } static final class WebhookReceiver { private final Store store; WebhookReceiver(Store store) { this.store = store; } /** Returns "processed" or "duplicate"; providers only need a 2xx. */ String handle(Event event) { store.begin(); try { if (!store.claim(event.id())) { store.commit(); return "duplicate: " + event.id() + " skipped"; } store.grantCredit(event.account(), event.cents()); store.commit(); return "processed: " + event.id() + " (+" + event.cents() + "c)"; } catch (RuntimeException e) { store.rollback(); // claim rolls back too: redelivery will process throw e; } } } public static void main(String[] args) { Store store = new Store(); WebhookReceiver receiver = new WebhookReceiver(store); Event event = new Event("evt_001", "acc-9", 500); System.out.println(receiver.handle(event)); // processed: evt_001 (+500c) System.out.println(receiver.handle(event)); // duplicate: evt_001 skipped (redelivery) System.out.println(receiver.handle(event)); // duplicate: evt_001 skipped System.out.println("balance: " + store.balance("acc-9")); // 500 - granted once } } ``` The retention window is part of the correctness claim: an id purged before the sender's redelivery horizon expires lets that ancient duplicate through, so the window must outlast the sender's worst retry schedule. ## Trade-offs **Gains** - Effects count deliveries once, no matter how the transport misbehaves. - The dedup decision is transactional with the work - no crash window between "remembered" and "done". - Duplicate rates per sender become an observable signal about the pipeline. **Costs** - A processed-ids store to own, size, and purge; its retention window is a correctness parameter. - The claim must share the work's transaction, which couples the dedup store to the primary datastore. - Real processing must still be retryable: a failure after the claim commits cannot be fixed by redelivery. - Ids must actually be stable across retries - sender cooperation or careful derivation required. ## Relations - [Transactional Outbox](https://aipatterns.dev/design-patterns/transactional-outbox.md): the producing half of the same contract - its relay publishes at least once, which is precisely why this pattern exists on the consuming side. - [Saga](https://aipatterns.dev/design-patterns/saga.md): every saga action and compensation re-runs after crashes; idempotency keys are what make re-running them harmless. - [Retry](https://aipatterns.dev/design-patterns/retry.md): retries are the source of duplicates - the sender's retry policy defines the retention window this pattern must outlast. - [Command](https://aipatterns.dev/design-patterns/command.md): a claimed message is a command executed once; modeling messages as commands with stable ids makes the dedup key obvious. ## Agent checklist - [ ] Every message has a stable id across retries, and the id choice is written down. - [ ] The claim and the work share one transaction; a unique constraint settles concurrent deliveries. - [ ] Duplicates are acknowledged, logged at info, and never re-processed. - [ ] The retention window outlasts every sender's maximum redelivery horizon. - [ ] The handler is retryable even after a late claim commit. - [ ] Duplicate rates per sender are monitored. --- # CQRS > Split the command model from per-view query models, joined by projections - each side shaped for its job, at the price of two models and eventual consistency. - kind: pattern - category: data - aliases: Command-Query Responsibility Segregation - canonical: https://aipatterns.dev/design-patterns/cqrs - inspired-by: https://martinfowler.com/bliki/CQRS.html ## Intent Use a different model to update information than the model you use to read it. Commands go through the write model - validation, invariants, business rules; queries go against read models shaped for specific views - denormalized, pre-joined, maybe in another store. Projections update the read models from the write side's changes. Each model is simpler than the merged one that tried to serve both, and the views get exactly the shape their queries need. ## Forces - **One model** vs **two jobs**: reads want denormalized, view-shaped rows; writes want normalized structures that protect invariants. One model does both worse. - **Consistency** vs **read speed**: projections lag the write, so a query right after a command may not see it - read-your-writes becomes a design decision. - **Simplicity for most of the system** vs **power where it pays**: CQRS is worthwhile on the few contentious parts of a system; applying it everywhere converts a codebase into two codebases. - **Query freedom** vs **schema coupling**: free-form read models break when commands change shape, and every projection is another thing to rebuild. ## Therefore **Keep commands on a write model that enforces invariants, publish its changes, and maintain per-view read models through projections - then prove the read lag, the rebuild story, and the second model are each worth their cost before writing either.** CQRS is a scaling decision, not an architecture. Most of a system fits CRUD comfortably, and Fowler's caution stands: be very cautious, because the merged model he saw replaced with CQRS was usually doing neither job well - but the replacement cost more than it saved more often than not. Adopt it on the bounded piece where reads demonstrably fight writes. ## Structure ```mermaid classDiagram class OrderCommands { +place(order) +ship(order) } class OrderStore { +append(event) } class HistoryProjection { +onOrderPlaced(event) +onOrderShipped(event) } class OrderHistory { +rows } OrderCommands --> OrderStore : commits changes OrderStore --> HistoryProjection : streams changes HistoryProjection --> OrderHistory : maintains ``` ### Participants - **Write model** (`OrderCommands`): handles commands. Validates, enforces invariants, and records what changed as events or rows. The only writer. - **Change stream** (`OrderStore`): the write side's committed changes, in order - event rows, an outbox, or change-data-capture. - **Projection** (`HistoryProjection`): consumes the stream and maintains one read model. Dumb and rebuildable: drop it, replay the stream, get the same rows. - **Read model** (`OrderHistory`): denormalized rows shaped for one query family. No invariants, no logic - it serves. ## Use it when - Reads and writes fight over shape: the query the UI needs needs a join of six tables the write side must keep normalized, and the fight shows in both. - Read and write scale differently - a read-heavy dashboard over a write-protected ledger - and one store cannot be tuned for both. - You need several materially different views of the same data (search index, reporting table, API view), and each is rebuilt from changes anyway. - The team can operate eventual consistency on the read side, including the "query after command" answer. ## Do not use it when - The system fits CRUD. Most do; forcing CQRS on them doubles every change for nothing. - The team has not operated eventual consistency before. The read lag will surface as "the API lies" bugs at the worst time. - There is one view per entity and no scaling pressure - a well-indexed table with a view is the same result at a tenth of the machinery. - You have not measured the contention. CQRS applied to an uncontended part of the system is the textbook poor fit Fowler describes. ## Application grill **Inspect first:** the reads that fight the write model (aggregate queries, joins at request time) and their measured cost; whether the database already has a smaller answer (indexes, materialized views, read replicas); existing events, outbox or queue infrastructure a projection could ride. **Missing pieces:** a projection pipeline needs an event source (outbox, queue, or CDC such as Debezium), a worker to apply events, and a read store per view. The smallest step is denormalized tables or materialized views in the same database, refreshed in-transaction or on a short interval; a separate search index (Typesense, Meilisearch, Elasticsearch) only for full-text or faceted reads. Every piece is something to operate. **Ask before you build:** - **Scale:** which read is actually slow, measured - and how many views genuinely need their own model? - **Performance:** how stale may a view be, and must a user see their own writes immediately (read-your-writes routing)? - **Reliability:** when a projection falls behind or its worker dies, what does the UI show and who gets paged? - **Budget:** a second store plus a pipeline, or one denormalized table in the existing database? - **Implementation:** is there a rebuild runbook (replay events from zero), and who has executed it? ## Implementation steps 1. Bound it: pick the one subdomain where reads demonstrably fight writes - measured, not felt. Everything else stays CRUD. 2. Keep the write model in charge of truth: commands validate and commit changes, and record what changed (event rows are the simplest stream). 3. Start with one projection to one read model that already hurts. The projection is a loop: read unprocessed changes in order, apply, record the position, commit. 4. Make projections rebuildable from zero. If you cannot drop the read model and replay, the stream is not the source of truth and the pattern is not really in place. 5. Decide the read-lag contract per view: which queries may serve slightly stale rows, and which must read the write model directly. Read-your-writes is per-endpoint, not global. 6. Version the stream: projections must tolerate events written by older command versions, or every command change becomes a migration. 7. Monitor projection lag per view. Lag is the system honestly reporting its consistency; hide it and you have hidden the pattern's main cost. ## Example An orders subdomain: commands append changes to the write store; a history projection maintains the flat rows the support UI queries. A query right after a command shows the lag honestly - the projection has not ticked yet. ```ts title="orders.ts" type OrderEvent = | { type: "placed"; orderId: string; sku: string; totalCents: number } | { type: "shipped"; orderId: string }; // Write side: the only writer, enforcing invariants. class OrderCommands { private shipped = new Set(); constructor(private readonly store: OrderEvent[]) {} place(orderId: string, sku: string, totalCents: number): void { this.store.push({ type: "placed", orderId, sku, totalCents }); } ship(orderId: string): void { if (!this.store.some((e) => e.type === "placed" && e.orderId === orderId)) throw new Error(`unknown order ${orderId}`); // invariant lives here only this.shipped.add(orderId); this.store.push({ type: "shipped", orderId }); } } // Read side: denormalized rows shaped for the support UI's one query. type HistoryRow = { orderId: string; sku: string; totalCents: number; status: string; }; class HistoryProjection { readonly rows = new Map(); private position = 0; tick(store: OrderEvent[]): void { for (; this.position < store.length; this.position++) { const e = store[this.position]; if (e.type === "placed") this.rows.set(e.orderId, { orderId: e.orderId, sku: e.sku, totalCents: e.totalCents, status: "placed", }); else this.rows.get(e.orderId)!.status = "shipped"; } } } const store: OrderEvent[] = []; const commands = new OrderCommands(store); const projection = new HistoryProjection(); commands.place("o-1", "tea-1", 450); console.log(`rows before projection tick: ${projection.rows.size}`); // 0 - the lag projection.tick(store); console.log(projection.rows.get("o-1")); // placed commands.ship("o-1"); projection.tick(store); console.log(projection.rows.get("o-1")!.status); // shipped // try { commands.ship("o-1"); } catch (e) { console.log((e as Error).message); } // commands.ship("o-99") would throw: invariants live on the write side only ``` ```python title="orders.py" from dataclasses import dataclass, field from typing import Any @dataclass class Placed: order_id: str sku: str total_cents: int @dataclass class Shipped: order_id: str OrderEvent = Placed | Shipped class OrderCommands: """Write side: the only writer, enforcing invariants.""" def __init__(self, store: list[OrderEvent]) -> None: self._store = store def place(self, order_id: str, sku: str, total_cents: int) -> None: self._store.append(Placed(order_id, sku, total_cents)) def ship(self, order_id: str) -> None: if not any( isinstance(e, Placed) and e.order_id == order_id for e in self._store ): raise ValueError(f"unknown order {order_id}") # invariant lives here only self._store.append(Shipped(order_id)) @dataclass class HistoryRow: order_id: str sku: str total_cents: int status: str class HistoryProjection: """Read side: denormalized rows shaped for the support UI's one query.""" def __init__(self) -> None: self.rows: dict[str, HistoryRow] = {} self._position = 0 def tick(self, store: list[OrderEvent]) -> None: while self._position < len(store): event = store[self._position] self._position += 1 if isinstance(event, Placed): self.rows[event.order_id] = HistoryRow( event.order_id, event.sku, event.total_cents, "placed" ) else: self.rows[event.order_id].status = "shipped" store: list[OrderEvent] = [] commands = OrderCommands(store) projection = HistoryProjection() commands.place("o-1", "tea-1", 450) print(f"rows before projection tick: {len(projection.rows)}") # 0 - the lag projection.tick(store) print(projection.rows["o-1"]) # placed commands.ship("o-1") projection.tick(store) print(projection.rows["o-1"].status) # shipped ``` ```go title="orders.go" package main import "fmt" type OrderEvent struct { OrderID string Type string // "placed" | "shipped" SKU string TotalCents int } // Write side: the only writer, enforcing invariants. type OrderCommands struct{ store *[]OrderEvent } func (c OrderCommands) place(orderID, sku string, totalCents int) { *c.store = append(*c.store, OrderEvent{orderID, "placed", sku, totalCents}) } func (c OrderCommands) ship(orderID string) error { for _, e := range *c.store { if e.Type == "placed" && e.OrderID == orderID { *c.store = append(*c.store, OrderEvent{OrderID: orderID, Type: "shipped"}) return nil } } return fmt.Errorf("unknown order %s", orderID) // invariant lives here only } // Read side: denormalized rows shaped for the support UI's one query. type HistoryRow struct { OrderID string SKU string TotalCents int Status string } type HistoryProjection struct { rows map[string]HistoryRow position int } func NewHistoryProjection() *HistoryProjection { return &HistoryProjection{rows: map[string]HistoryRow{}} } func (p *HistoryProjection) Tick(store []OrderEvent) { for ; p.position < len(store); p.position++ { e := store[p.position] if e.Type == "placed" { p.rows[e.OrderID] = HistoryRow{e.OrderID, e.SKU, e.TotalCents, "placed"} } else { row := p.rows[e.OrderID] row.Status = "shipped" p.rows[e.OrderID] = row } } } func main() { store := []OrderEvent{} commands := OrderCommands{store: &store} projection := NewHistoryProjection() commands.place("o-1", "tea-1", 450) fmt.Printf("rows before projection tick: %d\n", len(projection.rows)) // 0 - the lag projection.Tick(store) fmt.Printf("%+v\n", projection.rows["o-1"]) // placed if err := commands.ship("o-1"); err != nil { fmt.Println(err) } projection.Tick(store) fmt.Println(projection.rows["o-1"].Status) // shipped if err := commands.ship("o-99"); err != nil { fmt.Println(err) // unknown order o-99 - invariants live on the write side } } ``` ```java title="Orders.java" import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class Orders { sealed interface OrderEvent permits Placed, Shipped {} record Placed(String orderId, String sku, int totalCents) implements OrderEvent {} record Shipped(String orderId) implements OrderEvent {} /** Write side: the only writer, enforcing invariants. */ static final class OrderCommands { private final List store; OrderCommands(List store) { this.store = store; } void place(String orderId, String sku, int totalCents) { store.add(new Placed(orderId, sku, totalCents)); } void ship(String orderId) { boolean known = store.stream() .anyMatch(e -> e instanceof Placed p && p.orderId().equals(orderId)); if (!known) throw new IllegalArgumentException("unknown order " + orderId); store.add(new Shipped(orderId)); // invariant lives here only } } /** Read side: denormalized rows shaped for the support UI's one query. */ static final class HistoryRow { final String orderId; final String sku; final int totalCents; String status; HistoryRow(String orderId, String sku, int totalCents) { this.orderId = orderId; this.sku = sku; this.totalCents = totalCents; this.status = "placed"; } } static final class HistoryProjection { final Map rows = new HashMap<>(); private int position = 0; void tick(List store) { while (position < store.size()) { OrderEvent e = store.get(position++); if (e instanceof Placed p) { rows.put(p.orderId(), new HistoryRow(p.orderId(), p.sku(), p.totalCents())); } else if (e instanceof Shipped s) { rows.get(s.orderId()).status = "shipped"; } } } } public static void main(String[] args) { List store = new ArrayList<>(); OrderCommands commands = new OrderCommands(store); HistoryProjection projection = new HistoryProjection(); commands.place("o-1", "tea-1", 450); System.out.println("rows before projection tick: " + projection.rows.size()); // 0 projection.tick(store); System.out.println(projection.rows.get("o-1").status); // placed commands.ship("o-1"); projection.tick(store); System.out.println(projection.rows.get("o-1").status); // shipped try { commands.ship("o-99"); } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); // unknown order o-99 } } } ``` The write side's `ship` rejecting an unknown order is the point of the split: invariants live only where commands run, and read models never re-check them. ## Trade-offs **Gains** - Queries get exactly their shape - pre-joined, pre-computed, indexed for the one question they ask. - Write model stays small and invariant-focused; business rules stop negotiating with query convenience. - Read models scale and evolve independently - add a view, replay the stream, no write-side change. **Costs** - Two models for one concept, plus a projection each - every command-side change touches read-side code. - Eventual consistency on reads is a product decision, per view, forever. - Projections must handle old event versions and be rebuildable; both are ongoing obligations. - Applies well to a bounded piece of a system and poorly to a whole one - the boundary is a permanent design object. ## Relations - [Transactional Outbox](https://aipatterns.dev/design-patterns/transactional-outbox.md): the write side's changes reach projections through one - the stream is an outbox read by projection relays. - [Cache-Aside](https://aipatterns.dev/design-patterns/cache-aside.md): the lighter tool for the same pressure - when one cached read model answers the need, prefer it and keep the single model. - [Saga](https://aipatterns.dev/design-patterns/saga.md): the write side of a saga publishes step results through the stream that projections consume. ## Agent checklist - [ ] The boundary is one measured, contentious subdomain - the rest of the system stays CRUD. - [ ] Invariants live only in the write model; read models never re-check them. - [ ] Every projection can be dropped and rebuilt by replaying the stream. - [ ] Read-lag expectations are decided per view, and read-your-writes endpoints bypass the projection. - [ ] Projection lag is monitored per view. - [ ] Event/projection versioning has an answer before the first command changes. --- # Timeout > Give every remote call a deadline shorter than the caller's own budget, cancel the work when it expires, and surface the timeout as a distinct error. - kind: pattern - category: resilience - aliases: Deadline - canonical: https://aipatterns.dev/design-patterns/timeout - inspired-by: https://learn.microsoft.com/en-us/azure/architecture/best-practices/transient-faults ## Intent Every call that crosses a process boundary gets a maximum wait. When the deadline passes, the call fails with a distinct timeout error and the resources it held are released. The caller then decides: retry, fall back, or report. Without a deadline, a hung dependency converts your healthy process into a parked one. ## Forces - **Unbounded waits** vs **finite capacity**: a worker blocked forever on a dead socket is a worker you have lost, and the queue behind it grows. - **One hung dependency** vs **whole-system failure**: without deadlines, slowness propagates upstream until every caller of the slow one is stuck. - **Callee's design** vs **caller's budget**: the dependency may be built for 30-second responses while your user journey allows 2. - **Cancel** vs **abandon**: cancelling stops the downstream work; abandoning only stops waiting while the work keeps running and consuming. ## Therefore **Set an explicit deadline on every remote or expensive call, shorter than the caller's own budget, cancel the underlying work when it expires, and raise a distinct timeout error.** The deadline is a property of the caller, not of the dependency: each layer subtracts its own overhead from what it was given and passes the remainder down. A timeout is not a fix for slowness - it converts slow failure into fast failure so the rest of the system can react. ## Structure ```mermaid classDiagram class QuoteSource { <> +quote(postcode, signal) Number } class RemoteQuotes { +quote(postcode, signal) Number } class BoundedQuotes { -Duration limit +quote(postcode, signal) Number } QuoteSource <|.. RemoteQuotes QuoteSource <|.. BoundedQuotes BoundedQuotes --> QuoteSource : inner ``` ### Participants - **Deadline holder** (`BoundedQuotes`): starts a timer per call, and cancels the call when it fires. A thin wrapper - the same shape as a [Decorator](https://aipatterns.dev/design-patterns/decorator.md). - **Cancellable operation** (`QuoteSource`): every layer accepts a cancellation token (abort signal, context, interrupt flag) so the deadline can reach the actual I/O. - **Dependency** (`RemoteQuotes`): does the work and honours the cancellation signal. - **Caller**: receives `TimeoutError` as a type it can branch on. ## Use it when - A call crosses a network, another process, or any resource with its own latency profile: HTTP, RPC, database, queue, file system. - The caller has its own budget: a user is waiting, a request deadline exists, or a job has a `maxDuration`. - You are about to add retries: each attempt needs a deadline, or one hung attempt hangs the whole retry loop. - Code calls a library whose defaults are absent or generous (no socket timeout, 60-second HTTP client default). ## Do not use it when - The call is in-process and cannot hang. A function that throws on bad input needs no deadline. - The operation is legitimately long-running. Model it as an async job with status polling instead of a 30-second synchronous call you will inevitably cancel. - The platform already propagates deadlines (gRPC deadline propagation, Go `context.Context`, .NET `CancellationToken`). Use the platform mechanism; do not build a parallel timer layer next to it. - You are using a timeout to excuse a slow dependency. It stops the bleeding; capacity work or a [Fallback](https://aipatterns.dev/design-patterns/fallback.md) fixes the experience. - Every caller would pick the same number and the client library exposes one cleanly. Configure the library's timeout option instead of wrapping each call site. ## Application grill **Inspect first:** every remote call's deadline today (HTTP clients default to none or minutes - find each one); the platform's own limits (serverless execution caps, load balancer idle timeouts, database statement_timeout); the caller's budget chain, since every timeout must be shorter than its caller's remaining budget. **Missing pieces:** none to buy: `AbortController`, context deadlines, and statement timeouts are platform features. The infrastructure work is alignment - setting database, proxy and load balancer timeouts to agree with the code's, so no layer quietly outlives another. **Ask before you build:** - **Scale:** what per-dependency latency budget fits the workload - base deadlines on measured p99, not hope? - **Performance:** does cancellation propagate - when a parent times out, do child calls abort (context linking)? - **Reliability:** on timeout, is the server-side work cancelled or orphaned (zombie requests keep spending)? - **Budget:** a timeout times a retry multiplies worst-case latency - who owns the total budget per request? - **Implementation:** is a timeout a distinct error type callers can classify, or an indistinguishable failure? ## Implementation steps 1. Inventory the remote calls in the path. Note which have explicit timeouts today; most have none or an OS default measured in minutes. 2. Derive each deadline from the caller's budget: if the request may take 2 s and makes three calls, none may take more than its share. Write the number down next to the call. 3. Use the platform's cancellation primitive - `AbortSignal`, `context.Context`, `CancellationToken`, `Future.get(timeout)` - so expiry cancels the work instead of merely abandoning it. 4. Raise a distinct error type (`TimeoutError`, `context.DeadlineExceeded`) so callers can distinguish "slow" from "broken" from "rejected". 5. Propagate remaining time downstream: pass the context or deadline through, so a dependency cannot outlive its caller's budget. 6. Test the timeout path with a fake dependency that sleeps past the deadline, and assert the caller recovers. ## Example A checkout service asks a shipping API for delivery quotes. The API usually answers in 20 ms, but when its database stalls it answers in never - and without a deadline, checkout workers pile up behind it. ```ts title="checkout.ts" // Distinct type so callers can branch on "slow" without string matching. class TimeoutError extends Error { constructor(ms: number) { super(`no answer within ${ms} ms`); this.name = "TimeoutError"; } } interface QuoteSource { quote(postcode: string, signal: AbortSignal): Promise; } // The dependency: a shipping API that hangs when its database stalls. class RemoteQuotes implements QuoteSource { quote(postcode: string, signal: AbortSignal): Promise { const hang = postcode === "XXXXX"; // Real client: fetch(url, { signal }). Fake: a timer that honours the signal. return new Promise((resolve, reject) => { const done = setTimeout(() => resolve(4.99), hang ? 10_000 : 20); signal.addEventListener( "abort", () => { clearTimeout(done); reject(signal.reason ?? new Error("cancelled")); }, { once: true }, ); }); } } // The deadline: each call gets its own abort controller with a timer. function withTimeout(inner: QuoteSource, ms: number): QuoteSource { return { quote(postcode) { const controller = new AbortController(); const timer = setTimeout( () => controller.abort(new TimeoutError(ms)), ms, ); return inner .quote(postcode, controller.signal) .finally(() => clearTimeout(timer)); }, }; } const quotes = withTimeout(new RemoteQuotes(), 500); const fast = await quotes.quote("90210"); console.log(`quote: $${fast.toFixed(2)}`); // quote: $4.99 try { await quotes.quote("XXXXX"); // hangs; the deadline fires at 500 ms } catch (e) { console.log(`${(e as Error).name}: ${(e as Error).message}`); // TimeoutError: no answer within 500 ms } ``` ```python title="checkout.py" # Python 3.11+: asyncio.timeout cancels the work for real. On 3.10 use asyncio.wait_for. import asyncio from typing import Protocol class RemoteQuotes: """A shipping API that hangs when its database stalls.""" async def quote(self, postcode: str) -> float: hang = postcode == "XXXXX" await asyncio.sleep(10 if hang else 0.02) return 4.99 def with_timeout(inner: Protocol, seconds: float): """Bound every call; the deadline cancels the inner work on expiry.""" class Bounded: async def quote(self, postcode: str) -> float: async with asyncio.timeout(seconds): return await inner.quote(postcode) return Bounded() async def main() -> None: quotes = with_timeout(RemoteQuotes(), 0.5) fast = await quotes.quote("90210") print(f"quote: ${fast:.2f}") # quote: $4.99 try: await quotes.quote("XXXXX") # hangs; the inner coroutine is cancelled at 0.5 s except TimeoutError: # asyncio.TimeoutError; TimeoutError in 3.11+ print("gave up: shipping quote timed out") asyncio.run(main()) ``` ```go title="checkout.go" package main import ( "context" "errors" "fmt" "time" ) type QuoteSource interface { Quote(ctx context.Context, postcode string) (float64, error) } // RemoteQuotes is a shipping API that hangs when its database stalls. type RemoteQuotes struct{} func (RemoteQuotes) Quote(ctx context.Context, postcode string) (float64, error) { delay := 20 * time.Millisecond if postcode == "XXXXX" { delay = 10 * time.Second } select { case <-time.After(delay): return 4.99, nil case <-ctx.Done(): // the deadline fired: stop the work return 0, ctx.Err() } } // BoundedQuotes enforces a per-call deadline. ctx carries it down, so the // dependency's work is cancelled, not abandoned. type BoundedQuotes struct { inner QuoteSource limit time.Duration } func (b BoundedQuotes) Quote(ctx context.Context, postcode string) (float64, error) { ctx, cancel := context.WithTimeout(ctx, b.limit) defer cancel() return b.inner.Quote(ctx, postcode) } func main() { quotes := BoundedQuotes{inner: RemoteQuotes{}, limit: 500 * time.Millisecond} if q, err := quotes.Quote(context.Background(), "90210"); err != nil { fmt.Println("err:", err) } else { fmt.Printf("quote: $%.2f\n", q) // quote: $4.99 } if _, err := quotes.Quote(context.Background(), "XXXXX"); errors.Is(err, context.DeadlineExceeded) { fmt.Println("gave up:", err) // gave up: context deadline exceeded } } ``` ```java title="Checkout.java" import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class Checkout { interface QuoteSource { double quote(String postcode) throws Exception; } /** A shipping API that hangs when its database stalls. */ static final class RemoteQuotes implements QuoteSource { public double quote(String postcode) throws Exception { Thread.sleep("XXXXX".equals(postcode) ? 10_000 : 20); return 4.99; } } static final class QuoteTimeoutException extends RuntimeException { QuoteTimeoutException(long ms) { super("no answer within " + ms + " ms"); } } /** Bounds each call and interrupts the worker when the deadline passes. */ record BoundedQuotes(QuoteSource inner, ExecutorService pool, long ms) implements QuoteSource { public double quote(String postcode) throws Exception { Future job = pool.submit(() -> inner.quote(postcode)); try { return job.get(ms, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { job.cancel(true); // interrupt the worker; do not abandon it throw new QuoteTimeoutException(ms); } } } public static void main(String[] args) throws Exception { ExecutorService pool = Executors.newCachedThreadPool(); QuoteSource quotes = new BoundedQuotes(new RemoteQuotes(), pool, 500); try { System.out.printf("quote: $%.2f%n", quotes.quote("90210")); // quote: $4.99 quotes.quote("XXXXX"); } catch (QuoteTimeoutException e) { System.out.println("gave up: " + e.getMessage()); } finally { pool.shutdownNow(); } } } ``` Go needs no wrapper at all when callers create their own contexts: `context.WithTimeout` at the call site is the pattern. The wrapper earns its place when you want one number to apply to every caller of a client. ## Trade-offs **Gains** - Latency becomes bounded and predictable; capacity planning gets possible. - Hung dependencies release workers instead of accumulating them. - Slow and broken become distinguishable errors. **Costs** - A call that would have succeeded at 501 ms is cancelled at 500 ms and its work is wasted. - Too-low deadlines convert a slow dependency into a failing one. - Cancellation is per language and per client; where the primitive is weak (threads that ignore interrupts), you abandon instead of cancel and the work still runs. ## Relations - [Retry](https://aipatterns.dev/design-patterns/retry.md) repeats the operation - but every attempt must carry its own timeout, or one hung attempt hangs the loop. - [Circuit Breaker](https://aipatterns.dev/design-patterns/circuit-breaker.md) counts failures to decide when to stop calling; timeouts must be classified as failures or hangs never trip it. - [Exponential Backoff](https://aipatterns.dev/design-patterns/exponential-backoff.md) spaces re-attempts; the timeout bounds each attempt within the schedule. - [Fallback](https://aipatterns.dev/design-patterns/fallback.md) is what the caller serves when the deadline expires with no answer. - [Decorator](https://aipatterns.dev/design-patterns/decorator.md) is the shape the wrapper takes: same interface, added behaviour, composed at the call site. ## Agent checklist - [ ] Every remote call in the changed path has an explicit, written-down deadline. - [ ] The deadline is shorter than the caller's own budget, including downstream calls it triggers. - [ ] Expiry cancels the underlying work through the platform primitive, not just the wait. - [ ] Timeout is a distinct error type callers branch on, not a generic `Error`. - [ ] The timeout path has a test with a dependency that sleeps past the deadline. --- # Circuit Breaker > Count consecutive failures to a dependency, refuse calls while it looks broken, and let one probe test recovery - so a dead service cannot eat its callers. - kind: pattern - category: resilience - aliases: Breaker - canonical: https://aipatterns.dev/design-patterns/circuit-breaker - inspired-by: https://learn.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker ## Intent Wrap calls to a dependency in a small state machine. While closed, calls pass and failures are counted. After failures cross a threshold the breaker opens: calls fail immediately without touching the dependency, giving it room to recover and freeing the callers. After a cool-off, the breaker half-opens and lets one probe through; success closes it again, failure reopens it. ## Forces - **Hopeful retries** vs **retry storms**: when every caller retries a failing dependency, each one multiplies the load exactly when the dependency is least able to serve it. - **Fail fast** vs **fail wrongly**: while open, callers get instant errors - including for work that might have succeeded. - **Sensitivity** vs **flapping**: a threshold too low trips on single blips; too high lets a dead dependency consume pools and threads first. - **Shared state** vs **local view**: with ten instances, one instance's closed breaker says nothing about what the other nine saw. ## Therefore **Track recent failures per dependency and trip into an open state at a threshold, fail calls fast while open, then admit a limited probe after a cool-off to test recovery.** The breaker is a [State](https://aipatterns.dev/design-patterns/state.md) machine with three states - closed, open, half-open - sitting in front of one dependency, the same position a [Proxy](https://aipatterns.dev/design-patterns/proxy.md) takes. While open it raises a distinct error so callers can react: serve a [Fallback](https://aipatterns.dev/design-patterns/fallback.md), queue the work, or surface a clean "temporarily unavailable". ## Structure ```mermaid stateDiagram-v2 [*] --> closed closed --> open : consecutive failures reach threshold open --> halfOpen : cool-off elapses halfOpen --> closed : probe succeeds halfOpen --> open : probe fails ``` ### Participants - **Breaker** (`Breaker`): the guard. Holds the state, the failure count, the threshold, the cool-off, and the clock. Raises `CircuitOpenError` while open. - **Dependency** (`PaymentGateway`): the remote call the breaker guards. One breaker per dependency and operation. - **Caller**: calls the breaker as if it were the dependency; on `CircuitOpenError` it degrades instead of waiting. - **Clock** (`now`): injected so transitions are testable without sleeping. ## Use it when - A dependency fails in episodes - a bad deploy, a stalled database, an overload - and callers would otherwise wait or retry through the whole episode. - Callers pile up behind a failing dependency: threads held, connection pools drained, latency cascading upstream. - You need the dependency to be left alone to recover: open means zero traffic, which is the only thing that lets an overloaded service recover. - A burst of failures means "stop" rather than "try again" - paying before you know the gateway is down is worse than failing the checkout. ## Do not use it when - The failure is rare and returns instantly. An occasional 500 that comes back in 30 ms costs nothing; a retry handles it. A breaker adds state to guard a problem you do not have. - The operation must be attempted (or explicitly deferred) for correctness - capturing a payment. Failing fast to the user is wrong; queue the work with [Command](https://aipatterns.dev/design-patterns/command.md) and a durable queue instead. - Calls are local and cannot hang. No network, no failure mode worth a state machine. - Your framework ships one ([Resilience4j](https://resilience4j.readme.io), .NET `Microsoft.Extensions.Http.Resilience`, Go libraries). Configure it; the entry below shows what it does inside, not that you should hand-roll it in production paths. - Every failure means "this request is bad" (validation). The breaker would open on your clients' bugs. Breakers are for dependencies failing, not callers. ## Application grill **Inspect first:** the calls to the failing dependency and the client library wrapping them; whether one process or many instances serve the traffic, which decides per-instance versus shared breaker state; existing resilience libraries before adding a second. **Missing pieces:** single instance: a small in-process breaker (a counter and three states) or a library (opossum in Node, resilience4j on the JVM, Polly in .NET). Many instances tripping independently still hammer the dependency as a herd; shared state in Redis makes them open together and adds a datastore with its own failure modes - start per-instance. **Ask before you build:** - **Scale:** how many instances run, and is per-instance tripping acceptable or must the fleet open as one? - **Performance:** what latency counts as a failure - is there a per-dependency timeout budget feeding the breaker? - **Reliability:** is fail-fast acceptable to callers while open, and what does the single half-open probe test? - **Budget:** is shared breaker state worth a second datastore, or does per-instance cover the failure pattern? - **Implementation:** how do on-call engineers see that a breaker is open (a metric, a log, an alert)? ## Implementation steps 1. Give the wrapped calls deadlines first ([Timeout](https://aipatterns.dev/design-patterns/timeout.md)); a hang must count as a failure or the breaker never trips. 2. Classify errors: count dependency failures (5xx, timeouts, connection errors); do not count caller errors (4xx, business rejections). 3. Store per breaker: state, consecutive failure count, threshold (5 is a common start), cool-off (10–60 s), and the time it opened. Key it by dependency and operation - the payment breaker must not trip because shipping is down. 4. On failure in closed state, increment; at the threshold, open and record the time. On success, reset the count. 5. While open, fail immediately with a distinct `CircuitOpenError` - do not touch the dependency, do not let callers retry it. 6. After the cool-off, admit a limited probe (one call, or a small rate): success closes the breaker and resets the count; failure reopens and restarts the cool-off. 7. Decide the state's scope: per process instance is simplest and each instance trips independently; a shared store gives one view at the cost of a read on every call. 8. Log or emit a metric on every transition; an open breaker is an incident signal, not a silent optimization. ## Example Checkout charges a payment gateway. The gateway's deploy is broken from t=1000 to t=2000 on our injected clock. Two failed charges trip the breaker; the third caller fails instantly without touching the gateway; after the 5-second cool-off a probe succeeds and traffic resumes. ```ts title="payments.ts" interface PaymentGateway { charge(cents: number): string; } class CircuitOpenError extends Error { constructor() { super("circuit open: dependency is down"); this.name = "CircuitOpenError"; } } // The dependency: 503 while its deploy is broken, then it recovers. class FlakyGateway implements PaymentGateway { constructor( private readonly brokenUntil: number, private readonly now: () => number, ) {} charge(cents: number): string { if (this.now() < this.brokenUntil) throw new Error("503 unavailable"); return `charged ${cents}`; } } class Breaker implements PaymentGateway { private state: "closed" | "open" | "halfOpen" = "closed"; private failures = 0; private openedAt = 0; constructor( private readonly inner: PaymentGateway, private readonly threshold: number, // consecutive failures before opening private readonly coolOffMs: number, // how long the breaker stays open private readonly now: () => number = Date.now, ) {} charge(cents: number): string { if (this.state === "open") { if (this.now() - this.openedAt < this.coolOffMs) throw new CircuitOpenError(); // fail fast: no call is made this.state = "halfOpen"; // cool-off over: this call is the probe } try { const result = this.inner.charge(cents); this.failures = 0; this.state = "closed"; return result; } catch (e) { const probe = this.state === "halfOpen"; this.state = "closed"; if (probe || ++this.failures >= this.threshold) this.trip(); throw e; } } private trip() { this.state = "open"; this.openedAt = this.now(); this.failures = 0; console.log(` [breaker] open at t=${this.openedAt}`); } } let clock = 1_000; const breaker = new Breaker(new FlakyGateway(2_000, () => clock), 2, 5_000, () => clock); try { breaker.charge(500); } catch {} // 503, failure 1 try { breaker.charge(500); } catch {} // 503, failure 2 -> opens try { breaker.charge(500); } catch (e) { console.log(`${(e as Error).name}: no gateway call was made`); // CircuitOpenError: no gateway call was made } clock += 6_000; // cool-off (5 s) has passed by t=7000 console.log(breaker.charge(500)); // probe succeeds -> closed, charged 500 ``` ```python title="payments.py" from typing import Callable, Protocol class CircuitOpenError(Exception): pass class PaymentGateway(Protocol): def charge(self, cents: int) -> str: ... class FlakyGateway: """503 while its deploy is broken, then it recovers.""" def __init__(self, broken_until: int, now: Callable[[], int]) -> None: self._broken_until, self._now = broken_until, now def charge(self, cents: int) -> str: if self._now() < self._broken_until: raise RuntimeError("503 unavailable") return f"charged {cents}" class Breaker: def __init__( self, inner: PaymentGateway, threshold: int, cool_off_ms: int, now: Callable[[], int], ) -> None: self._inner, self._threshold = inner, threshold self._cool_off, self._now = cool_off_ms, now self._state, self._failures, self._opened_at = "closed", 0, 0 def charge(self, cents: int) -> str: if self._state == "open": if self._now() - self._opened_at < self._cool_off: raise CircuitOpenError("circuit open: dependency is down") self._state = "halfOpen" # cool-off over: this call is the probe try: result = self._inner.charge(cents) self._failures, self._state = 0, "closed" return result except Exception as e: probe = self._state == "halfOpen" self._state = "closed" self._failures += 1 if probe or self._failures >= self._threshold: self._trip() raise e def _trip(self) -> None: self._state, self._opened_at, self._failures = "open", self._now(), 0 print(f" [breaker] open at t={self._opened_at}") clock = 1_000 now = lambda: clock # injected clock; reassigning clock moves time for both objects breaker = Breaker( FlakyGateway(broken_until=2_000, now=now), threshold=2, cool_off_ms=5_000, now=now ) for _ in range(2): try: breaker.charge(500) except RuntimeError: pass # 503, failures 1 and 2 -> opens try: breaker.charge(500) except CircuitOpenError as e: print(f"{type(e).__name__}: no gateway call was made") clock += 6_000 # cool-off (5 s) has passed print(breaker.charge(500)) # probe succeeds -> closed, charged 500 ``` ```go title="payments.go" package main import ( "errors" "fmt" "time" ) type PaymentGateway interface { Charge(cents int) (string, error) } var errUnavailable = errors.New("503 unavailable") // FlakyGateway 503s while its deploy is broken, then recovers. type FlakyGateway struct { brokenUntil time.Time now func() time.Time } func (g FlakyGateway) Charge(cents int) (string, error) { if g.now().Before(g.brokenUntil) { return "", errUnavailable } return fmt.Sprintf("charged %d", cents), nil } // ErrCircuitOpen is returned while the breaker is open. Callers must not retry it. var ErrCircuitOpen = errors.New("circuit open: dependency is down") type breakerState int const ( closed breakerState = iota open halfOpen ) type Breaker struct { inner PaymentGateway threshold int coolOff time.Duration now func() time.Time state breakerState failures int openedAt time.Time } func NewBreaker(inner PaymentGateway, threshold int, coolOff time.Duration, now func() time.Time) *Breaker { return &Breaker{inner: inner, threshold: threshold, coolOff: coolOff, now: now} } func (b *Breaker) Charge(cents int) (string, error) { if b.state == open { if b.now().Sub(b.openedAt) < b.coolOff { return "", ErrCircuitOpen // fail fast: no call is made } b.state = halfOpen // cool-off over: this call is the probe } result, err := b.inner.Charge(cents) probe := b.state == halfOpen b.state = closed switch { case err == nil: b.failures = 0 case probe: b.trip() default: b.failures++ if b.failures >= b.threshold { b.trip() } } return result, err } func (b *Breaker) trip() { b.state, b.openedAt, b.failures = open, b.now(), 0 fmt.Printf(" [breaker] open at t=%d\n", b.openedAt.UnixMilli()) } func main() { clock := time.UnixMilli(1_000) now := func() time.Time { return clock } breaker := NewBreaker( FlakyGateway{brokenUntil: time.UnixMilli(2_000), now: now}, 2, 5*time.Second, now) for i := 0; i < 2; i++ { breaker.Charge(500) // 503, failures 1 and 2 -> opens } if _, err := breaker.Charge(500); errors.Is(err, ErrCircuitOpen) { fmt.Println("CircuitOpenError: no gateway call was made") } clock = clock.Add(6 * time.Second) // cool-off (5 s) has passed if result, err := breaker.Charge(500); err == nil { fmt.Println(result) // probe succeeds -> closed, charged 500 } } ``` ```java title="Payments.java" import java.time.Instant; import java.util.function.LongSupplier; public class Payments { interface PaymentGateway { String charge(int cents); } static final class CircuitOpenException extends RuntimeException { CircuitOpenException() { super("circuit open: dependency is down"); } } /** 503 while its deploy is broken, then it recovers. */ record FlakyGateway(long brokenUntil, LongSupplier now) implements PaymentGateway { public String charge(int cents) { if (now.getAsLong() < brokenUntil) throw new IllegalStateException("503 unavailable"); return "charged " + cents; } } static final class Breaker implements PaymentGateway { private enum State { CLOSED, OPEN, HALF_OPEN } private final PaymentGateway inner; private final int threshold; private final long coolOffMs; private final LongSupplier now; private State state = State.CLOSED; private int failures = 0; private long openedAt = 0; Breaker(PaymentGateway inner, int threshold, long coolOffMs, LongSupplier now) { this.inner = inner; this.threshold = threshold; this.coolOffMs = coolOffMs; this.now = now; } public String charge(int cents) { if (state == State.OPEN) { if (now.getAsLong() - openedAt < coolOffMs) throw new CircuitOpenException(); // fail fast: no call is made state = State.HALF_OPEN; // cool-off over: this call is the probe } try { String result = inner.charge(cents); failures = 0; state = State.CLOSED; return result; } catch (RuntimeException e) { boolean probe = state == State.HALF_OPEN; state = State.CLOSED; if (probe || ++failures >= threshold) trip(); throw e; } } private void trip() { state = State.OPEN; openedAt = now.getAsLong(); failures = 0; System.out.println(" [breaker] open at t=" + openedAt); } } public static void main(String[] args) { long[] clock = {1_000}; Breaker breaker = new Breaker( new FlakyGateway(2_000, () -> clock[0]), 2, 5_000, () -> clock[0]); for (int i = 0; i < 2; i++) { try { breaker.charge(500); } catch (RuntimeException ignored) { } // 503 x2 -> opens } try { breaker.charge(500); } catch (CircuitOpenException e) { System.out.println("CircuitOpenException: no gateway call was made"); } clock[0] += 6_000; // cool-off (5 s) has passed System.out.println(breaker.charge(500)); // probe succeeds -> closed, charged 500 } } ``` In concurrent code the same logic needs a lock (or atomics) around the state, and "one probe" must mean one at a time: admit a single in-flight probe and reject or queue the rest until it resolves. ## Trade-offs **Gains** - A failing dependency stops receiving traffic, which is often the only condition under which it recovers. - Callers fail in microseconds instead of queuing behind a dead service. - `CircuitOpenError` makes the outage explicit and branchable. **Costs** - State must be scoped and, under concurrency, synchronised; instances trip independently unless you share state. - Thresholds and cool-offs need tuning per dependency; wrong ones flap or open too late. - While open, work that would have succeeded is rejected. - A probe can re-trip the breaker and extend the outage window. ## Relations - [State](https://aipatterns.dev/design-patterns/state.md): the breaker is a three-state machine; this entry implements it with an enum because the states are few, but the State pattern applies as transitions grow. - [Proxy](https://aipatterns.dev/design-patterns/proxy.md): the breaker stands in front of the dependency with the same interface; unlike a classic proxy it is usually applied by the caller, like a [Decorator](https://aipatterns.dev/design-patterns/decorator.md). - [Timeout](https://aipatterns.dev/design-patterns/timeout.md): feeds it - a hang is a failure, so calls need deadlines before the breaker can count them. - [Retry](https://aipatterns.dev/design-patterns/retry.md): a retry loop must treat `CircuitOpenError` as terminal; retrying an open breaker is the storm the breaker exists to stop. - [Fallback](https://aipatterns.dev/design-patterns/fallback.md): what callers serve while the breaker is open. - [Command](https://aipatterns.dev/design-patterns/command.md): work that must not be dropped while the breaker is open goes to a durable queue instead of the caller's error path. ## Agent checklist - [ ] One breaker per dependency and operation, not one global breaker. - [ ] Calls carry timeouts first, so hangs count as failures. - [ ] Caller errors (4xx, validation) do not advance the failure count. - [ ] `CircuitOpenError` is a distinct type, and no retry loop in the path retries it. - [ ] The cool-off admits a limited probe, and success resets the failure count. - [ ] Transitions are logged or emitted as metrics, and the state's scope (per instance or shared) is a written decision. --- # Retry > Re-attempt only retryable, repeatable operations a bounded number of times, honour Retry-After, check cancellation between attempts, and surface the last error. - kind: pattern - category: resilience - aliases: Retry Policy, Automatic Retry - canonical: https://aipatterns.dev/design-patterns/retry - inspired-by: https://learn.microsoft.com/en-us/azure/architecture/patterns/retry ## Intent Some failures are transient: a 503 during a deploy, a 429 from a rate limiter, a connection dropped mid-handshake. Repeating the operation after a short wait turns them into successes. A retry policy decides which errors qualify, how many attempts are allowed, how much total time may be spent, and how the loop stops - including when the caller cancels. ## Forces - **Transient** vs **permanent**: a 503 usually heals; a 400 never will. Retrying permanent errors wastes time and hides the bug. - **Persistence** vs **amplification**: every retry adds load to a dependency that is already struggling; N callers retrying M times are N×M requests. - **Idempotency** vs **duplication**: if you cannot tell whether attempt one landed, repeating it may charge the card twice. - **Keep trying** vs **keep waiting**: without a deadline, a retry loop is a hang with extra steps. ## Therefore **Classify the error, and only for retryable ones repeat a bounded number of attempts with spacing, within an overall deadline, checking cancellation between attempts - then fail with the last error.** The naive version - `for i in range(3): try: call()` with no error classification, no spacing, no deadline, and no cancellation check - retries permanent failures, hammers the dependency at full speed, and ignores the caller giving up. Every one of those four omissions is a production incident. ## Structure ```mermaid sequenceDiagram participant C as Caller participant R as Retry loop participant D as Dependency C->>R: deliver(event, signal) R->>D: attempt 1 D-->>R: 503, Retry-After 10 ms R->>R: retryable? budget left? not cancelled? R->>D: attempt 2 (after wait) D-->>R: 503, Retry-After 10 ms R->>D: attempt 3 (after wait) D-->>R: delivered R-->>C: done ``` ### Participants - **Retry loop** (`deliverWithRetry`): owns the policy - retryable classification, `maxAttempts`, deadline, wait function, cancellation checks. - **Operation** (`deliver`): the repeatable call. Must be idempotent or safely repeatable, and cancellable. - **Dependency** (`RemoteEndpoint`): may answer with transient failures, ideally with `Retry-After`. - **Caller**: passes a cancellation signal and receives the last error when the policy gives up. ## Use it when - The error is transient-looking - timeouts, connection resets, 429, 502, 503 - and the operation can be repeated safely. - The operation is idempotent by nature (reads, conditional writes) or made repeatable with an idempotency key. - A platform SDK documents transient failures and expects callers to retry (most cloud SDKs do). - The caller can afford the added latency, or the work is background where latency is free. ## Do not use it when - The operation is not repeatable and has no idempotency key. A retried charge or a retried "create user" duplicates side effects; make it repeatable first ([Command](https://aipatterns.dev/design-patterns/command.md) plus a durable queue with deduplication) or fail. - The error is permanent: validation failures, missing resources, authorization denials, business rejections. Fix the input; do not repeat it. - The dependency is down for an episode, not blipping. Retrying through an outage piles on load; a [Circuit Breaker](https://aipatterns.dev/design-patterns/circuit-breaker.md) must stop the loop instead. - The caller is interactive with a 1–2 s budget. Two attempts at most, then [Fallback](https://aipatterns.dev/design-patterns/fallback.md) or fail. - The HTTP client or SDK already retries. Wrapping a second loop around a built-in one multiplies attempts; configure one policy instead. - The failure is a bug (a `TypeError`, a nil dereference). Retrying hides it and slows every future failure. ## Application grill **Inspect first:** the operations and whether each is idempotent and repeatable - that gate decides what may retry; the retry knobs already set (HTTP client defaults, SDK policies, queue visibility timeouts) that a second layer would multiply; how cancellation propagates between attempts. **Missing pieces:** a small shared helper is enough; a library (p-retry, tenacity, resilience4j) pays off only when policy must be uniform across many call sites. Honoring Retry-After needs no infrastructure. **Ask before you build:** - **Scale:** what bounded attempt count, times callers, times failure rate, can the dependency absorb during its incident? - **Performance:** does the retry sequence fit the caller's remaining deadline after the first attempt? - **Reliability:** which errors are retryable (timeouts, 5xx) versus permanent (4xx), and where is that classified? - **Budget:** stacked retries across layers multiply load in exactly the incident window - who audits the layers? - **Implementation:** is the final failure surfaced with its attempt count and last error, and are retries marked in metrics? ## Implementation steps 1. Make the operation repeatable: idempotency key on writes, conditional update, or an inherent read. If you cannot, stop here. 2. Classify errors explicitly. Retryable: timeouts, connection resets, 429, 502, 503. Terminal: 4xx (except 429), business errors, programming errors. Write the classifier as code, not as a habit. 3. Fix the bounds: `maxAttempts` (3–5), an overall deadline, and a per-attempt timeout ([Timeout](https://aipatterns.dev/design-patterns/timeout.md)). Total wait is capped by the deadline, not by the schedule. 4. Space the attempts (a fixed short delay, or the server's `Retry-After`). The schedule discipline is [Exponential Backoff](https://aipatterns.dev/design-patterns/exponential-backoff.md). 5. Check cancellation before every wait and every attempt: an aborted signal must end the loop promptly instead of starting attempt N+1. 6. On give-up, surface the last error with the attempt count attached, so logs tell the whole story. 7. Emit metrics: attempts per call, outcomes, and give-up reasons. A rising retry rate is the first signal of a dependency degrading. ## Example A webhook delivery must survive a dependency that returns 503 with `Retry-After` while it restarts. Deliveries carry an idempotency key, attempts are bounded by count and by deadline, and a cancellation signal stops the loop between attempts. ```ts title="deliver.ts" // Errors the dependency raises; status decides retryability, not the message. type HttpError = Error & { status: number; retryAfterMs: number | null }; const statusOf = (e: unknown) => (e as HttpError)?.status; const isRetryable = (e: unknown) => [429, 500, 502, 503].includes(statusOf(e)); const sleep = (ms: number, signal: AbortSignal) => new Promise((resolve, reject) => { const t = setTimeout(resolve, ms); signal.addEventListener( "abort", () => { clearTimeout(t); reject(new Error("cancelled")); }, { once: true }, ); }); interface DeliveryTarget { deliver(event: string, idempotencyKey: string): Promise; } // The dependency: 503 with Retry-After while it restarts, then it serves. class RemoteEndpoint implements DeliveryTarget { constructor(private failuresLeft: number) {} async deliver(event: string): Promise { if (this.failuresLeft-- > 0) { const e = new Error("503 restarting") as HttpError; e.status = 503; e.retryAfterMs = 10; throw e; } console.log(` [endpoint] delivered ${event}`); } } async function deliverWithRetry( target: DeliveryTarget, event: string, opts: { maxAttempts: number; deadlineMs: number; signal: AbortSignal }, ): Promise { const startedAt = Date.now(); let lastError: unknown; for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) { if (opts.signal.aborted) throw new Error("cancelled before attempt"); try { await target.deliver(event, `evt:${event}:1`); // idempotency key return; } catch (e) { lastError = e; const wait = (e as HttpError).retryAfterMs ?? 10; // server hint first const withinBudget = Date.now() - startedAt + wait < opts.deadlineMs && isRetryable(e); if (!withinBudget) throw e; // give up: last error carries the story console.log( ` [retry] attempt ${attempt} failed (${(e as Error).message}); waiting ${wait} ms`, ); await sleep(wait, opts.signal); } } throw lastError; } const endpoint = new RemoteEndpoint(2); await deliverWithRetry(endpoint, "order.paid", { maxAttempts: 4, deadlineMs: 1_000, signal: AbortSignal.timeout(2_000), }); // 2 failed attempts, then: [endpoint] delivered order.paid try { await deliverWithRetry(new RemoteEndpoint(99), "order.refunded", { maxAttempts: 4, deadlineMs: 50, // budget runs out before the endpoint recovers signal: AbortSignal.timeout(2_000), }); } catch (e) { console.log(`gave up: ${(e as Error).message}`); // gave up: 503 restarting } ``` ```python title="deliver.py" import time from typing import Callable class HttpError(Exception): def __init__(self, status: int, retry_after_ms: int | None = None) -> None: super().__init__(f"{status}") self.status, self.retry_after_ms = status, retry_after_ms RETRYABLE = {429, 500, 502, 503} class RemoteEndpoint: """503 with Retry-After while it restarts, then it serves.""" def __init__(self, failures_left: int) -> None: self._failures_left = failures_left def deliver(self, event: str, idempotency_key: str) -> None: if self._failures_left > 0: self._failures_left -= 1 raise HttpError(503, retry_after_ms=10) print(f" [endpoint] delivered {event}") def deliver_with_retry( target: RemoteEndpoint, event: str, max_attempts: int, deadline_ms: int, cancelled: Callable[[], bool], ) -> None: started_at = time.monotonic() last_error: Exception | None = None for attempt in range(1, max_attempts + 1): if cancelled(): # cancellation beats retrying raise InterruptedError("cancelled before attempt") try: target.deliver(event, f"evt:{event}:1") # idempotency key return except HttpError as e: last_error = e wait = e.retry_after_ms if e.retry_after_ms is not None else 10 in_budget = ( (time.monotonic() - started_at) * 1000 + wait < deadline_ms and e.status in RETRYABLE ) if not in_budget: raise # give up: the last error carries the story print(f" [retry] attempt {attempt} failed ({e}); waiting {wait} ms") time.sleep(wait / 1000) raise last_error # type: ignore[misc] deliver_with_retry(RemoteEndpoint(failures_left=2), "order.paid", max_attempts=4, deadline_ms=1_000, cancelled=lambda: False) # 2 failed attempts, then: [endpoint] delivered order.paid try: deliver_with_retry(RemoteEndpoint(failures_left=99), "order.refunded", max_attempts=4, deadline_ms=50, cancelled=lambda: False) except HttpError as e: print(f"gave up: {e}") # gave up: 503 ``` ```go title="deliver.go" package main import ( "context" "errors" "fmt" "net/http" "time" ) // HTTPError carries the status and the server's Retry-After hint. type HTTPError struct { Status int RetryAfter time.Duration } func (e HTTPError) Error() string { return fmt.Sprintf("%d", e.Status) } func retryable(status int) bool { return status == http.StatusTooManyRequests || status >= 500 } // RemoteEndpoint 503s with Retry-After while it restarts, then it serves. type RemoteEndpoint struct{ failuresLeft int } func (r *RemoteEndpoint) Deliver(ctx context.Context, event, idempotencyKey string) error { if r.failuresLeft > 0 { r.failuresLeft-- return HTTPError{Status: 503, RetryAfter: 10 * time.Millisecond} } fmt.Println(" [endpoint] delivered", event) return nil } func deliverWithRetry(ctx context.Context, target *RemoteEndpoint, event string) error { startedAt := time.Now() var lastErr error for attempt := 1; attempt <= 4; attempt++ { if err := ctx.Err(); err != nil { // cancellation beats retrying return err } err := target.Deliver(ctx, event, "evt:"+event+":1") // idempotency key if err == nil { return nil } var httpErr HTTPError lastErr = err wait := 10 * time.Millisecond if errors.As(err, &httpErr) { if httpErr.RetryAfter > 0 { // server hint first wait = httpErr.RetryAfter } if !retryable(httpErr.Status) { return err // permanent: do not repeat } } if time.Since(startedAt)+wait > time.Second { return err // overall deadline exceeded: give up } fmt.Printf(" [retry] attempt %d failed (%v); waiting %v\n", attempt, err, wait) select { case <-time.After(wait): case <-ctx.Done(): // cancelled mid-wait return ctx.Err() } } return lastErr } func main() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := deliverWithRetry(ctx, &RemoteEndpoint{failuresLeft: 2}, "order.paid"); err != nil { fmt.Println("err:", err) } // 2 failed attempts, then: [endpoint] delivered order.paid err := deliverWithRetry(ctx, &RemoteEndpoint{failuresLeft: 99}, "order.refunded") fmt.Println("gave up:", err) // gave up: 503 } ``` ```java title="Deliver.java" public class Deliver { static final class HttpException extends RuntimeException { final int status; final long retryAfterMs; HttpException(int status, long retryAfterMs) { super(String.valueOf(status)); this.status = status; this.retryAfterMs = retryAfterMs; } } /** 503 with Retry-After while it restarts, then it serves. */ static final class RemoteEndpoint { private int failuresLeft; RemoteEndpoint(int failuresLeft) { this.failuresLeft = failuresLeft; } void deliver(String event, String idempotencyKey) { if (failuresLeft > 0) { failuresLeft--; throw new HttpException(503, 10); } System.out.println(" [endpoint] delivered " + event); } } static void deliverWithRetry(RemoteEndpoint target, String event, int maxAttempts, long deadlineMs) throws InterruptedException { long startedAt = System.nanoTime(); RuntimeException lastError = null; for (int attempt = 1; attempt <= maxAttempts; attempt++) { if (Thread.currentThread().isInterrupted()) // cancellation beats retrying throw new InterruptedException("cancelled before attempt"); try { target.deliver(event, "evt:" + event + ":1"); // idempotency key return; } catch (HttpException e) { lastError = e; long wait = e.retryAfterMs() > 0 ? e.retryAfterMs() : 10; // hint first boolean retryable = e.status() == 429 || e.status() >= 500; boolean withinBudget = (System.nanoTime() - startedAt) / 1_000_000 + wait < deadlineMs; if (!retryable || !withinBudget) throw e; // give up System.out.println(" [retry] attempt " + attempt + " failed (" + e.getMessage() + "); waiting " + wait + " ms"); Thread.sleep(wait); } } throw lastError; } public static void main(String[] args) throws InterruptedException { deliverWithRetry(new RemoteEndpoint(2), "order.paid", 4, 1_000); // 2 failed attempts, then: [endpoint] delivered order.paid try { deliverWithRetry(new RemoteEndpoint(99), "order.refunded", 4, 50); } catch (HttpException e) { System.out.println("gave up: " + e.getMessage()); // gave up: 503 } } } ``` The fixed 10 ms wait keeps this example honest about scope; in production that wait comes from the backoff schedule. ## Trade-offs **Gains** - Transient failures disappear before anyone notices. - The policy is one place: classification, bounds, and waits are testable and consistent. - Giving up is loud: the last error plus attempt count tells the operator what happened. **Costs** - Load multiplies exactly when the dependency is least able to serve it. - Duplicate side effects when idempotency was assumed instead of built. - Latency grows: a caller that would fail in 100 ms now fails in attempts × waits. - Two stacked retry layers (client library plus hand-rolled loop) multiply attempts silently. ## Relations - [Exponential Backoff](https://aipatterns.dev/design-patterns/exponential-backoff.md) is the spacing policy this entry's waits should use in production. - [Timeout](https://aipatterns.dev/design-patterns/timeout.md) bounds each attempt; an attempt without a deadline can hang the whole loop. - [Circuit Breaker](https://aipatterns.dev/design-patterns/circuit-breaker.md) stops the loop when failures stop being transient; retrying an open breaker is the storm it prevents. - [Rate Limiting](https://aipatterns.dev/design-patterns/rate-limiting.md) turns `429` into a schedule: its `Retry-After` is this policy's input, and a client-side budget prevents the refusals in the first place. - [Fallback](https://aipatterns.dev/design-patterns/fallback.md) is the caller's plan when attempts run out. - [Command](https://aipatterns.dev/design-patterns/command.md) moves delivery to a durable queue when the work must happen eventually but not synchronously. - [Decorator](https://aipatterns.dev/design-patterns/decorator.md) is the implementation shape: wrap the flaky operation, expose the same interface. ## Agent checklist - [ ] The operation is idempotent or made repeatable with an idempotency key before any retry exists. - [ ] Retryable and terminal errors are classified in code, not by instinct; 4xx validation errors are never retried. - [ ] Attempts are bounded by both count and overall deadline, and each attempt has its own timeout. - [ ] Waits honor the server's `Retry-After` when present. - [ ] Cancellation is checked before every attempt and wait, and mid-wait too. - [ ] Giving up raises the last error with the attempt count, and retry rates are measurable. --- # Exponential Backoff > Space repeat attempts at base times two raised to the attempt, capped, with random jitter - so retries spread out instead of arriving in a synchronized herd. - kind: pattern - category: resilience - aliases: Backoff, Jittered Backoff - canonical: https://aipatterns.dev/design-patterns/exponential-backoff - inspired-by: https://learn.microsoft.com/en-us/azure/architecture/best-practices/transient-faults ## Intent When an operation is repeated, double the wait each time: 100 ms, 200 ms, 400 ms, capped at a few seconds. Then randomize each wait within its range (jitter), because a thousand clients that all failed together will otherwise all retry together - reproducing the exact spike that caused the failure. ## Forces - **Retry now** vs **thundering herd**: synchronized retries hit the recovering dependency as a second outage. - **Back off** vs **never finish**: pure doubling eventually schedules multi-minute waits; a cap keeps retries finishable. - **Fairness** vs **starvation**: without jitter, the client that failed first always retries first, every round. - **Compute a schedule** vs **obey the server**: a `Retry-After` header is a fact; the computed delay is a guess. ## Therefore **Wait `random(0, min(cap, base × 2^attempt))` before each repeat - full jitter over a doubling, capped range - and stop when attempts or the deadline run out.** The doubling matches how outages actually recover: most blips clear in the first second, so early attempts come quickly; genuine outages need the later, longer gaps. The jitter converts one synchronized spike into a spread. Fixed delays and zero jitter are the two defaults that turn a retry policy into a self-inflicted denial of service. ## Structure ```mermaid flowchart TD A["attempt"] --> B{"succeeded?"} B -- "yes" --> C["return"] B -- "no" --> D{"attempts or deadline exhausted?"} D -- "yes" --> E["fail with last error"] D -- "no" --> F["delay = random(0, min(cap, base x 2^n))"] F --> G["sleep, honouring Retry-After if sent"] G --> A ``` ### Participants - **Schedule** (`backoffDelay`): a pure function from attempt number to delay. Deterministic given its RNG, so tests can assert it. - **Loop**: the [Retry](https://aipatterns.dev/design-patterns/retry.md) policy that consults the schedule, checks its budget, and sleeps. - **Dependency**: the failing call; ideally it sends `Retry-After`, which overrides the computed delay. ## Use it when - Any automatic retry loop spans more than one attempt - this is the default spacing for [Retry](https://aipatterns.dev/design-patterns/retry.md). - A job or queue consumer re-runs failed work on a schedule. - You poll a not-yet-ready resource (a job status endpoint, a deployment). - Many clients call one rate-limited or capacity-limited API. ## Do not use it when - The server sent `Retry-After`. Obey it; the server knows its own recovery better than your formula does. - The caller is interactive with a 1–2 s budget. The schedule outgrows the budget after the first doubling; retry once or fail instead. - The error is permanent. No spacing fixes a 400; do not schedule retries for it. - There is exactly one caller and no contention: the jitter buys nothing, though the doubling still helps. - You are the dependency being hammered. Backoff in clients does not fix your capacity; throttle or shed load at the source. ## Application grill **Inspect first:** every retry site and its current spacing; whether the HTTP client or SDK already retries with its own backoff - a second layer multiplies attempts silently; the platform's own retries (load balancers, service meshes, queue redeliveries). **Missing pieces:** none beyond a helper: delay = min(cap, base * 2^attempt) with jitter. No library is needed; if the codebase adopts one (p-retry, tenacity), keep exactly one. **Ask before you build:** - **Scale:** what total attempt count across all callers can the recovering dependency absorb (cap times callers)? - **Performance:** does the backoff sequence fit the caller's own deadline - timeouts shorten the room backoff has? - **Reliability:** full jitter or decorrelated jitter - is a synchronized herd acceptable during recovery windows? - **Budget:** is the policy one shared helper or drifting per call site - who audits when an incident needs a global change? - **Implementation:** are attempt numbers logged so retry storms are visible in the metrics that already exist? ## Implementation steps 1. Pick `base` near the dependency's normal response time - 100 ms for APIs, seconds for batch systems. 2. Compute the raw delay as `base × 2^attempt` and cap it (1–5 s for interactive paths, minutes for batch). 3. Apply full jitter: sleep `random(0, min(cap, raw))`. Decorrelated jitter (`random(base, last_delay × 3)` capped) is an alternative when delays should not shrink across attempts. 4. Bound the loop with max attempts (3–5 interactive) and an overall deadline; the cap plus the deadline decides when to stop, not the exponent. 5. Let `Retry-After` override the computed delay, capped by the deadline. 6. Keep each attempt's own [Timeout](https://aipatterns.dev/design-patterns/timeout.md): the schedule bounds the gaps, not the attempts. 7. Seed the RNG in tests so schedules are assertable; inject the sleep in tests so they do not actually wait. ## Example Three workers hit a rate-limited API and fail together. With a fixed delay they would all retry on the same millisecond, every round. Full jitter spreads them; the cap keeps every wait finishable. The output below shows the schedule each worker computes for its first five attempts. ```ts title="backoff.ts" // Full jitter: uniform over the capped doubling range. export function backoffDelay( attempt: number, // 0-based baseMs: number, capMs: number, rand: () => number, ): number { return Math.floor(rand() * Math.min(capMs, baseMs * 2 ** attempt)); } // Deterministic MINSTD generator so the demo output is reproducible. // In production: Math.random. (48271 keeps the math exact in 64-bit floats.) const rng = (seed: number) => () => (seed = (seed * 48271) % 2147483647) / 2147483647; const BASE = 100; const CAP = 2_000; const raw = [0, 1, 2, 3, 4].map((n) => Math.min(CAP, BASE * 2 ** n)); console.log(`no jitter, every worker: ${raw.join(", ")} ms`); for (const [i, worker] of ["worker-1", "worker-2", "worker-3"].entries()) { const rand = rng((i + 1) * 7919); // one stream per worker const waits = [0, 1, 2, 3, 4].map((n) => backoffDelay(n, BASE, CAP, rand)); console.log(`${worker}: ${waits.join(", ")} ms`); } // worker-1: 17, 74, 44, 535, 385 ms // worker-2: 35, 148, 89, 271, 771 ms <- never in lockstep with worker-1 // worker-3: 53, 23, 133, 7, 1157 ms <- waits may shrink; only the range is fixed ``` ```python title="backoff.py" def backoff_delay(attempt: int, base_ms: int, cap_ms: int, rand) -> int: """Full jitter: uniform over the capped doubling range.""" return int(rand() * min(cap_ms, base_ms * 2**attempt)) class Minstd: """Deterministic generator so the demo output is reproducible. In production pass random.random. """ def __init__(self, seed: int) -> None: self._state = seed def __call__(self) -> float: self._state = (self._state * 48271) % 2147483647 return self._state / 2147483647 BASE, CAP = 100, 2_000 raw = [min(CAP, BASE * 2**n) for n in range(5)] print(f"no jitter, every worker: {', '.join(map(str, raw))} ms") for i in range(3): rand = Minstd(seed=(i + 1) * 7919) # one stream per worker waits = [backoff_delay(n, BASE, CAP, rand) for n in range(5)] print(f"worker-{i + 1}: {', '.join(map(str, waits))} ms") # worker-1: 17, 74, 44, 535, 385 ms # worker-2: 35, 148, 89, 271, 771 ms <- never in lockstep with worker-1 # worker-3: 53, 23, 133, 7, 1157 ms <- waits may shrink; only the range is fixed ``` ```go title="backoff.go" package main import "fmt" // Deterministic MINSTD generator so the demo output is reproducible. type minstd struct{ state int64 } func (l *minstd) next() float64 { l.state = (l.state * 48271) % 2147483647 return float64(l.state) / 2147483647 } // Full jitter: uniform over the capped doubling range. func backoffDelay(attempt int, baseMs, capMs int64, r *minstd) int64 { raw := baseMs << attempt // base * 2^attempt if raw > capMs || raw <= 0 { raw = capMs } return int64(r.next() * float64(raw)) } func main() { const baseMs, capMs = int64(100), int64(2_000) fmt.Print("no jitter, every worker: ") for n := 0; n < 5; n++ { raw := baseMs << n if raw > capMs || raw <= 0 { raw = capMs } fmt.Printf("%d ms ", raw) } fmt.Println() for i := 1; i <= 3; i++ { r := &minstd{state: int64(i) * 7919} // one stream per worker fmt.Printf("worker-%d: ", i) for n := 0; n < 5; n++ { fmt.Printf("%d ms ", backoffDelay(n, baseMs, capMs, r)) } fmt.Println() } // worker-1: 17 ms 74 ms 44 ms 535 ms 385 ms // worker-2: 35 ms 148 ms 89 ms 271 ms 771 ms <- never in lockstep // worker-3: 53 ms 23 ms 133 ms 7 ms 1157 ms } ``` ```java title="Backoff.java" public class Backoff { /** Deterministic MINSTD generator so the demo output is reproducible. */ static final class Minstd { private long state; Minstd(long seed) { this.state = seed; } double next() { state = (state * 48271L) % 2147483647L; return (double) state / 2147483647L; } } /** Full jitter: uniform over the capped doubling range. */ static long backoffDelay(int attempt, long baseMs, long capMs, Minstd rand) { long raw = baseMs << attempt; // base * 2^attempt if (raw > capMs || raw <= 0) raw = capMs; return (long) (rand.next() * raw); } public static void main(String[] args) { long BASE = 100, CAP = 2_000; StringBuilder rawLine = new StringBuilder("no jitter, every worker: "); for (int n = 0; n < 5; n++) { long raw = BASE << n; if (raw > CAP || raw <= 0) raw = CAP; rawLine.append(raw).append(" ms "); } System.out.println(rawLine); for (int i = 1; i <= 3; i++) { Minstd rand = new Minstd((long) i * 7919); // one stream per worker StringBuilder line = new StringBuilder("worker-" + i + ": "); for (int n = 0; n < 5; n++) line.append(backoffDelay(n, BASE, CAP, rand)).append(" ms "); System.out.println(line); } // worker-1: 17 ms 74 ms 44 ms 535 ms 385 ms // worker-2: 35 ms 148 ms 89 ms 271 ms 771 ms <- never in lockstep // worker-3: 53 ms 23 ms 133 ms 7 ms 1157 ms } } ``` The three workers share a raw schedule but never retry in the same millisecond, and no wait exceeds the cap. ## Trade-offs **Gains** - Retry load spreads instead of arriving as a synchronized second spike. - The doubling curve matches how quickly transient failures actually clear. - The cap guarantees the loop stays finishable within the deadline. **Costs** - Added latency: the median caller waits longer than with an immediate retry. - Two knobs to tune per dependency (`base`, `cap`), both wrong more often than absent. - Without a cap, exponential growth schedules multi-minute waits nobody approved. - Jitter makes runs non-reproducible unless the RNG is seeded. ## Relations - [Retry](https://aipatterns.dev/design-patterns/retry.md) owns what to repeat and when to stop; this pattern owns only the spacing between repeats. - [Timeout](https://aipatterns.dev/design-patterns/timeout.md) bounds each attempt; the backoff bounds the gaps between attempts. - [Circuit Breaker](https://aipatterns.dev/design-patterns/circuit-breaker.md) is the next step when spaced retries keep failing: stop calling entirely. - [Command](https://aipatterns.dev/design-patterns/command.md): when waits grow to minutes, move the repeat into a scheduled queue instead of a sleeping caller. ## Agent checklist - [ ] The delay is randomized (full jitter), and the RNG is seeded where tests assert schedules. - [ ] The doubling range has a cap; no wait can exceed it. - [ ] `Retry-After` overrides the computed delay when the server sends one. - [ ] Attempts and overall deadline still bound the loop; the schedule does not decide alone. - [ ] Each attempt keeps its own timeout. --- # Fallback > Return a pre-decided lesser answer when a dependency cannot - stale cache, static default, partial result - so the journey continues, marked as degraded. - kind: pattern - category: resilience - aliases: Graceful Degradation, Degrade - canonical: https://aipatterns.dev/design-patterns/fallback ## Intent Decide in advance what a missing answer costs, and on failure return the substitute that costs least: the last-known-good value, a static default, an empty result, a queue-for-later. The caller's journey continues instead of failing - degraded, never silently wrong. ## Forces - **Fail honestly** vs **fail usefully**: an error is truthful but stops the journey; a default is useful but must not be mistaken for the real answer. - **Freshness** vs **availability**: yesterday's recommendations beat an error page, but yesterday's inventory count may not. - **One substitute** vs **per caller**: only the caller knows whether a missing answer is cosmetic or load-bearing. - **Degrade** vs **hide**: a fallback that nobody can see turns an outage into a mystery. ## Therefore **Give each call site a substitute decided in advance, return it when the dependency fails or times out, and mark the response as degraded so monitoring and users can tell.** The substitute is a product decision, not a code pattern: marketing copy can go stale, a payment cannot. Code exists to make that decision explicit and observable at every call site. ## Structure ```mermaid classDiagram class RecommendationSource { <> +recommendationsFor(userId) Recommendations } class PersonalizationService { +recommendationsFor(userId) Recommendations } class CachedRecommendations { -maxAge Duration +get(userId) String[] +put(userId, items) } class ResilientRecommendations { -inner RecommendationSource -cache CachedRecommendations +recommendationsFor(userId) Recommendations } RecommendationSource <|.. PersonalizationService RecommendationSource <|.. ResilientRecommendations ResilientRecommendations --> RecommendationSource : inner ResilientRecommendations --> CachedRecommendations : cache ``` ### Participants - **Resilient wrapper** (`ResilientRecommendations`): tries the real source, and on failure serves the substitute and marks the response degraded. - **Dependency** (`PersonalizationService`): the remote call that can fail. - **Substitute** (`CachedRecommendations`): last-known-good values with a freshness bound; the code also holds a static default list for cache misses. - **Caller**: reads `degraded` and can adjust (hide the section, show generic content, skip the call next time). ## Use it when - The data is nice-to-have: recommendations, avatars, follower counts, "customers also bought". - Reads tolerate staleness with a known bound (prices valid for minutes, cached until refresh). - A [Circuit Breaker](https://aipatterns.dev/design-patterns/circuit-breaker.md) is open: serving cached data beats erroring every request. - Batch jobs should skip-and-continue: one dead source must not fail a nightly export of twenty. - Non-critical steps decorate a critical path (notifications, analytics) and must not fail the transaction. ## Do not use it when - The answer must be correct: payments, inventory commits, authorization checks, anything consumed as a financial record. Fail loudly instead. - The substitute silently changes meaning. Returning zero as "no discount" when the truth is "unknown discount" is a business bug, not resilience. - The dependency is the product. A fallback for the primary data of the page is a different, wrong product. - Nothing marks the degradation. An unmarked fallback hides a chronic outage; pair it with a metric and an alert on fallback rate. - A queue would do better. If the work must happen eventually (send the email, update the index), defer with [Command](https://aipatterns.dev/design-patterns/command.md) and a durable queue rather than serving a substitute forever. ## Application grill **Inspect first:** the dependencies whose failure should degrade and what lesser answers already exist (a cache from cache-aside, static defaults, partial data); whether the UI has any degraded-state convention; feature-flag infrastructure. **Missing pieces:** a fallback needs a source for the lesser answer - usually the cache, a bundled default, or a read replica. Feature flags control degraded UI; a flag service is justified when non-developers must toggle, otherwise environment configuration is enough. **Ask before you build:** - **Scale:** which dependencies may degrade, and can every fallback be on at once during a real outage? - **Performance:** how stale may the fallback answer be before it is worse than an error? - **Reliability:** how is the degraded state surfaced to users and metrics - a silent fallback is how outages hide? - **Budget:** build per-dependency fallbacks or standardize one wrapper - who owns the second failure path for each? - **Implementation:** when was each fallback last exercised in staging - a fallback that never ran is a hope? ## Implementation steps 1. For each call site, decide what a missing answer costs and who can tolerate what. This is a product conversation; the code only encodes its outcome. 2. Pick the substitute per call site: cached last-known-good, static default, empty result, a second provider, or skip-and-continue in a loop. 3. Bound staleness: a cache entry older than `maxAge` counts as a miss, so degradation has a shelf life. 4. Mark degraded responses in the type (`degraded: true`), in metrics, and in logs - never return a substitute unmarked. 5. Wire the trigger: fire the fallback when [Timeout](https://aipatterns.dev/design-patterns/timeout.md) expires, [Retry](https://aipatterns.dev/design-patterns/retry.md) is exhausted, or the [Circuit Breaker](https://aipatterns.dev/design-patterns/circuit-breaker.md) is open - not as a blanket `catch` around everything. 6. Alert on the fallback rate. Degradation that lasts a day is an incident with better UX, not a steady state. 7. Test the fallback path like a feature: stale cache, empty cache, and marked responses. ## Example A product page shows personalized recommendations. The personalization service fails; the page serves last-known-good recommendations from cache, or a static "popular this week" list for unknown users - every degraded response carries `degraded: true` so the page can label it and the dashboard can count it. ```ts title="recommendations.ts" type Recommendations = { items: string[]; degraded: boolean }; interface RecommendationSource { recommendationsFor(userId: string): Promise; } // The substitute: last-known-good answers with a freshness bound. class CachedRecommendations { private readonly store = new Map(); constructor(private readonly maxAgeMs: number, private readonly now = Date.now) {} put(userId: string, items: string[]) { this.store.set(userId, { items, at: this.now() }); } get(userId: string): string[] | null { const hit = this.store.get(userId); return hit && this.now() - hit.at <= this.maxAgeMs ? hit.items : null; } } // The static default for users with no cache entry. const POPULAR_PICKS = ["best-seller-1", "best-seller-2", "new-arrival-1"]; // The dependency: a personalization service that answers once, then breaks. class PersonalizationService implements RecommendationSource { constructor(private successesLeft: number) {} async recommendationsFor(userId: string): Promise { if (this.successesLeft-- <= 0) throw new Error("503 unavailable"); return { items: [`picks-for-${userId}`], degraded: false }; } } class ResilientRecommendations implements RecommendationSource { constructor( private readonly inner: RecommendationSource, private readonly cache: CachedRecommendations, private readonly onDegraded: () => void, ) {} async recommendationsFor(userId: string): Promise { try { const fresh = await this.inner.recommendationsFor(userId); this.cache.put(userId, fresh.items); return fresh; } catch (e) { this.onDegraded(); // count it; alert on the rate const cached = this.cache.get(userId); if (cached) return { items: cached, degraded: true }; return { items: POPULAR_PICKS, degraded: true }; } } } const cache = new CachedRecommendations(60_000); let degradations = 0; const source = new ResilientRecommendations( new PersonalizationService(1), // one good answer, then it fails cache, () => degradations++, ); console.log(await source.recommendationsFor("u-1")); // { items: [ 'picks-for-u-1' ], degraded: false } console.log(await source.recommendationsFor("u-1")); // { items: [ 'picks-for-u-1' ], degraded: true } <- served from cache console.log(await source.recommendationsFor("u-2"), "degradations:", degradations); // { items: [ 'best-seller-1', ... ], degraded: true } degradations: 2 ``` ```python title="recommendations.py" import time from typing import Callable, Protocol class RecommendationSource(Protocol): def recommendations_for(self, user_id: str) -> dict: ... class CachedRecommendations: """The substitute: last-known-good answers with a freshness bound.""" def __init__(self, max_age_ms: int, now: Callable[[], float] = time.monotonic) -> None: self._max_age_ms, self._now = max_age_ms, now self._store: dict[str, tuple[list[str], float]] = {} def put(self, user_id: str, items: list[str]) -> None: self._store[user_id] = (items, self._now()) def get(self, user_id: str) -> list[str] | None: hit = self._store.get(user_id) if hit and (self._now() - hit[1]) * 1000 <= self._max_age_ms: return hit[0] return None POPULAR_PICKS = ["best-seller-1", "best-seller-2", "new-arrival-1"] class PersonalizationService: """The dependency: answers once, then breaks.""" def __init__(self, successes_left: int) -> None: self._successes_left = successes_left def recommendations_for(self, user_id: str) -> dict: if self._successes_left <= 0: raise RuntimeError("503 unavailable") self._successes_left -= 1 return {"items": [f"picks-for-{user_id}"], "degraded": False} class ResilientRecommendations: def __init__( self, inner: RecommendationSource, cache: CachedRecommendations, on_degraded: Callable[[], None], ) -> None: self._inner, self._cache, self._on_degraded = inner, cache, on_degraded def recommendations_for(self, user_id: str) -> dict: try: fresh = self._inner.recommendations_for(user_id) self._cache.put(user_id, fresh["items"]) return fresh except RuntimeError: self._on_degraded() # count it; alert on the rate cached = self._cache.get(user_id) items = cached if cached is not None else POPULAR_PICKS return {"items": items, "degraded": True} cache = CachedRecommendations(max_age_ms=60_000) degradations = 0 def count() -> None: global degradations degradations += 1 source = ResilientRecommendations( PersonalizationService(successes_left=1), # one good answer, then it fails cache, count, ) print(source.recommendations_for("u-1")) # {'items': ['picks-for-u-1'], 'degraded': False} print(source.recommendations_for("u-1")) # {'items': ['picks-for-u-1'], 'degraded': True} <- served from cache print(source.recommendations_for("u-2"), f"degradations: {degradations}") # {'items': ['best-seller-1', ...], 'degraded': True} degradations: 2 ``` ```go title="recommendations.go" package main import ( "errors" "fmt" "time" ) type Recommendations struct { Items []string Degraded bool } type RecommendationSource interface { RecommendationsFor(userID string) (Recommendations, error) } // CachedRecommendations is the substitute: last-known-good answers with a freshness bound. type CachedRecommendations struct { maxAge time.Duration now func() time.Time store map[string]entry } type entry struct { items []string at time.Time } func NewCache(maxAge time.Duration, now func() time.Time) *CachedRecommendations { return &CachedRecommendations{maxAge: maxAge, now: now, store: map[string]entry{}} } func (c *CachedRecommendations) put(userID string, items []string) { c.store[userID] = entry{items, c.now()} } func (c *CachedRecommendations) get(userID string) []string { h, ok := c.store[userID] if !ok || c.now().Sub(h.at) > c.maxAge { return nil } return h.items } var popularPicks = []string{"best-seller-1", "best-seller-2", "new-arrival-1"} // PersonalizationService answers once, then breaks. type PersonalizationService struct{ successesLeft int } func (s *PersonalizationService) RecommendationsFor(userID string) (Recommendations, error) { if s.successesLeft <= 0 { return Recommendations{}, errors.New("503 unavailable") } s.successesLeft-- return Recommendations{Items: []string{"picks-for-" + userID}}, nil } type ResilientRecommendations struct { inner RecommendationSource cache *CachedRecommendations degradations *int } func (r ResilientRecommendations) RecommendationsFor(userID string) (Recommendations, error) { fresh, err := r.inner.RecommendationsFor(userID) if err == nil { r.cache.put(userID, fresh.Items) return fresh, nil } *r.degradations++ // count it; alert on the rate if cached := r.cache.get(userID); cached != nil { return Recommendations{Items: cached, Degraded: true}, nil } return Recommendations{Items: popularPicks, Degraded: true}, nil } func main() { cache := NewCache(time.Minute, time.Now) degradations := 0 source := ResilientRecommendations{ inner: &PersonalizationService{successesLeft: 1}, cache: cache, degradations: °radations, } first, _ := source.RecommendationsFor("u-1") fmt.Println(first.Items, first.Degraded) // [picks-for-u-1] false second, _ := source.RecommendationsFor("u-1") fmt.Println(second.Items, second.Degraded) // [picks-for-u-1] true (from cache) third, _ := source.RecommendationsFor("u-2") fmt.Println(third.Items, third.Degraded, degradations) // [best-seller-1 ...] true 2 } ``` ```java title="Recommendations.java" import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Supplier; public class Recommendations { record Answers(List items, boolean degraded) {} interface RecommendationSource { Answers recommendationsFor(String userId); } /** The substitute: last-known-good answers with a freshness bound. */ static final class CachedRecommendations { private final long maxAgeMs; private final Supplier now; private final Map store = new HashMap<>(); private record Timed(List items, Instant at) {} CachedRecommendations(long maxAgeMs, Supplier now) { this.maxAgeMs = maxAgeMs; this.now = now; } void put(String userId, List items) { store.put(userId, new Timed(items, now.get())); } List get(String userId) { Timed hit = store.get(userId); return hit != null && now.get().toEpochMilli() - hit.at().toEpochMilli() <= maxAgeMs ? hit.items() : null; } } static final List POPULAR_PICKS = List.of("best-seller-1", "best-seller-2", "new-arrival-1"); /** The dependency: answers once, then breaks. */ static final class PersonalizationService implements RecommendationSource { private int successesLeft; PersonalizationService(int successesLeft) { this.successesLeft = successesLeft; } public Answers recommendationsFor(String userId) { if (successesLeft-- <= 0) throw new IllegalStateException("503 unavailable"); return new Answers(List.of("picks-for-" + userId), false); } } record ResilientRecommendations( RecommendationSource inner, CachedRecommendations cache, Runnable onDegraded) implements RecommendationSource { public Answers recommendationsFor(String userId) { try { Answers fresh = inner.recommendationsFor(userId); cache.put(userId, fresh.items()); return fresh; } catch (RuntimeException e) { onDegraded.run(); // count it; alert on the rate List cached = cache.get(userId); return new Answers(cached != null ? cached : POPULAR_PICKS, true); } } } public static void main(String[] args) { int[] degradations = {0}; ResilientRecommendations source = new ResilientRecommendations( new PersonalizationService(1), new CachedRecommendations(60_000, Instant::now), () -> degradations[0]++); System.out.println(source.recommendationsFor("u-1")); // Answers[items=[picks-for-u-1], degraded=false] System.out.println(source.recommendationsFor("u-1")); // degraded=true, served from cache System.out.println(source.recommendationsFor("u-2") + " degradations=" + degradations[0]); } } ``` The wrapper has the same interface as the service, so it is also where a [Timeout](https://aipatterns.dev/design-patterns/timeout.md) or retry would live - the fallback fires after those give up. ## Trade-offs **Gains** - The journey continues during an outage; users see slightly worse content, not an error page. - Non-critical failures stop failing critical paths. - The `degraded` flag turns silent rot into a visible, alertable signal. **Costs** - Every substitute is a product decision with a staleness or correctness risk attached. - Unmonitored fallbacks hide chronic outages behind a calm UI. - The wrapper adds a layer and a cache to own and to test. - Wrong substitutes (zero, empty, "unknown") can corrupt decisions downstream. ## Relations - [Retry](https://aipatterns.dev/design-patterns/retry.md) exhausts the transient attempts first; the fallback answers when retry gives up. - [Circuit Breaker](https://aipatterns.dev/design-patterns/circuit-breaker.md) makes fallback a steady companion: while the breaker is open, every call serves the substitute immediately. - [Timeout](https://aipatterns.dev/design-patterns/timeout.md) provides the trigger - without deadlines, the fallback path never fires because the call never ends. - [Decorator](https://aipatterns.dev/design-patterns/decorator.md): the wrapper is a decorator; several resilience wrappers (timeout, retry, fallback) stack around one interface. - [Strategy](https://aipatterns.dev/design-patterns/strategy.md): when several substitutes exist (cache, second provider, static list), the choice of substitute is a runtime strategy the composition root picks. ## Agent checklist - [ ] Each call site's substitute is a deliberate product decision, not a default `catch`. - [ ] Staleness has a bound (`maxAge`), and expired entries count as misses. - [ ] Degraded responses are marked in the type and counted in metrics. - [ ] Correctness-critical paths (payments, authorization, inventory commits) have no fallback. - [ ] The fallback path has tests: stale cache, cold cache, and the marked response. - [ ] An alert exists on the fallback rate, so degradation cannot become invisible. --- # Rate Limiting > Give each client a refillable budget of calls - take a token to proceed, refuse with 429 and a Retry-After when the bucket is empty - so one client's burst cannot spend everyone's capacity. - kind: pattern - category: resilience - aliases: Throttling - canonical: https://aipatterns.dev/design-patterns/rate-limiting - inspired-by: https://learn.microsoft.com/en-us/azure/architecture/patterns/rate-limiting-pattern ## Intent Ration a shared resource by client. Each client holds a bucket of tokens that refills at a fixed rate; every call takes one token. A call with tokens available proceeds; an empty bucket refuses immediately with a `429` and a `Retry-After` that says exactly when to come back. Bursts within the budget pass through, sustained demand is capped at the refill rate, and no client's spike can spend another client's capacity. ## Forces - **Shared capacity** vs **uneven demand**: the resource serves everyone, but one client's burst can consume what the others needed. - **Reject early** vs **queue forever**: refusing fast lets the caller reschedule; letting everything through converts your limit into everyone's timeout. - **Burst allowance** vs **average cap**: clients work in bursts, so a flat requests-per-second is either too tight or too loose; a bucket gives both a burst size and a sustained rate. - **Fair share** vs **total throughput**: strict per-client fairness refuses work the resource could have done; dropping fairness rewards the loudest tenant. ## Therefore **Charge every call to a per-client token bucket that refills on time; when the bucket is empty, refuse with 429 and a computed Retry-After instead of admitting the call.** The bucket is a counter plus a clock, keyed per client. Refusal is not an error state to avoid - it is the contract that makes the caller's [Retry](https://aipatterns.dev/design-patterns/retry.md) policy honest: `Retry-After` tells the client exactly when capacity exists, and a well-behaved client spends the wait instead of hammering. ## Structure ```mermaid flowchart TD A[Request arrives] --> B{Bucket has a token?} B -- yes --> C[Take one token and handle the request] B -- no --> D[Refuse with 429 and Retry-After] E[Time passes] -- refills at a fixed rate --> B ``` ### Participants - **Limiter** (`RateLimiter`): holds one bucket per client. `acquire` refills by elapsed time, takes a token, or returns the wait until one exists. - **Bucket** (implicit in `RateLimiter`): capacity (burst size), refill rate (sustained rate), current tokens, last refill time. - **Endpoint** (`ReportsEndpoint`): the guarded resource. Checks the limiter first; on refusal answers `429` with `Retry-After` instead of doing the work. - **Client**: a caller identified by key - user id, API key, tenant. On `429`, waits the stated time (client-side limiting). - **Clock** (`now`): injected so refill is testable without sleeping. ## Use it when - A resource is shared by clients whose demand is uneven - an API route in front of a database, an expensive report, a model invocation billed per call. - You must protect the resource from bursts while still letting well-behaved bursts through: the bucket's capacity is the burst allowance, the refill is the sustained rate. - Each tenant needs a guaranteed share: one client hammering the endpoint must not starve the others. - You are the client of a provider that rate-limits you - budget your own calls per key and honor the provider's `429` + `Retry-After`, so the provider's limiter never has to teach you the lesson. ## Do not use it when - The resource is genuinely too small for its normal load. A limiter rationing an under-provisioned database moves the outage from everyone-at-once to first-come-first-served; add capacity or fix the query instead. - The goal is stopping attackers or abusive scrapers. Limits throttle honest clients that count their calls; bad actors spin up more clients. Use allowlists, blocking and bot detection for that job. - The calls are in-process functions. There is no shared resource to ration and no client boundary to key on. - Work must not be lost under any circumstance. A `429` drops the call; if it must happen eventually, queue it with [Command](https://aipatterns.dev/design-patterns/command.md) and let a worker drain at a safe rate. ## Application grill **Inspect first:** the entry points that need limits (public routes, webhooks, LLM and third-party calls with hard quotas); the platform's built-in limiters first (WAF rules, Vercel or Cloudflare rate limiting, gateway plugins, framework middleware); the instance count, which decides per-process versus shared counters. **Missing pieces:** single instance: an in-memory token bucket - free. Multiple instances or serverless: shared counters in Redis or the platform's rate-limit bindings; a shared store adds its own latency to every request, so prefer the edge limiter when one exists. **Ask before you build:** - **Scale:** what is the limit per - user, IP, API key - and what burst is legitimate before refusing? - **Performance:** can the check run at the edge so the origin never pays for refused calls? - **Reliability:** when the counter store is down, does the service fail open (allow) or closed (refuse everything)? - **Budget:** whose spend does the limiter protect (LLM tokens, vendor calls), and what does exceeding it cost? - **Implementation:** what does a refused caller receive (429 with Retry-After), and where are limits configured? ## Implementation steps 1. Name the resource and the unit of spend: requests, model tokens, dollars, queue slots. The unit decides what one token means. 2. Pick the algorithm: a token bucket when clients work in bursts under an average cap; a sliding window when a hard count per period is the actual rule. 3. Key the buckets by client identity, and by resource when one client has different budgets for different endpoints. 4. Compute `Retry-After` from the bucket - the time until the next token exists - so refusal carries a recovery plan, not just a status code. 5. Decide where bucket state lives. In-process is simplest and means every instance enforces its own bucket; a shared store (Redis) gives one true budget per client at the cost of a read per call. Pick deliberately; silent per-instance limits are the default bug. 6. On the client side, treat `429` as a scheduled retry: wait exactly `Retry-After`, then [Retry](https://aipatterns.dev/design-patterns/retry.md) with [Exponential Backoff](https://aipatterns.dev/design-patterns/exponential-backoff.md) for anything without the header. 7. Emit per-client metrics: refused-per-minute per key is your fair-use signal, and a spike in refusals is a client bug or a capacity conversation, not noise. ## Example A reports endpoint generates an expensive document per client. Each client holds a bucket of 2 tokens refilling at 1 per second. The first two calls of a burst pass; the third is refused with a `Retry-After` computed from the refill; after the wait, capacity exists again. A second client's bucket is untouched by the first client's burst. ```ts title="reports.ts" type Refusal = { ok: false; retryAfterMs: number }; class RateLimiter { private buckets = new Map(); constructor( private readonly capacity: number, // burst size private readonly refillPerMs: number, // sustained rate private readonly now: () => number, ) {} acquire(client: string): { ok: true } | Refusal { const bucket = this.buckets.get(client) ?? { tokens: this.capacity, at: this.now(), }; // Refill for the time that passed since the bucket was last touched. bucket.tokens = Math.min( this.capacity, bucket.tokens + (this.now() - bucket.at) * this.refillPerMs, ); bucket.at = this.now(); if (bucket.tokens >= 1) { bucket.tokens -= 1; this.buckets.set(client, bucket); return { ok: true }; } this.buckets.set(client, bucket); const wait = Math.ceil((1 - bucket.tokens) / this.refillPerMs); return { ok: false, retryAfterMs: wait }; } } class ReportsEndpoint { private limiter = new RateLimiter(2, 1 / 1_000, () => clock); // 2 burst, 1/s private generated = 0; report(client: string): string { const grant = this.limiter.acquire(client); if (!grant.ok) return `429 to ${client}, Retry-After ${grant.retryAfterMs}ms`; this.generated++; return `200 to ${client} (report #${this.generated})`; } } let clock = 0; const endpoint = new ReportsEndpoint(); console.log(endpoint.report("acme")); // 200: token 1 of the burst console.log(endpoint.report("acme")); // 200: token 2 of the burst console.log(endpoint.report("acme")); // 429 with Retry-After 1000ms console.log(endpoint.report("globex")); // 200: acme's burst never touched globex clock += 1_000; // one refill period later console.log(endpoint.report("acme")); // 200: the wait bought a token ``` ```python title="reports.py" import math from typing import Callable class Refusal: def __init__(self, retry_after_ms: int) -> None: self.retry_after_ms = retry_after_ms class RateLimiter: def __init__( self, capacity: int, refill_per_ms: float, now: Callable[[], int] ) -> None: self._capacity, self._refill, self._now = capacity, refill_per_ms, now self._buckets: dict[str, list[float]] = {} # client -> [tokens, last_at] def acquire(self, client: str) -> None | Refusal: tokens, at = self._buckets.get(client, [float(self._capacity), self._now()]) tokens = min(self._capacity, tokens + (self._now() - at) * self._refill) if tokens >= 1: self._buckets[client] = [tokens - 1, self._now()] return None self._buckets[client] = [tokens, self._now()] return Refusal(retry_after_ms=math.ceil((1 - tokens) / self._refill)) class ReportsEndpoint: def __init__(self, now: Callable[[], int]) -> None: self._limiter = RateLimiter(capacity=2, refill_per_ms=1 / 1_000, now=now) self._generated = 0 def report(self, client: str) -> str: refusal = self._limiter.acquire(client) if refusal is not None: return f"429 to {client}, Retry-After {refusal.retry_after_ms}ms" self._generated += 1 return f"200 to {client} (report #{self._generated})" clock = 0 now = lambda: clock # noqa: E731 - injected clock endpoint = ReportsEndpoint(now) print(endpoint.report("acme")) # 200: token 1 of the burst print(endpoint.report("acme")) # 200: token 2 of the burst print(endpoint.report("acme")) # 429 with Retry-After 1000ms print(endpoint.report("globex")) # 200: acme's burst never touched globex clock += 1_000 # one refill period later print(endpoint.report("acme")) # 200: the wait bought a token ``` ```go title="reports.go" package main import ( "fmt" "math" ) type refusal struct{ retryAfterMs int } type RateLimiter struct { capacity int refillPerMs float64 now func() int buckets map[string]*bucketState } type bucketState struct { tokens float64 at int } func NewRateLimiter(capacity int, refillPerMs float64, now func() int) *RateLimiter { return &RateLimiter{ capacity: capacity, refillPerMs: refillPerMs, now: now, buckets: map[string]*bucketState{}, } } func (l *RateLimiter) acquire(client string) *refusal { b, ok := l.buckets[client] if !ok { b = &bucketState{tokens: float64(l.capacity), at: l.now()} } b.tokens = math.Min(float64(l.capacity), b.tokens+float64(l.now()-b.at)*l.refillPerMs) if b.tokens >= 1 { b.tokens-- b.at = l.now() l.buckets[client] = b return nil } b.at = l.now() l.buckets[client] = b wait := int(math.Ceil((1 - b.tokens) / l.refillPerMs)) return &refusal{retryAfterMs: wait} } type ReportsEndpoint struct { limiter *RateLimiter generated int } func NewReportsEndpoint(now func() int) *ReportsEndpoint { // 2-token burst, sustained rate of 1 per second. return &ReportsEndpoint{limiter: NewRateLimiter(2, 1.0/1_000, now)} } func (e *ReportsEndpoint) report(client string) string { if r := e.limiter.acquire(client); r != nil { return fmt.Sprintf("429 to %s, Retry-After %dms", client, r.retryAfterMs) } e.generated++ return fmt.Sprintf("200 to %s (report #%d)", client, e.generated) } func main() { clock := 0 now := func() int { return clock } endpoint := NewReportsEndpoint(now) fmt.Println(endpoint.report("acme")) // 200: token 1 of the burst fmt.Println(endpoint.report("acme")) // 200: token 2 of the burst fmt.Println(endpoint.report("acme")) // 429 with Retry-After 1000ms fmt.Println(endpoint.report("globex")) // 200: acme's burst never touched globex clock += 1_000 // one refill period later fmt.Println(endpoint.report("acme")) // 200: the wait bought a token } ``` ```java title="Reports.java" import java.util.HashMap; import java.util.Map; import java.util.function.IntSupplier; public class Reports { record Refusal(int retryAfterMs) {} static final class RateLimiter { private static final class Bucket { double tokens; long at; } private final int capacity; private final double refillPerMs; private final IntSupplier now; private final Map buckets = new HashMap<>(); RateLimiter(int capacity, double refillPerMs, IntSupplier now) { this.capacity = capacity; this.refillPerMs = refillPerMs; this.now = now; } Refusal acquire(String client) { Bucket b = buckets.computeIfAbsent(client, k -> { Bucket fresh = new Bucket(); fresh.tokens = capacity; fresh.at = now.getAsLong(); return fresh; }); b.tokens = Math.min(capacity, b.tokens + (now.getAsLong() - b.at) * refillPerMs); if (b.tokens >= 1) { b.tokens -= 1; b.at = now.getAsLong(); return null; } b.at = now.getAsLong(); return new Refusal((int) Math.ceil((1 - b.tokens) / refillPerMs)); } } static final class ReportsEndpoint { private final RateLimiter limiter; private int generated = 0; ReportsEndpoint(IntSupplier now) { // 2-token burst, sustained rate of 1 per second. this.limiter = new RateLimiter(2, 1.0 / 1_000, now); } String report(String client) { Refusal refusal = limiter.acquire(client); if (refusal != null) { return "429 to " + client + ", Retry-After " + refusal.retryAfterMs() + "ms"; } generated++; return "200 to " + client + " (report #" + generated + ")"; } } public static void main(String[] args) { long[] clock = {0}; ReportsEndpoint endpoint = new ReportsEndpoint(() -> clock[0]); System.out.println(endpoint.report("acme")); // 200: token 1 of the burst System.out.println(endpoint.report("acme")); // 200: token 2 of the burst System.out.println(endpoint.report("acme")); // 429 with Retry-After 1000ms System.out.println(endpoint.report("globex")); // 200: acme's burst left globex alone clock[0] += 1_000; // one refill period later System.out.println(endpoint.report("acme")); // 200: the wait bought a token } } ``` Under concurrency, the read-refill-take sequence must be atomic per bucket - a lock, an atomic compare-and-swap, or a Lua script in Redis - or two simultaneous callers can both see the last token and both take it. ## Trade-offs **Gains** - One client's burst stops being everyone's outage; per-client budgets make demand predictable. - `429` + `Retry-After` converts overload into a schedule the client can follow. - Refusing before the work starts protects the expensive resource, not just the response time. **Costs** - Rejected work is dropped - callers need the retry discipline, and non-idempotent calls need care ([Retry](https://aipatterns.dev/design-patterns/retry.md)). - Bucket state must live somewhere: per-instance limits multiply with your fleet unless you share state. - Thresholds are policy: too tight refuses honest load, too loose protects nothing, and both failings surface as support tickets. - Fairness at the client key is only as good as the key - shared API keys collapse many users into one bucket. ## Relations - [Retry](https://aipatterns.dev/design-patterns/retry.md): the two halves of one contract - the limiter's `Retry-After` is the retry policy's input, and a client's own budget prevents the `429`s in the first place. - [Exponential Backoff](https://aipatterns.dev/design-patterns/exponential-backoff.md): the spacing rule for `429`s that arrive without a `Retry-After` header. - [Circuit Breaker](https://aipatterns.dev/design-patterns/circuit-breaker.md): complementary jobs - the limiter rations a dependency that is up; the breaker stops calling one that is down. - [Timeout](https://aipatterns.dev/design-patterns/timeout.md): bounds each admitted call; a limiter that admits a request cannot also promise it finishes. - [Fallback](https://aipatterns.dev/design-patterns/fallback.md): the degraded answer a caller serves instead of the refused work - a cached report, a queue position, a "try later". ## Agent checklist - [ ] Buckets are keyed per client (and per resource where budgets differ), and the key survives auth changes. - [ ] Refusal answers `429` with a computed `Retry-After`, never a bare error. - [ ] The read-refill-take step is atomic under concurrency. - [ ] Bucket state's location (per instance or shared) is a written decision, not an accident. - [ ] Client-side callers honor `Retry-After` and never retry a refusal immediately. - [ ] Refusal counts per key are monitored; a rising refusal rate triggers a capacity or client-bug review, not silence. --- # Adapter > Wrap an incompatible class in an object that exposes the interface your code expects, so you can use it without changing either side. - kind: pattern - category: structural - aliases: Wrapper, Translator - canonical: https://aipatterns.dev/design-patterns/adapter - inspired-by: https://refactoring.guru/design-patterns/adapter ## Intent Make a class with the wrong interface usable by code that expects a different one. The adapter implements the interface your code owns and translates each call into the calls, argument shapes and error conventions of the class it wraps. Neither the caller nor the wrapped class changes. ## Forces - **Your domain's vocabulary** vs **a vendor SDK's vocabulary**: your code says `send(phone, message)`; the SDK wants a payload object, a stripped phone number and a sender ID. - **Code you own** vs **code you cannot edit**: third-party libraries, generated clients and legacy modules are fixed, but your callers should not bend to them. - **One integration point** vs **SDK calls scattered everywhere**: when every caller talks to the vendor directly, switching vendors becomes [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md). - **Error conventions**: the SDK returns status codes, your code expects exceptions or `error` values. Someone has to translate, exactly once. ## Therefore **Define the interface your code wants, then write one small class that implements it by delegating to the incompatible class and translating inputs, outputs and errors.** The interface belongs to *your* side. In hexagonal architecture it is the port, and this class is the adapter that plugs a vendor into it. Callers depend only on that interface, so tests use a fake, and a vendor switch means writing a second adapter. Keep the adapter thin: translation only, no business rules. ## Structure ```mermaid classDiagram class SignupService { -SmsSender sms +sendLoginCode(phone, code) void } class SmsSender { <> +send(phone, message) void } class TelcoSmsAdapter { -TelcoClient client -String senderId +send(phone, message) void } class TelcoClient { +dispatch(payload) VendorResult } SignupService --> SmsSender SmsSender <|.. TelcoSmsAdapter TelcoSmsAdapter --> TelcoClient : delegates ``` ### Participants - **Target** (`SmsSender`): the interface your code depends on, shaped by your needs, not the vendor's. - **Adaptee** (`TelcoClient`): the existing class with the incompatible interface. You do not modify it. - **Adapter** (`TelcoSmsAdapter`): implements the target, holds the adaptee, and translates in both directions. - **Client** (`SignupService`): calls the target and never sees the adaptee's types. ## Use it when - You integrate a third-party SDK, generated API client or legacy module whose method names, argument types or error style don't match your code. - You want to swap a vendor later, or run two vendors side by side, behind one interface. - Vendor types are leaking into domain code: imports of the SDK appear in services, handlers and tests. - You need a fake for tests and the real dependency has no interface to implement. - Two existing classes do the same job with different method signatures ([Alternative Classes with Different Interfaces](https://aipatterns.dev/refactoring/smells/alternative-classes-with-different-interfaces.md)) and you can't rename one of them. ## Do not use it when - You own both sides. Change the class's interface directly ([Rename Method](https://aipatterns.dev/refactoring/techniques/rename-method.md), [Add Parameter](https://aipatterns.dev/refactoring/techniques/add-parameter.md)) instead of wrapping your own code. - The mismatch is one function call in one place. A local helper function that converts arguments is enough; a class and an interface for a single call site is ceremony. - The only difference is a function's shape and your language has first-class functions. Pass a lambda such as `(phone, msg) => client.dispatch({...})` where a one-method interface is expected. - You are about to add caching, retries or logging. That is [Decorator](https://aipatterns.dev/design-patterns/decorator.md): same interface in and out, extra behavior. An adapter changes the interface and adds nothing. - You want to hide a whole subsystem of several classes behind a simpler entry point. That is [Facade](https://aipatterns.dev/design-patterns/facade.md). ## Application grill **Inspect first:** both interfaces and who owns each side (your code versus a third-party SDK with its own version cadence); every call site that would bypass the adapter and hit the vendor directly; the SDK version pinned in the lockfile. **Missing pieces:** none beyond the language: one class or function that exposes your interface and delegates. The adapter is the one file to touch on an SDK upgrade - say so in its doc comment so upgrades stay local. **Ask before you build:** - **Scale:** is the wrapped call on a hot path where per-call overhead or connection reuse (pooling inside the SDK) matters? - **Performance:** does the adapter own one long-lived client, or build a new client per call? - **Reliability:** how are vendor errors translated - an error map or a rethrow - and which side's contract do callers now see? - **Budget:** one adapter for one consumer, or an interface several services will share? - **Implementation:** can tests run the adapter against a fake vendor, and are the error translations covered? ## Implementation steps 1. Write the target interface from the caller's point of view: only the methods callers need, using your domain types. 2. Create the adapter class implementing the target. Take the adaptee as a constructor argument (do not construct it inside). 3. Implement each method by converting arguments, calling the adaptee and converting the result. 4. Map the adaptee's errors to your own error types, so callers never catch vendor exceptions or check vendor codes. 5. Replace direct adaptee calls in client code with calls through the target interface, one call site at a time, running tests after each. 6. Wire the adapter at the composition root and give tests a fake implementation of the target. ## Example A signup service sends login codes by SMS. The telecom SDK wants a payload with a digits-only `msisdn` and reports failure with numeric codes instead of errors. ```ts title="notifications.ts" // Target: the interface your code owns. interface SmsSender { send(phone: string, message: string): void; } // --- Third-party SDK (simulated). You cannot edit this code. --- interface VendorPayload { msisdn: string; text: string; senderId: string } interface VendorResult { code: number; detail: string } class TelcoClient { dispatch(p: VendorPayload): VendorResult { if (!/^\d{8,15}$/.test(p.msisdn)) return { code: 21, detail: "invalid msisdn" }; console.log(`[telco] ${p.senderId} -> ${p.msisdn}: ${p.text}`); return { code: 0, detail: "queued" }; } } // --- end SDK --- class SmsDeliveryError extends Error {} class TelcoSmsAdapter implements SmsSender { constructor( private readonly client: TelcoClient, private readonly senderId: string, ) {} send(phone: string, message: string): void { const result = this.client.dispatch({ msisdn: phone.replace(/^\+/, ""), text: message, senderId: this.senderId, }); if (result.code !== 0) { throw new SmsDeliveryError(`telco error ${result.code}: ${result.detail}`); } } } // Client code: knows SmsSender, never TelcoClient. function sendLoginCode(sms: SmsSender, phone: string, code: string): void { sms.send(phone, `Your login code is ${code}`); } const sms: SmsSender = new TelcoSmsAdapter(new TelcoClient(), "ACME"); sendLoginCode(sms, "+4915112345678", "482913"); try { sendLoginCode(sms, "+12", "000"); } catch (e) { console.log(e instanceof SmsDeliveryError, (e as Error).message); // true telco error 21: invalid msisdn } ``` ```python title="notifications.py" from dataclasses import dataclass from typing import Protocol class SmsSender(Protocol): def send(self, phone: str, message: str) -> None: ... # --- Third-party SDK (simulated). You cannot edit this code. --- @dataclass class VendorResult: code: int detail: str class TelcoClient: def dispatch(self, payload: dict) -> VendorResult: msisdn = payload["msisdn"] if not (msisdn.isdigit() and 8 <= len(msisdn) <= 15): return VendorResult(21, "invalid msisdn") print(f"[telco] {payload['sender_id']} -> {msisdn}: {payload['text']}") return VendorResult(0, "queued") # --- end SDK --- class SmsDeliveryError(Exception): pass class TelcoSmsAdapter: def __init__(self, client: TelcoClient, sender_id: str) -> None: self._client = client self._sender_id = sender_id def send(self, phone: str, message: str) -> None: result = self._client.dispatch( {"msisdn": phone.removeprefix("+"), "text": message, "sender_id": self._sender_id} ) if result.code != 0: raise SmsDeliveryError(f"telco error {result.code}: {result.detail}") def send_login_code(sms: SmsSender, phone: str, code: str) -> None: sms.send(phone, f"Your login code is {code}") if __name__ == "__main__": sms = TelcoSmsAdapter(TelcoClient(), "ACME") send_login_code(sms, "+4915112345678", "482913") try: send_login_code(sms, "+12", "000") except SmsDeliveryError as e: print("failed:", e) # failed: telco error 21: invalid msisdn ``` ```go title="notifications.go" package main import ( "errors" "fmt" "strings" ) // SmsSender is the target interface your code owns. type SmsSender interface { Send(phone, message string) error } // --- Third-party SDK (simulated). You cannot edit this code. --- type VendorPayload struct{ MSISDN, Text, SenderID string } type VendorResult struct { Code int Detail string } type TelcoClient struct{} func (TelcoClient) Dispatch(p VendorPayload) VendorResult { n := len(p.MSISDN) if n < 8 || n > 15 || strings.Trim(p.MSISDN, "0123456789") != "" { return VendorResult{Code: 21, Detail: "invalid msisdn"} } fmt.Printf("[telco] %s -> %s: %s\n", p.SenderID, p.MSISDN, p.Text) return VendorResult{Code: 0, Detail: "queued"} } // --- end SDK --- var ErrDelivery = errors.New("sms delivery failed") type TelcoSmsAdapter struct { Client TelcoClient SenderID string } func (a TelcoSmsAdapter) Send(phone, message string) error { r := a.Client.Dispatch(VendorPayload{ MSISDN: strings.TrimPrefix(phone, "+"), Text: message, SenderID: a.SenderID, }) if r.Code != 0 { return fmt.Errorf("%w: telco error %d: %s", ErrDelivery, r.Code, r.Detail) } return nil } func SendLoginCode(sms SmsSender, phone, code string) error { return sms.Send(phone, "Your login code is "+code) } func main() { var sms SmsSender = TelcoSmsAdapter{SenderID: "ACME"} fmt.Println(SendLoginCode(sms, "+4915112345678", "482913")) // err := SendLoginCode(sms, "+12", "000") fmt.Println(errors.Is(err, ErrDelivery), err) } ``` ```java title="Notifications.java" public class Notifications { interface SmsSender { void send(String phone, String message); } // --- Third-party SDK (simulated). You cannot edit this code. --- record VendorPayload(String msisdn, String text, String senderId) {} record VendorResult(int code, String detail) {} static class TelcoClient { VendorResult dispatch(VendorPayload p) { if (!p.msisdn().matches("\\d{8,15}")) return new VendorResult(21, "invalid msisdn"); System.out.printf("[telco] %s -> %s: %s%n", p.senderId(), p.msisdn(), p.text()); return new VendorResult(0, "queued"); } } // --- end SDK --- static class SmsDeliveryException extends RuntimeException { SmsDeliveryException(String message) { super(message); } } static final class TelcoSmsAdapter implements SmsSender { private final TelcoClient client; private final String senderId; TelcoSmsAdapter(TelcoClient client, String senderId) { this.client = client; this.senderId = senderId; } @Override public void send(String phone, String message) { VendorResult r = client.dispatch( new VendorPayload(phone.replaceFirst("^\\+", ""), message, senderId)); if (r.code() != 0) { throw new SmsDeliveryException("telco error " + r.code() + ": " + r.detail()); } } } static void sendLoginCode(SmsSender sms, String phone, String code) { sms.send(phone, "Your login code is " + code); } public static void main(String[] args) { SmsSender sms = new TelcoSmsAdapter(new TelcoClient(), "ACME"); sendLoginCode(sms, "+4915112345678", "482913"); try { sendLoginCode(sms, "+12", "000"); } catch (SmsDeliveryException e) { System.out.println("failed: " + e.getMessage()); // failed: telco error 21: invalid msisdn } } } ``` ## Trade-offs **Gains** - Vendor types and quirks live in one file. Callers speak your domain's language. - Switching or adding a vendor means writing another adapter; client code does not change. - Tests swap in a fake `SmsSender` with no SDK, network or credentials. - Error translation happens once, consistently. **Costs** - One more type and one more hop when reading the call path. - A target interface designed too close to the first vendor will not fit the second; you end up adapting the adapter. - Features the target does not expose (delivery receipts, unicode options) are hidden until you widen the interface. - Adapters attract business logic over time. Keep them translation-only. ## Relations - [Bridge](https://aipatterns.dev/design-patterns/bridge.md) is designed up front so two hierarchies can vary independently. Adapter is applied after the fact to make existing parts fit. - [Decorator](https://aipatterns.dev/design-patterns/decorator.md) keeps the interface and adds behavior. Adapter changes the interface and adds none. - [Facade](https://aipatterns.dev/design-patterns/facade.md) defines a new, simpler interface over many classes. Adapter makes one class fit an interface that already exists. - [Proxy](https://aipatterns.dev/design-patterns/proxy.md) has the same interface as its subject and controls access to it. Adapter has a different interface from what it wraps. ## Agent checklist - [ ] The target interface is defined in your code's terms and contains only methods callers use. - [ ] You checked that you cannot change the adaptee itself; if you own it, rename or reshape it instead. - [ ] The adaptee is injected into the adapter, not constructed inside it. - [ ] Vendor errors and status codes are translated to your error types; no vendor exception escapes the adapter. - [ ] No imports of the adaptee's package remain outside the adapter and the composition root. - [ ] The adapter contains no business rules, and a fake of the target is used in client tests. --- # Bridge > Split a class that varies along two dimensions into two hierarchies joined by composition, so each side grows without multiplying subclasses. - kind: pattern - category: structural - aliases: Handle/Body - canonical: https://aipatterns.dev/design-patterns/bridge - inspired-by: https://refactoring.guru/design-patterns/bridge ## Intent Separate *what* an object does (the abstraction) from *how* the low-level work gets done (the implementation), and connect the two with a reference instead of inheritance. Each side gets its own hierarchy, so adding a report type or an output format is one new class, not one new class per combination. ## Forces - **Two independent dimensions** vs **one inheritance tree**: invoice and usage reports, times text and CSV output, gives `InvoiceTextReport`, `InvoiceCsvReport`, `UsageTextReport`, … and the count is n × m. - **High-level logic** vs **platform details**: what goes in a report should not change when you add a new output format, and the reverse. - **Compile-time binding** vs **runtime choice**: subclassing fixes the pairing forever; users pick the format per request. - **Shared code** vs **duplicated code**: in a combined tree, the invoice layout is copied into every format subclass ([Parallel Inheritance Hierarchies](https://aipatterns.dev/refactoring/smells/parallel-inheritance-hierarchies.md)). ## Therefore **Give the abstraction a field that holds an implementor interface, express the abstraction's high-level operations in terms of the implementor's primitive operations, and let both sides be subclassed independently.** The implementor interface is deliberately low level: `heading`, `row`, `document`. It knows nothing about invoices. The abstraction builds meaning out of those primitives. If you find yourself adding `renderInvoice` to the implementor, the split is in the wrong place. ## Structure ```mermaid classDiagram class Report { <> -Renderer out +render() String } class InvoiceReport { +render() String } class UsageReport { +render() String } class Renderer { <> +heading(text) String +row(cells) String +document(parts) String } class TextRenderer { +heading(text) String +row(cells) String +document(parts) String } class CsvRenderer { +heading(text) String +row(cells) String +document(parts) String } Report o-- Renderer : bridge Report <|-- InvoiceReport Report <|-- UsageReport Renderer <|.. TextRenderer Renderer <|.. CsvRenderer ``` ### Participants - **Abstraction** (`Report`): the high-level interface clients use. Holds a reference to an implementor. - **Refined abstractions** (`InvoiceReport`, `UsageReport`): variants of the high-level behavior, written only against the implementor interface. - **Implementor** (`Renderer`): the primitive operations every platform or backend must provide. - **Concrete implementors** (`TextRenderer`, `CsvRenderer`): one platform each, with no knowledge of reports. - **Client**: pairs an abstraction with an implementor, usually at the composition root. ## Use it when - A class hierarchy is growing in two directions at once and subclass names read like ``. - You need to pick or change the backend (renderer, storage engine, driver, OS API) at runtime, per request or per tenant. - Both dimensions will keep growing, and you want each new member to be one class. - You want to ship or test the high-level logic without the platform code, such as rendering reports against an in-memory recorder. - You are designing a library that must run on several platforms with a stable public API (the classic "handle/body" split). ## Do not use it when - Only one dimension varies. One interface with several implementations is plain dependency injection or [Strategy](https://aipatterns.dev/design-patterns/strategy.md); naming it a bridge adds nothing. - There is only one implementation and no concrete second one. An abstraction layer over a single backend is speculative generality; call the backend directly. - The two dimensions are not independent: some combinations are invalid or need special cases. You will end up with `if (renderer instanceof CsvRenderer)` in the abstraction, which defeats the split. Model the valid combinations explicitly instead. - You are integrating an existing class with the wrong interface. That is [Adapter](https://aipatterns.dev/design-patterns/adapter.md), applied after the fact. Bridge is a design decision made up front. - The refined abstractions are just data (report = title + rows). Pass a data structure to one renderer function per format; no abstraction hierarchy is needed. ## Application grill **Inspect first:** the two axes of variation and evidence that both really change (the existing subclass matrix or paired switches); the codebase's stance on inheritance versus composition. **Missing pieces:** none beyond the language: two small hierarchies joined by one composition edge. **Ask before you build:** - **Scale:** is each axis really growing, or is one dimension frozen - splitting a frozen axis is speculative generality? - **Performance:** does the indirection across the join sit on a hot path? - **Reliability:** when both sides hold resources, which side owns cleanup order? - **Budget:** the price is two hierarchies plus wiring; who reviews that on every change? - **Implementation:** are the cross-products tested table-driven, or does each combination need its own case? ## Implementation steps 1. Name the two dimensions. Confirm each has, or will soon have, at least two members. 2. List the primitive operations the high-level side needs from the low-level side. That list is the implementor interface. 3. Write one concrete implementor per platform, moving the platform-specific code out of the existing subclasses. 4. Create the abstraction with a constructor that takes an implementor and stores it. 5. Rewrite each high-level variant as a refined abstraction that calls only implementor primitives. 6. Delete the combined subclasses, and wire pairs together where the client decides (config, request parameter, factory). ## Example A billing service exports invoices and API usage reports. Finance wants CSV, and support wants plain text in emails. Each new report or format must be one class. ```ts title="reports.ts" // Implementor: low-level primitives, no knowledge of reports. interface Renderer { heading(text: string): string; row(cells: string[]): string; document(parts: string[]): string; } class TextRenderer implements Renderer { heading(text: string) { return `${text}\n${"=".repeat(text.length)}`; } row(cells: string[]) { return cells.map((c) => c.padEnd(20)).join("").trimEnd(); } document(parts: string[]) { return parts.filter(Boolean).join("\n"); } } class CsvRenderer implements Renderer { heading() { return ""; } // CSV has no headings row(cells: string[]) { return cells.map((c) => (/[",\n]/.test(c) ? `"${c.replace(/"/g, '""')}"` : c)).join(","); } document(parts: string[]) { return parts.filter(Boolean).join("\n"); } } // Abstraction: high-level meaning built from primitives. abstract class Report { constructor(protected readonly out: Renderer) {} abstract render(): string; } const money = (cents: number) => (cents / 100).toFixed(2); interface Line { item: string; qty: number; unitCents: number } class InvoiceReport extends Report { constructor(out: Renderer, private readonly id: string, private readonly lines: Line[]) { super(out); } render(): string { const total = this.lines.reduce((s, l) => s + l.qty * l.unitCents, 0); return this.out.document([ this.out.heading(`Invoice ${this.id}`), ...this.lines.map((l) => this.out.row([l.item, String(l.qty), money(l.qty * l.unitCents)])), this.out.row(["Total", "", money(total)]), ]); } } class UsageReport extends Report { constructor(out: Renderer, private readonly account: string, private readonly daily: [string, number][]) { super(out); } render(): string { return this.out.document([ this.out.heading(`API usage for ${this.account}`), ...this.daily.map(([day, calls]) => this.out.row([day, String(calls)])), ]); } } const lines: Line[] = [ { item: "Pro plan", qty: 1, unitCents: 4900 }, { item: "Extra seats", qty: 3, unitCents: 1500 }, { item: "Support, priority", qty: 1, unitCents: 9900 }, ]; for (const out of [new TextRenderer(), new CsvRenderer()]) { console.log(new InvoiceReport(out, "INV-1042", lines).render(), "\n"); console.log(new UsageReport(out, "acme", [["2026-10-01", 18234], ["2026-10-02", 20511]]).render(), "\n"); } ``` ```python title="reports.py" import csv import io from abc import ABC, abstractmethod from typing import Protocol class Renderer(Protocol): def heading(self, text: str) -> str: ... def row(self, cells: list[str]) -> str: ... def document(self, parts: list[str]) -> str: ... class TextRenderer: def heading(self, text: str) -> str: return f"{text}\n{'=' * len(text)}" def row(self, cells: list[str]) -> str: return "".join(c.ljust(20) for c in cells).rstrip() def document(self, parts: list[str]) -> str: return "\n".join(p for p in parts if p) class CsvRenderer: def heading(self, text: str) -> str: return "" # CSV has no headings def row(self, cells: list[str]) -> str: buf = io.StringIO() csv.writer(buf, lineterminator="").writerow(cells) return buf.getvalue() def document(self, parts: list[str]) -> str: return "\n".join(p for p in parts if p) class Report(ABC): def __init__(self, out: Renderer) -> None: self.out = out @abstractmethod def render(self) -> str: ... def money(cents: int) -> str: return f"{cents / 100:.2f}" class InvoiceReport(Report): def __init__(self, out: Renderer, invoice_id: str, lines: list[tuple[str, int, int]]) -> None: super().__init__(out) self.invoice_id, self.lines = invoice_id, lines def render(self) -> str: total = sum(qty * unit for _, qty, unit in self.lines) return self.out.document([ self.out.heading(f"Invoice {self.invoice_id}"), *(self.out.row([item, str(qty), money(qty * unit)]) for item, qty, unit in self.lines), self.out.row(["Total", "", money(total)]), ]) class UsageReport(Report): def __init__(self, out: Renderer, account: str, daily: list[tuple[str, int]]) -> None: super().__init__(out) self.account, self.daily = account, daily def render(self) -> str: return self.out.document([ self.out.heading(f"API usage for {self.account}"), *(self.out.row([day, str(calls)]) for day, calls in self.daily), ]) if __name__ == "__main__": lines = [("Pro plan", 1, 4900), ("Extra seats", 3, 1500), ("Support, priority", 1, 9900)] for out in (TextRenderer(), CsvRenderer()): print(InvoiceReport(out, "INV-1042", lines).render(), "\n") print(UsageReport(out, "acme", [("2026-10-01", 18234), ("2026-10-02", 20511)]).render(), "\n") ``` ```go title="reports.go" package main import ( "encoding/csv" "fmt" "strings" ) // Renderer is the implementor: primitives only. type Renderer interface { Heading(text string) string Row(cells []string) string Document(parts []string) string } func joinNonEmpty(parts []string) string { kept := parts[:0:0] for _, p := range parts { if p != "" { kept = append(kept, p) } } return strings.Join(kept, "\n") } type TextRenderer struct{} func (TextRenderer) Heading(t string) string { return t + "\n" + strings.Repeat("=", len(t)) } func (TextRenderer) Row(cells []string) string { var b strings.Builder for _, c := range cells { fmt.Fprintf(&b, "%-20s", c) } return strings.TrimRight(b.String(), " ") } func (TextRenderer) Document(parts []string) string { return joinNonEmpty(parts) } type CsvRenderer struct{} func (CsvRenderer) Heading(string) string { return "" } // CSV has no headings func (CsvRenderer) Row(cells []string) string { var b strings.Builder w := csv.NewWriter(&b) _ = w.Write(cells) // writes to memory; cannot fail w.Flush() return strings.TrimSuffix(b.String(), "\n") } func (CsvRenderer) Document(parts []string) string { return joinNonEmpty(parts) } // Report is the abstraction; each struct below is a refined abstraction. type Report interface{ Render() string } type Line struct { Item string Qty int UnitCents int } func money(cents int) string { return fmt.Sprintf("%.2f", float64(cents)/100) } type InvoiceReport struct { Out Renderer ID string Lines []Line } func (r InvoiceReport) Render() string { parts := []string{r.Out.Heading("Invoice " + r.ID)} total := 0 for _, l := range r.Lines { total += l.Qty * l.UnitCents parts = append(parts, r.Out.Row([]string{l.Item, fmt.Sprint(l.Qty), money(l.Qty * l.UnitCents)})) } parts = append(parts, r.Out.Row([]string{"Total", "", money(total)})) return r.Out.Document(parts) } type UsageReport struct { Out Renderer Account string Days []string Calls []int } func (r UsageReport) Render() string { parts := []string{r.Out.Heading("API usage for " + r.Account)} for i, day := range r.Days { parts = append(parts, r.Out.Row([]string{day, fmt.Sprint(r.Calls[i])})) } return r.Out.Document(parts) } func main() { lines := []Line{{"Pro plan", 1, 4900}, {"Extra seats", 3, 1500}, {"Support, priority", 1, 9900}} for _, out := range []Renderer{TextRenderer{}, CsvRenderer{}} { reports := []Report{ InvoiceReport{Out: out, ID: "INV-1042", Lines: lines}, UsageReport{Out: out, Account: "acme", Days: []string{"2026-10-01", "2026-10-02"}, Calls: []int{18234, 20511}}, } for _, r := range reports { fmt.Println(r.Render() + "\n") } } } ``` ```java title="Reports.java" import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; public class Reports { interface Renderer { String heading(String text); String row(List cells); default String document(List parts) { return parts.stream().filter(p -> !p.isEmpty()).collect(Collectors.joining("\n")); } } static final class TextRenderer implements Renderer { public String heading(String t) { return t + "\n" + "=".repeat(t.length()); } public String row(List cells) { return cells.stream().map(c -> String.format("%-20s", c)).collect(Collectors.joining()).stripTrailing(); } } static final class CsvRenderer implements Renderer { public String heading(String t) { return ""; } // CSV has no headings public String row(List cells) { return cells.stream() .map(c -> c.matches("(?s).*[\",\n].*") ? "\"" + c.replace("\"", "\"\"") + "\"" : c) .collect(Collectors.joining(",")); } } abstract static class Report { protected final Renderer out; Report(Renderer out) { this.out = out; } abstract String render(); } record Line(String item, int qty, long unitCents) {} static String money(long cents) { return String.format("%.2f", cents / 100.0); } static final class InvoiceReport extends Report { private final String id; private final List lines; InvoiceReport(Renderer out, String id, List lines) { super(out); this.id = id; this.lines = lines; } String render() { List parts = new ArrayList<>(List.of(out.heading("Invoice " + id))); long total = 0; for (Line l : lines) { total += l.qty() * l.unitCents(); parts.add(out.row(List.of(l.item(), String.valueOf(l.qty()), money(l.qty() * l.unitCents())))); } parts.add(out.row(List.of("Total", "", money(total)))); return out.document(parts); } } static final class UsageReport extends Report { private final String account; private final List> daily; UsageReport(Renderer out, String account, List> daily) { super(out); this.account = account; this.daily = daily; } String render() { List parts = new ArrayList<>(List.of(out.heading("API usage for " + account))); daily.forEach(d -> parts.add(out.row(d))); return out.document(parts); } } public static void main(String[] args) { List lines = List.of(new Line("Pro plan", 1, 4900), new Line("Extra seats", 3, 1500), new Line("Support, priority", 1, 9900)); for (Renderer out : List.of(new TextRenderer(), new CsvRenderer())) { System.out.println(new InvoiceReport(out, "INV-1042", lines).render() + "\n"); System.out.println(new UsageReport(out, "acme", List.of(List.of("2026-10-01", "18234"), List.of("2026-10-02", "20511"))).render() + "\n"); } } } ``` Two reports and two formats take four classes. A third format, such as HTML, is one more class and works with every report. ## Trade-offs **Gains** - Class count grows as n + m instead of n × m. - High-level logic and platform code change independently and are tested independently. - The implementor can be chosen or swapped at runtime. - Platform details stay out of the client-facing API. **Costs** - The hardest part is choosing the primitives. A wrong implementor interface forces changes on every concrete implementor. - Indirection: reading a report means reading two hierarchies. - Capabilities only one platform has (CSV has no headings) must be expressed as no-ops or optional features. - Premature for codebases where the second dimension never arrives. ## Relations - [Adapter](https://aipatterns.dev/design-patterns/adapter.md) makes unrelated existing classes fit after the fact. Bridge separates the hierarchies on purpose, before they tangle. - [Strategy](https://aipatterns.dev/design-patterns/strategy.md) has the same delegation shape, but swaps one algorithm. Bridge splits a whole abstraction from its implementation, and both sides have their own hierarchies. - [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md) can create a matching set of implementors (renderer, paginator, stylesheet) for one platform. - [State](https://aipatterns.dev/design-patterns/state.md) also delegates to a changeable object, but the object changes itself as the context's situation changes. ## Agent checklist - [ ] Both dimensions have at least two real members, or the second is concretely planned. - [ ] The implementor interface contains primitives only, with no domain words from the abstraction side. - [ ] Refined abstractions never check the concrete implementor type. - [ ] Every valid pairing works; invalid pairings, if any, are rejected at construction, not inside methods. - [ ] The old combined subclasses are deleted and their tests now cover each side separately. --- # Composite > Treat single objects and nested groups of them through one interface, so code can walk a tree of any depth without checking node types. - kind: pattern - category: structural - aliases: Object Tree, Part-Whole Hierarchy - canonical: https://aipatterns.dev/design-patterns/composite - inspired-by: https://refactoring.guru/design-patterns/composite ## Intent Represent part-whole hierarchies as a tree in which leaves and containers share one interface. A container implements each operation by delegating to its children and combining their results, so client code calls the same method on a single item or on a whole subtree. ## Forces - **Uniform client code** vs **two kinds of node**: callers want `node.monthlyCost()`, not `if (node is Group) ... else ...` at every use. - **Arbitrary depth** vs **fixed loops**: nested loops work for exactly two levels; org charts, folders and cost centers have as many as they like. - **Type safety** vs **one interface**: putting `add(child)` on leaves keeps the interface uniform but lets callers do nonsense; leaving it off means callers need to know which node they hold. - **Recomputing on every call** vs **caching aggregates**: a deep tree recalculates its totals on every read unless you cache, and caching needs invalidation. ## Therefore **Define one component interface for the operations that make sense on both a single item and a group; leaves implement them directly, and composites implement them by looping over their children.** Recursion does the work: a composite does not know whether its children are leaves or other composites. Keep child management (`add`, `remove`) on the composite type unless callers truly need to build trees through the component interface. ## Structure ```mermaid classDiagram class CostNode { <> +name() String +monthlyCost() Money +lines(depth) List } class Resource { -Money cost +monthlyCost() Money +lines(depth) List } class Group { -CostNode[] children +add(node) Group +monthlyCost() Money +lines(depth) List } class BudgetAlert { +check(node, limit) List } CostNode <|.. Resource CostNode <|.. Group Group o-- CostNode : children BudgetAlert ..> CostNode ``` ### Participants - **Component** (`CostNode`): the operations shared by leaves and containers. - **Leaf** (`Resource`): a node with no children that does the real work (here, knows its own cost). - **Composite** (`Group`): holds child components, delegates to them, and combines the results. - **Client** (`BudgetAlert`): works through the component interface and never checks which kind it holds. ## Use it when - Your data is a tree of unbounded depth: folders, org units, menu items, cost centers, UI widgets, bill-of-materials parts. - Clients repeat the same `instanceof`/type switch to tell single items from groups. - An operation on a group is the combination of the same operation on its members: totals, counts, rendering, validation, enable/disable. - You want to pass a single item wherever a group is accepted, and the reverse. ## Do not use it when - The tree has a fixed, known shape, such as "teams contain resources" and nothing deeper. Two nested loops over plain data are clearer. - Leaves and groups share almost no operations. Forcing them into one interface fills leaves with methods that throw `UnsupportedOperationException`. Keep two types. - The node types are stable but the operations change often. A data-only tree (a TypeScript discriminated union, a Java `sealed` interface with records, Python dataclasses with `match`) plus one recursive function per operation is simpler; see also [Visitor](https://aipatterns.dev/design-patterns/visitor.md). - The structure is really a graph with shared children or cycles. Composite assumes each node has one parent; use a graph model and an explicit traversal with a visited set. - You only need to flatten nested lists once. A recursive generator or `flatMap` does it without new types. ## Application grill **Inspect first:** the tree shapes and the code that walks them; whether nesting is user-controlled (menus, org charts, comment threads) or fixed; existing recursion or iteration helpers. **Missing pieces:** none beyond the language: one interface for leaf and group. Deep, user-controlled trees need an explicit stack instead of recursion, so hostile depth cannot exhaust the call stack. **Ask before you build:** - **Scale:** how deep can a tree get, and is depth bounded by construction or by user input? - **Performance:** does a small change re-walk the whole tree, and is incremental update needed? - **Reliability:** can a cycle exist (a child pointing at its ancestor), and where is that rejected? - **Budget:** one node interface simplifies callers, but every node kind now implements it - is the type count owned? - **Implementation:** are node kinds a closed set (a switch is fine) or open (visitors and plugins must extend)? ## Implementation steps 1. Confirm the domain is a recursive tree and list the operations that apply to both single items and groups. 2. Declare the component interface with only those operations. 3. Implement leaf classes for the concrete items. 4. Implement the composite: a child collection, `add`/`remove`, and each operation as a loop over children that combines results. 5. Replace client type checks with calls on the component interface. 6. If aggregates are expensive and the tree is large, add caching in the composite with explicit invalidation on `add`/`remove`, and measure first. ## Example A FinOps tool rolls up monthly cloud spend across nested groups (company, department, team) and flags any node over budget, whether it is one database or a whole department. ```ts title="costs.ts" const usd = (cents: number) => `$${(cents / 100).toFixed(2)}`; interface CostNode { readonly name: string; monthlyCost(): number; // cents lines(depth?: number): string[]; } class Resource implements CostNode { constructor(readonly name: string, private readonly cents: number) {} monthlyCost(): number { return this.cents; } lines(depth = 0): string[] { return [`${" ".repeat(depth)}${this.name} ${usd(this.cents)}`]; } } class Group implements CostNode { private readonly children: CostNode[] = []; constructor(readonly name: string) {} add(...nodes: CostNode[]): this { this.children.push(...nodes); return this; } monthlyCost(): number { return this.children.reduce((sum, c) => sum + c.monthlyCost(), 0); } lines(depth = 0): string[] { const head = `${" ".repeat(depth)}${this.name}/ ${usd(this.monthlyCost())}`; return [head, ...this.children.flatMap((c) => c.lines(depth + 1))]; } } // Client: one rule, any node. function overBudget(node: CostNode, limitCents: number): string | null { const cost = node.monthlyCost(); return cost > limitCents ? `${node.name} is ${usd(cost - limitCents)} over budget` : null; } const payments = new Group("payments").add( new Resource("postgres-primary", 84000), new Resource("api-cluster", 126000), ); const search = new Group("search").add(new Resource("opensearch", 61000)); const company = new Group("acme").add( new Group("engineering").add(payments, search), new Resource("office-vpn", 9000), ); console.log(company.lines().join("\n")); for (const node of [company, payments, new Resource("ci-runners", 45000)]) { console.log(overBudget(node, 200000) ?? `${node.name} ok`); } ``` ```python title="costs.py" from __future__ import annotations from dataclasses import dataclass, field from typing import Protocol def usd(cents: int) -> str: return f"${cents / 100:.2f}" class CostNode(Protocol): name: str def monthly_cost(self) -> int: ... def lines(self, depth: int = 0) -> list[str]: ... @dataclass class Resource: name: str cents: int def monthly_cost(self) -> int: return self.cents def lines(self, depth: int = 0) -> list[str]: return [f"{' ' * depth}{self.name} {usd(self.cents)}"] @dataclass class Group: name: str children: list[CostNode] = field(default_factory=list) def add(self, *nodes: CostNode) -> Group: self.children.extend(nodes) return self def monthly_cost(self) -> int: return sum(c.monthly_cost() for c in self.children) def lines(self, depth: int = 0) -> list[str]: head = f"{' ' * depth}{self.name}/ {usd(self.monthly_cost())}" return [head] + [line for c in self.children for line in c.lines(depth + 1)] def over_budget(node: CostNode, limit_cents: int) -> str | None: cost = node.monthly_cost() return f"{node.name} is {usd(cost - limit_cents)} over budget" if cost > limit_cents else None if __name__ == "__main__": payments = Group("payments").add(Resource("postgres-primary", 84000), Resource("api-cluster", 126000)) search = Group("search").add(Resource("opensearch", 61000)) company = Group("acme").add(Group("engineering").add(payments, search), Resource("office-vpn", 9000)) print("\n".join(company.lines())) for node in (company, payments, Resource("ci-runners", 45000)): print(over_budget(node, 200000) or f"{node.name} ok") ``` ```go title="costs.go" package main import ( "fmt" "strings" ) func usd(cents int) string { return fmt.Sprintf("$%.2f", float64(cents)/100) } type CostNode interface { Name() string MonthlyCost() int // cents Lines(depth int) []string } type Resource struct { name string cents int } func (r Resource) Name() string { return r.name } func (r Resource) MonthlyCost() int { return r.cents } func (r Resource) Lines(depth int) []string { return []string{fmt.Sprintf("%s%s %s", strings.Repeat(" ", depth), r.name, usd(r.cents))} } type Group struct { name string children []CostNode } func NewGroup(name string, children ...CostNode) *Group { return &Group{name: name, children: children} } func (g *Group) Add(nodes ...CostNode) { g.children = append(g.children, nodes...) } func (g *Group) Name() string { return g.name } func (g *Group) MonthlyCost() int { sum := 0 for _, c := range g.children { sum += c.MonthlyCost() } return sum } func (g *Group) Lines(depth int) []string { out := []string{fmt.Sprintf("%s%s/ %s", strings.Repeat(" ", depth), g.name, usd(g.MonthlyCost()))} for _, c := range g.children { out = append(out, c.Lines(depth+1)...) } return out } func overBudget(n CostNode, limit int) string { if cost := n.MonthlyCost(); cost > limit { return fmt.Sprintf("%s is %s over budget", n.Name(), usd(cost-limit)) } return n.Name() + " ok" } func main() { payments := NewGroup("payments", Resource{"postgres-primary", 84000}, Resource{"api-cluster", 126000}) search := NewGroup("search", Resource{"opensearch", 61000}) company := NewGroup("acme", NewGroup("engineering", payments, search), Resource{"office-vpn", 9000}) fmt.Println(strings.Join(company.Lines(0), "\n")) for _, n := range []CostNode{company, payments, Resource{"ci-runners", 45000}} { fmt.Println(overBudget(n, 200000)) } } ``` ```java title="Costs.java" import java.util.ArrayList; import java.util.List; public class Costs { static String usd(long cents) { return String.format("$%.2f", cents / 100.0); } interface CostNode { String name(); long monthlyCost(); // cents List lines(int depth); } record Resource(String name, long cents) implements CostNode { public long monthlyCost() { return cents; } public List lines(int depth) { return List.of(" ".repeat(depth) + name + " " + usd(cents)); } } static final class Group implements CostNode { private final String name; private final List children = new ArrayList<>(); Group(String name, CostNode... nodes) { this.name = name; children.addAll(List.of(nodes)); } Group add(CostNode node) { children.add(node); return this; } public String name() { return name; } public long monthlyCost() { return children.stream().mapToLong(CostNode::monthlyCost).sum(); } public List lines(int depth) { List out = new ArrayList<>(); out.add(" ".repeat(depth) + name + "/ " + usd(monthlyCost())); children.forEach(c -> out.addAll(c.lines(depth + 1))); return out; } } static String overBudget(CostNode node, long limit) { long cost = node.monthlyCost(); return cost > limit ? node.name() + " is " + usd(cost - limit) + " over budget" : node.name() + " ok"; } public static void main(String[] args) { Group payments = new Group("payments", new Resource("postgres-primary", 84000), new Resource("api-cluster", 126000)); Group search = new Group("search", new Resource("opensearch", 61000)); Group company = new Group("acme", new Group("engineering", payments, search), new Resource("office-vpn", 9000)); System.out.println(String.join("\n", company.lines(0))); for (CostNode n : List.of(company, payments, new Resource("ci-runners", 45000))) { System.out.println(overBudget(n, 200000)); } } } ``` `overBudget` receives a company, a team and a single resource and handles all three the same way. That uniformity is the whole point. ## Trade-offs **Gains** - Client code is type-check-free and works at any depth. - New leaf or composite types plug in without client changes. - Aggregation logic lives in one place, the composite, instead of in every caller. - Trees can be built from configuration or data at runtime. **Costs** - The component interface tends to be either too wide (leaves carry meaningless child methods) or too narrow (clients cast to reach `add`). - It is harder to enforce constraints such as "a team may not contain a company" through the type system. - Naive aggregates are recomputed on every call; `lines` above recomputes subtotals at each level. - Deep recursion can overflow the stack on pathological inputs; untrusted trees may need an explicit stack. ## Relations - [Decorator](https://aipatterns.dev/design-patterns/decorator.md) has a similar recursive shape, but wraps exactly one component to add behavior. A composite holds many children and combines their results. - [Iterator](https://aipatterns.dev/design-patterns/iterator.md) walks a composite tree without exposing its internal structure. - [Visitor](https://aipatterns.dev/design-patterns/visitor.md) adds new operations over a composite tree without editing every node class. - [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) often runs along a composite's parent links, passing a request from a leaf up to its ancestors. - [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md) can share identical leaves to save memory in very large trees, as long as they hold no parent reference. ## Agent checklist - [ ] The structure is a real tree of variable depth, not a fixed two-level grouping. - [ ] The component interface contains only operations meaningful for both leaves and composites. - [ ] No client code checks whether a node is a leaf or a composite after the change. - [ ] Each node has at most one parent, and cycles are impossible or rejected on `add`. - [ ] Tests cover an empty composite, a single leaf, and a nested tree at least three levels deep. --- # Decorator > Wrap an object in another object with the same interface that adds behavior before or after delegating, so features stack without subclassing. - kind: pattern - category: structural - aliases: Wrapper - canonical: https://aipatterns.dev/design-patterns/decorator - inspired-by: https://refactoring.guru/design-patterns/decorator ## Intent Add responsibilities to an object at runtime by wrapping it in another object that implements the same interface. The wrapper does its extra work (retry, cache, log, measure, authorize) and delegates the core call to the object inside. Because the wrapper looks exactly like the original, wrappers nest in any order. ## Forces - **Optional features** vs **subclass explosion**: retry, caching and logging in every combination would need `RetryingCachingLoggingRates` and seven siblings. - **Cross-cutting concerns** vs **focused classes**: the HTTP client should fetch rates, not also count latency and back off on timeouts. - **Configuring per use** vs **fixed at compile time**: tests want no retries, production wants three, a batch job wants caching too. - **Ordering matters** vs **hidden ordering**: cache-outside-retry behaves differently from retry-outside-cache, and the order must be visible somewhere. ## Therefore **Implement the component's interface in a wrapper class that holds another instance of that interface, add your behavior around the delegated call, and compose wrappers at the composition root.** Each decorator does one thing. The client receives the outermost wrapper as a plain `RateSource` and cannot tell it from the real one. The stacking order is written once, where the object graph is built. ## Structure ```mermaid classDiagram class RateSource { <> +rate(pair) Number } class RemoteRates { +rate(pair) Number } class RateDecorator { <> -RateSource inner +rate(pair) Number } class Retrying { -int attempts +rate(pair) Number } class Caching { -Map cache +rate(pair) Number } class Logging { +rate(pair) Number } RateSource <|.. RemoteRates RateSource <|.. RateDecorator RateDecorator o-- RateSource : inner RateDecorator <|-- Retrying RateDecorator <|-- Caching RateDecorator <|-- Logging ``` ### Participants - **Component** (`RateSource`): the interface both the real object and every wrapper implement. - **Concrete component** (`RemoteRates`): the object doing the core work. - **Base decorator** (`RateDecorator`): optional; holds `inner` and forwards by default. Useful when the interface has many methods and each decorator overrides only a few. With one method, skip it, as the example does. - **Concrete decorators** (`Retrying`, `Caching`, `Logging`): one added behavior each. - **Client**: depends on `RateSource` only. The composition root decides the stack. ## Use it when - You need to add retry, caching, metrics, tracing, rate limiting or authorization to an object without editing it. - Features are optional and combinable, and each combination would otherwise be a subclass. - You cannot subclass the component (it is `final`, sealed, or built by a third-party factory), but you can wrap it. - Different environments need different stacks: no cache in tests, extra logging in staging. ## Do not use it when - The interface has one method and function composition fits. `withRetry(withCache(fetchRate))` is the whole decorator stack, with no wrapper classes. - You are in a web framework or RPC stack that already has middleware or interceptors (Express middleware, Go `http.Handler` wrappers, gRPC interceptors, Spring AOP). Use that mechanism instead of hand-rolled wrapper classes. - The extra behavior is always on. Put it in the class itself; a wrapper that every instance always gets is just indirection. - The interface is wide (twenty methods) and you only want to change one. Every decorator must forward the other nineteen; prefer a function hook, an event, or a narrower interface. - Callers need to reach the concrete type behind the wrapper (`instanceof RemoteRates`). Decorators break identity checks; fix the caller first. - The wrapper changes the interface. That is [Adapter](https://aipatterns.dev/design-patterns/adapter.md). Note that Python's `@decorator` syntax is a related but different tool: it wraps a function at definition time. For one function, it is often the right choice over this pattern. ## Application grill **Inspect first:** the cross-cutting behavior already present as middleware, wrappers or higher-order functions (fetch wrappers, tracing clients) - the pattern may exist unlabelled; where instances are constructed, since stacking needs a wiring point; the interface's stability. **Missing pieces:** usually none for in-process stacking. If decorators would wrap an HTTP client for retries, caching or auth, check what the platform already provides (interceptors, SDK middleware stacks, OpenTelemetry instrumentation) before writing wrappers. **Ask before you build:** - **Scale:** how many behaviors stack on one object - three deep is readable, ten deep means the composition is the problem? - **Performance:** what does each layer allocate per call on hot paths? - **Reliability:** which layer owns errors, and is the ordering load-bearing (does auth wrap retry, or retry wrap auth)? - **Budget:** who owns the wiring order - it is logic, not configuration, and reordering changes behavior? - **Implementation:** do the same tests pass for base and decorated instances (the contract of an exact interface)? ## Implementation steps 1. Make sure callers depend on an interface, not on the concrete class. Extract one if needed ([Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md)). 2. Write one decorator per concern. Each takes the inner component in its constructor and implements the full interface. 3. In each method, do the extra work and delegate to `inner`. Forward unchanged methods as-is. 4. Remove the cross-cutting code from the concrete component once a decorator covers it. 5. Build the stack in one place (composition root, DI module, factory function) and document the order there. 6. Test each decorator alone against a fake inner component. ## Example A pricing service reads currency rates from a flaky upstream API. Production wraps it with retry, a cache and logging; tests use the bare fake. ```ts title="rates.ts" interface RateSource { rate(pair: string): number; } // Concrete component: an upstream API that times out on every other call. class RemoteRates implements RateSource { private calls = 0; private readonly table: Record = { "EUR/USD": 1.09, "GBP/USD": 1.27 }; rate(pair: string): number { if (++this.calls % 2 === 1) throw new Error(`timeout fetching ${pair}`); console.log(` [remote] served ${pair}`); return this.table[pair]; } } class Retrying implements RateSource { constructor(private readonly inner: RateSource, private readonly attempts: number) {} rate(pair: string): number { for (let i = 1; ; i++) { try { return this.inner.rate(pair); } catch (e) { if (i >= this.attempts) throw e; console.log(` [retry] attempt ${i} failed: ${(e as Error).message}`); } } } } class Caching implements RateSource { private readonly cache = new Map(); constructor(private readonly inner: RateSource) {} rate(pair: string): number { let r = this.cache.get(pair); if (r === undefined) { r = this.inner.rate(pair); this.cache.set(pair, r); } return r; } } class Logging implements RateSource { constructor(private readonly inner: RateSource) {} rate(pair: string): number { const r = this.inner.rate(pair); console.log(`rate(${pair}) = ${r}`); return r; } } // Order is explicit: cache hits skip retries; retries happen below the cache. const rates: RateSource = new Logging(new Caching(new Retrying(new RemoteRates(), 3))); const toUsd = (source: RateSource, amount: number, currency: string) => amount * source.rate(`${currency}/USD`); console.log(toUsd(rates, 100, "EUR").toFixed(2)); // timeout, retry, remote, 109.00 console.log(toUsd(rates, 250, "EUR").toFixed(2)); // cache hit, 272.50 console.log(toUsd(rates, 40, "GBP").toFixed(2)); // timeout, retry, remote, 50.80 ``` ```python title="rates.py" from typing import Protocol class RateSource(Protocol): def rate(self, pair: str) -> float: ... class RemoteRates: """Upstream API that times out on every other call.""" def __init__(self) -> None: self._calls = 0 self._table = {"EUR/USD": 1.09, "GBP/USD": 1.27} def rate(self, pair: str) -> float: self._calls += 1 if self._calls % 2 == 1: raise TimeoutError(f"timeout fetching {pair}") print(f" [remote] served {pair}") return self._table[pair] class Retrying: def __init__(self, inner: RateSource, attempts: int) -> None: self._inner, self._attempts = inner, attempts def rate(self, pair: str) -> float: for i in range(1, self._attempts + 1): try: return self._inner.rate(pair) except TimeoutError as e: if i == self._attempts: raise print(f" [retry] attempt {i} failed: {e}") raise AssertionError("unreachable") class Caching: def __init__(self, inner: RateSource) -> None: self._inner = inner self._cache: dict[str, float] = {} def rate(self, pair: str) -> float: if pair not in self._cache: self._cache[pair] = self._inner.rate(pair) return self._cache[pair] class Logging: def __init__(self, inner: RateSource) -> None: self._inner = inner def rate(self, pair: str) -> float: r = self._inner.rate(pair) print(f"rate({pair}) = {r}") return r def to_usd(source: RateSource, amount: float, currency: str) -> float: return amount * source.rate(f"{currency}/USD") if __name__ == "__main__": rates = Logging(Caching(Retrying(RemoteRates(), attempts=3))) print(f"{to_usd(rates, 100, 'EUR'):.2f}") # timeout, retry, remote, 109.00 print(f"{to_usd(rates, 250, 'EUR'):.2f}") # cache hit, 272.50 print(f"{to_usd(rates, 40, 'GBP'):.2f}") # timeout, retry, remote, 50.80 ``` ```go title="rates.go" package main import ( "fmt" "log" ) type RateSource interface { Rate(pair string) (float64, error) } // RemoteRates times out on every other call. type RemoteRates struct{ calls int } func (r *RemoteRates) Rate(pair string) (float64, error) { r.calls++ if r.calls%2 == 1 { return 0, fmt.Errorf("timeout fetching %s", pair) } fmt.Println(" [remote] served", pair) return map[string]float64{"EUR/USD": 1.09, "GBP/USD": 1.27}[pair], nil } type Retrying struct { Inner RateSource Attempts int } func (r Retrying) Rate(pair string) (float64, error) { var err error for i := 1; i <= r.Attempts; i++ { var v float64 if v, err = r.Inner.Rate(pair); err == nil { return v, nil } fmt.Printf(" [retry] attempt %d failed: %v\n", i, err) } return 0, err } type Caching struct { Inner RateSource cache map[string]float64 } func NewCaching(inner RateSource) *Caching { return &Caching{Inner: inner, cache: map[string]float64{}} } func (c *Caching) Rate(pair string) (float64, error) { if v, ok := c.cache[pair]; ok { return v, nil } v, err := c.Inner.Rate(pair) if err == nil { c.cache[pair] = v } return v, err } type Logging struct{ Inner RateSource } func (l Logging) Rate(pair string) (float64, error) { v, err := l.Inner.Rate(pair) fmt.Printf("rate(%s) = %v err=%v\n", pair, v, err) return v, err } func main() { var rates RateSource = Logging{NewCaching(Retrying{Inner: &RemoteRates{}, Attempts: 3})} for _, q := range []struct { amount float64 currency string }{{100, "EUR"}, {250, "EUR"}, {40, "GBP"}} { r, err := rates.Rate(q.currency + "/USD") if err != nil { log.Fatal(err) } fmt.Printf("%.2f\n", q.amount*r) } } ``` ```java title="Rates.java" import java.util.HashMap; import java.util.Map; public class Rates { interface RateSource { double rate(String pair); } /** Upstream API that times out on every other call. */ static final class RemoteRates implements RateSource { private int calls = 0; private final Map table = Map.of("EUR/USD", 1.09, "GBP/USD", 1.27); public double rate(String pair) { if (++calls % 2 == 1) throw new IllegalStateException("timeout fetching " + pair); System.out.println(" [remote] served " + pair); return table.get(pair); } } record Retrying(RateSource inner, int attempts) implements RateSource { public double rate(String pair) { for (int i = 1; ; i++) { try { return inner.rate(pair); } catch (IllegalStateException e) { if (i >= attempts) throw e; System.out.println(" [retry] attempt " + i + " failed: " + e.getMessage()); } } } } static final class Caching implements RateSource { private final RateSource inner; private final Map cache = new HashMap<>(); Caching(RateSource inner) { this.inner = inner; } public double rate(String pair) { Double hit = cache.get(pair); if (hit != null) return hit; double r = inner.rate(pair); cache.put(pair, r); return r; } } record Logging(RateSource inner) implements RateSource { public double rate(String pair) { double r = inner.rate(pair); System.out.println("rate(" + pair + ") = " + r); return r; } } static double toUsd(RateSource source, double amount, String currency) { return amount * source.rate(currency + "/USD"); } public static void main(String[] args) { RateSource rates = new Logging(new Caching(new Retrying(new RemoteRates(), 3))); System.out.printf("%.2f%n", toUsd(rates, 100, "EUR")); // timeout, retry, remote, 109.00 System.out.printf("%.2f%n", toUsd(rates, 250, "EUR")); // cache hit, 272.50 System.out.printf("%.2f%n", toUsd(rates, 40, "GBP")); // timeout, retry, remote, 50.80 } } ``` The function-composition version of the same stack in TypeScript is `const rate = logged(cached(retried(fetchRate, 3)))`, where each helper takes and returns `(pair: string) => number`. Prefer it when the interface is a single function. ## Trade-offs **Gains** - Each concern is a small class with a single job, tested alone. - Features combine freely at runtime without a subclass per combination. - The core component stays free of retry, cache and logging code. - You can decorate classes you cannot modify or subclass. **Costs** - Behavior depends on stacking order, and the wrong order (retry outside cache, logging inside retry) produces subtle bugs. - Stack traces and debuggers show layers of wrappers. - Identity and type checks fail: the outer object is not a `RemoteRates`. - Wide interfaces force each decorator to forward every method, which invites copy-paste. ## Relations - [Adapter](https://aipatterns.dev/design-patterns/adapter.md) changes an object's interface. Decorator keeps it and adds behavior. - [Proxy](https://aipatterns.dev/design-patterns/proxy.md) has the same shape, but controls access to the subject (lazy creation, permission, remote calls) and usually manages its lifecycle. Decorators are stacked by the client; a proxy usually is not. - [Composite](https://aipatterns.dev/design-patterns/composite.md) also recurses through a shared interface, but aggregates many children. A decorator has exactly one. - [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) links handlers in the same way, but any handler may stop the request. Decorators normally always delegate. - [Strategy](https://aipatterns.dev/design-patterns/strategy.md) swaps an object's internals. Decorator changes its outside. ## Agent checklist - [ ] Callers depend on an interface, so a wrapper can be substituted without changing them. - [ ] You considered function composition or the framework's middleware before writing wrapper classes. - [ ] Each decorator does one thing and forwards every method it does not change. - [ ] The stacking order is defined in one place and its reasoning is in a comment. - [ ] No caller checks the concrete type of the component after the change. - [ ] Each decorator has a test against a fake inner component, including the error path. --- # Facade > Give a complex subsystem one small entry point for a common task, so callers stop orchestrating its classes and calling them in the right order. - kind: pattern - category: structural - aliases: Service Layer, Gateway - canonical: https://aipatterns.dev/design-patterns/facade - inspired-by: https://refactoring.guru/design-patterns/facade ## Intent Provide a simple interface to a set of classes that must be called together, in order, with matching arguments. The facade owns the orchestration for one use case. Callers ask for the outcome ("hire this person") instead of driving five subsystems themselves. The subsystems stay public for callers that need fine control. ## Forces - **A one-line request** vs **a multi-step procedure**: onboarding means an account, group access, payroll and a laptop, in that order, with cleanup if a step fails. - **Duplicated orchestration** vs **one source of truth**: the HR portal, the CLI and the bulk-import job each copy the sequence and drift apart. - **Hiding complexity** vs **keeping control**: most callers want the default path; a few need to call one subsystem directly. - **Coupling** vs **convenience**: callers that import every subsystem break when any of them changes. ## Therefore **Create a class (or module) with one method per use case that calls the subsystem classes in the right order, handles their failures, and returns a result in the caller's terms.** The facade adds no new capability. It only sequences what already exists. Subsystems don't know the facade exists, and nothing stops a caller from bypassing it. Keep each facade focused on one area; several small facades beat one that fronts the whole system. ## Structure ```mermaid classDiagram class HrPortal { +submit(form) void } class Onboarding { +hire(newHire) OnboardingResult } class Directory { +createAccount(name) String +disable(email) void } class AccessControl { +grant(email, groups) void } class Payroll { +enroll(email, salary) String } class Equipment { +orderLaptop(email, model) String } HrPortal --> Onboarding Onboarding --> Directory Onboarding --> AccessControl Onboarding --> Payroll Onboarding --> Equipment ``` ### Participants - **Facade** (`Onboarding`): knows which subsystem classes handle each step, calls them in order, and handles partial failure. - **Subsystem classes** (`Directory`, `AccessControl`, `Payroll`, `Equipment`): do the real work and have no reference to the facade. - **Client** (`HrPortal`): calls the facade for the common path and may still use a subsystem directly for special cases. ## Use it when - Several callers repeat the same sequence of calls across the same group of classes. - A subsystem is hard to use correctly: required call order, shared IDs passed between steps, cleanup on failure. - You want to layer a system, so the upper layer talks to one entry point per lower-level area. - You are wrapping a sprawling library (a media toolkit, a cloud SDK) and your code uses a small slice of it. - You are preparing to replace a subsystem and want to shrink the surface that depends on it first. ## Do not use it when - The "facade" would front the whole application. One class with forty unrelated methods is a god object ([Large Class](https://aipatterns.dev/refactoring/smells/large-class.md)). Write one facade per use-case area instead. - Each facade method forwards to exactly one subsystem call. That is a [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md); delete it ([Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md)) and let callers use the class. - There is one caller. Put the sequence in a plain function next to that caller; promote it when a second caller appears. - Callers need fine-grained control over most steps. A facade that grows a flag per step (`skipPayroll`, `laptopModel`, `noGroups`) is hiding nothing; expose the subsystems. - You need to make one class fit an existing interface. That is [Adapter](https://aipatterns.dev/design-patterns/adapter.md). - Subsystem objects need to talk to each other in both directions. That is [Mediator](https://aipatterns.dev/design-patterns/mediator.md); a facade only calls down. ## Application grill **Inspect first:** the orchestration callers repeat today (setup, calls in order, teardown) and how many call sites repeat it; whether the subsystem is one package (a facade is one module) or scattered (a facade may hide that). **Missing pieces:** none beyond the language: one module with one exported function per common task. **Ask before you build:** - **Scale:** how many call sites repeat the orchestration - three or more makes the facade earn itself? - **Performance:** does the one-call convenience hide N+1 work inside that callers used to see and batch? - **Reliability:** which subsystem failures does the facade translate, and which bubble up untouched? - **Budget:** a facade that accumulates business logic stops being a facade - who keeps it thin? - **Implementation:** do tests exercise the facade end to end, or each subsystem separately? ## Implementation steps 1. Find the duplicated orchestration: the same calls, in the same order, in several callers. 2. Write the facade method signature in the caller's terms: what goes in and what outcome comes back. 3. Move one copy of the sequence into the facade, with the subsystems injected through its constructor. 4. Add the failure handling the callers were each doing (or forgetting): compensation, retries, consistent errors. 5. Replace each caller's copy with a call to the facade, running tests after each. 6. Leave the subsystems accessible. Remove direct subsystem imports only from callers that no longer need them. ## Example Three tools (the HR portal, a CLI and a CSV import job) each onboarded new employees by calling four internal services. Two of them forgot to disable the account when payroll enrollment failed. ```ts title="onboarding.ts" type Department = "engineering" | "sales"; interface NewHire { fullName: string; department: Department; annualSalary: number; laptop: string } interface OnboardingResult { email: string; employeeId: string; laptopTicket: string } // --- Subsystems: each one works alone and knows nothing of the facade --- class Directory { createAccount(fullName: string): string { const email = `${fullName.toLowerCase().replace(/\s+/g, ".")}@acme.io`; console.log(`[directory] created ${email}`); return email; } disable(email: string): void { console.log(`[directory] disabled ${email}`); } } class AccessControl { grant(email: string, groups: string[]): void { console.log(`[access] ${email}: ${groups.join(", ")}`); } } class Payroll { private nextId = 1001; enroll(email: string, annualSalary: number): string { if (annualSalary <= 0) throw new Error(`payroll rejected ${email}: invalid salary`); console.log(`[payroll] enrolled ${email}`); return `EMP-${this.nextId++}`; } } class Equipment { private nextTicket = 500; orderLaptop(email: string, model: string): string { console.log(`[it] ${model} for ${email}`); return `IT-${this.nextTicket++}`; } } // --- Facade --- const GROUPS: Record = { engineering: ["github", "aws-dev", "oncall"], sales: ["crm", "slack-sales"], }; class Onboarding { constructor( private readonly directory: Directory, private readonly access: AccessControl, private readonly payroll: Payroll, private readonly equipment: Equipment, ) {} hire(h: NewHire): OnboardingResult { const email = this.directory.createAccount(h.fullName); let employeeId: string; try { employeeId = this.payroll.enroll(email, h.annualSalary); } catch (e) { this.directory.disable(email); // no orphan accounts throw e; } this.access.grant(email, GROUPS[h.department]); const laptopTicket = this.equipment.orderLaptop(email, h.laptop); return { email, employeeId, laptopTicket }; } } const onboarding = new Onboarding(new Directory(), new AccessControl(), new Payroll(), new Equipment()); console.log(onboarding.hire({ fullName: "Dana Reyes", department: "engineering", annualSalary: 132000, laptop: "MBP 14" })); try { onboarding.hire({ fullName: "Sam Ito", department: "sales", annualSalary: 0, laptop: "XPS 13" }); } catch (e) { console.log((e as Error).message); } ``` ```python title="onboarding.py" from dataclasses import dataclass from itertools import count @dataclass(frozen=True) class NewHire: full_name: str department: str annual_salary: int laptop: str @dataclass(frozen=True) class OnboardingResult: email: str employee_id: str laptop_ticket: str # --- Subsystems --- class Directory: def create_account(self, full_name: str) -> str: email = ".".join(full_name.lower().split()) + "@acme.io" print(f"[directory] created {email}") return email def disable(self, email: str) -> None: print(f"[directory] disabled {email}") class AccessControl: def grant(self, email: str, groups: list[str]) -> None: print(f"[access] {email}: {', '.join(groups)}") class Payroll: def __init__(self) -> None: self._ids = count(1001) def enroll(self, email: str, annual_salary: int) -> str: if annual_salary <= 0: raise ValueError(f"payroll rejected {email}: invalid salary") print(f"[payroll] enrolled {email}") return f"EMP-{next(self._ids)}" class Equipment: def __init__(self) -> None: self._tickets = count(500) def order_laptop(self, email: str, model: str) -> str: print(f"[it] {model} for {email}") return f"IT-{next(self._tickets)}" # --- Facade --- GROUPS = {"engineering": ["github", "aws-dev", "oncall"], "sales": ["crm", "slack-sales"]} class Onboarding: def __init__(self, directory: Directory, access: AccessControl, payroll: Payroll, equipment: Equipment): self._directory, self._access = directory, access self._payroll, self._equipment = payroll, equipment def hire(self, h: NewHire) -> OnboardingResult: email = self._directory.create_account(h.full_name) try: employee_id = self._payroll.enroll(email, h.annual_salary) except ValueError: self._directory.disable(email) # no orphan accounts raise self._access.grant(email, GROUPS[h.department]) ticket = self._equipment.order_laptop(email, h.laptop) return OnboardingResult(email, employee_id, ticket) if __name__ == "__main__": onboarding = Onboarding(Directory(), AccessControl(), Payroll(), Equipment()) print(onboarding.hire(NewHire("Dana Reyes", "engineering", 132_000, "MBP 14"))) try: onboarding.hire(NewHire("Sam Ito", "sales", 0, "XPS 13")) except ValueError as e: print(e) ``` ```go title="onboarding.go" package main import ( "fmt" "strings" ) type NewHire struct { FullName, Department, Laptop string AnnualSalary int } type OnboardingResult struct{ Email, EmployeeID, LaptopTicket string } // --- Subsystems --- type Directory struct{} func (Directory) CreateAccount(fullName string) string { email := strings.Join(strings.Fields(strings.ToLower(fullName)), ".") + "@acme.io" fmt.Println("[directory] created", email) return email } func (Directory) Disable(email string) { fmt.Println("[directory] disabled", email) } type AccessControl struct{} func (AccessControl) Grant(email string, groups []string) { fmt.Printf("[access] %s: %s\n", email, strings.Join(groups, ", ")) } type Payroll struct{ nextID int } func (p *Payroll) Enroll(email string, salary int) (string, error) { if salary <= 0 { return "", fmt.Errorf("payroll rejected %s: invalid salary", email) } p.nextID++ fmt.Println("[payroll] enrolled", email) return fmt.Sprintf("EMP-%d", 1000+p.nextID), nil } type Equipment struct{ nextTicket int } func (e *Equipment) OrderLaptop(email, model string) string { e.nextTicket++ fmt.Printf("[it] %s for %s\n", model, email) return fmt.Sprintf("IT-%d", 499+e.nextTicket) } // --- Facade --- var groups = map[string][]string{ "engineering": {"github", "aws-dev", "oncall"}, "sales": {"crm", "slack-sales"}, } type Onboarding struct { Directory Directory Access AccessControl Payroll *Payroll Equipment *Equipment } func (o Onboarding) Hire(h NewHire) (OnboardingResult, error) { email := o.Directory.CreateAccount(h.FullName) id, err := o.Payroll.Enroll(email, h.AnnualSalary) if err != nil { o.Directory.Disable(email) // no orphan accounts return OnboardingResult{}, err } o.Access.Grant(email, groups[h.Department]) ticket := o.Equipment.OrderLaptop(email, h.Laptop) return OnboardingResult{email, id, ticket}, nil } func main() { o := Onboarding{Payroll: &Payroll{}, Equipment: &Equipment{}} fmt.Println(o.Hire(NewHire{FullName: "Dana Reyes", Department: "engineering", AnnualSalary: 132000, Laptop: "MBP 14"})) _, err := o.Hire(NewHire{FullName: "Sam Ito", Department: "sales", Laptop: "XPS 13"}) fmt.Println(err) } ``` ```java title="Onboarding.java" import java.util.List; import java.util.Map; public class Onboarding { record NewHire(String fullName, String department, long annualSalary, String laptop) {} record OnboardingResult(String email, String employeeId, String laptopTicket) {} // --- Subsystems --- static class Directory { String createAccount(String fullName) { String email = String.join(".", fullName.toLowerCase().trim().split("\\s+")) + "@acme.io"; System.out.println("[directory] created " + email); return email; } void disable(String email) { System.out.println("[directory] disabled " + email); } } static class AccessControl { void grant(String email, List groups) { System.out.println("[access] " + email + ": " + String.join(", ", groups)); } } static class Payroll { private int nextId = 1001; String enroll(String email, long annualSalary) { if (annualSalary <= 0) throw new IllegalArgumentException("payroll rejected " + email + ": invalid salary"); System.out.println("[payroll] enrolled " + email); return "EMP-" + nextId++; } } static class Equipment { private int nextTicket = 500; String orderLaptop(String email, String model) { System.out.println("[it] " + model + " for " + email); return "IT-" + nextTicket++; } } // --- Facade --- private static final Map> GROUPS = Map.of( "engineering", List.of("github", "aws-dev", "oncall"), "sales", List.of("crm", "slack-sales")); private final Directory directory; private final AccessControl access; private final Payroll payroll; private final Equipment equipment; Onboarding(Directory directory, AccessControl access, Payroll payroll, Equipment equipment) { this.directory = directory; this.access = access; this.payroll = payroll; this.equipment = equipment; } OnboardingResult hire(NewHire h) { String email = directory.createAccount(h.fullName()); String employeeId; try { employeeId = payroll.enroll(email, h.annualSalary()); } catch (IllegalArgumentException e) { directory.disable(email); // no orphan accounts throw e; } access.grant(email, GROUPS.get(h.department())); return new OnboardingResult(email, employeeId, equipment.orderLaptop(email, h.laptop())); } public static void main(String[] args) { Onboarding onboarding = new Onboarding(new Directory(), new AccessControl(), new Payroll(), new Equipment()); System.out.println(onboarding.hire(new NewHire("Dana Reyes", "engineering", 132_000, "MBP 14"))); try { onboarding.hire(new NewHire("Sam Ito", "sales", 0, "XPS 13")); } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } } } ``` The facade fixes the bug once: every caller now gets the compensation step. The IT team's asset tool still calls `Equipment` directly to order replacement laptops, and that is fine. ## Trade-offs **Gains** - One correct implementation of a multi-step procedure instead of several drifting copies. - Callers depend on one entry point, not on every subsystem class. - A natural seam for tests (fake the facade) and for swapping subsystems later. - Failure handling and ordering rules live in one place. **Costs** - A facade attracts every new "just add it here" method and can grow into a god object. - It can hide useful subsystem features, pushing callers to bypass it inconsistently. - An extra layer to read when the sequence is short. - The facade becomes a coupling point to every subsystem it fronts; changes in any of them land here. ## Relations - [Adapter](https://aipatterns.dev/design-patterns/adapter.md) makes one existing class match an interface someone else defined. A facade defines a new, simpler interface over several classes. - [Mediator](https://aipatterns.dev/design-patterns/mediator.md) also centralizes coordination, but its colleagues know it and talk through it. Subsystems never know about a facade. - [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md) can sit behind a facade to hide how subsystem objects are created. - [Singleton](https://aipatterns.dev/design-patterns/singleton.md) is often misused for facades; one instance is usually enough, but inject it rather than reaching for a global. - [Proxy](https://aipatterns.dev/design-patterns/proxy.md) keeps its subject's interface exactly. A facade offers a different, smaller interface. ## Agent checklist - [ ] At least two callers duplicated the orchestration, or the sequence has a correctness rule (order, cleanup) worth enforcing once. - [ ] Each facade method represents a use case and does more than forward one call. - [ ] Subsystems are injected into the facade and have no reference back to it. - [ ] Partial failures are handled (compensation or a clear error), and tested. - [ ] The facade covers one area; you split it before it reaches unrelated responsibilities. - [ ] Callers with special needs can still reach the subsystems directly. --- # Flyweight > Share one immutable object for state that many objects have in common, and pass the per-object rest in, to cut memory when instances number in the millions. - kind: pattern - category: structural - aliases: Interning, Hash Consing - canonical: https://aipatterns.dev/design-patterns/flyweight - inspired-by: https://refactoring.guru/design-patterns/flyweight ## Intent Fit far more objects in memory by splitting each object's state in two. The *intrinsic* part, identical across many objects, moves into a shared, immutable flyweight. The *extrinsic* part, unique to each object, stays with the object or is passed into the flyweight's methods. A factory hands out one flyweight per distinct intrinsic value. ## Forces - **Millions of fine-grained objects** vs **finite memory**: a spreadsheet with 600,000 cells cannot afford 600,000 copies of the same font, size and number format. - **Sharing** vs **mutability**: a shared object that one owner changes silently changes for every other owner. - **Object-oriented clarity** vs **memory layout**: each cell "having a style" reads well; each cell *owning* a style wastes space. - **Saving memory** vs **spending CPU**: lookups in the flyweight pool and passing extrinsic state around cost time. ## Therefore **Move the repeated, immutable state into a shared object, obtain instances only through a factory that returns an existing one when an equal value is requested, and keep or pass the varying state outside it.** The flyweight must be immutable. Without that, sharing is a bug. The factory is a map from the intrinsic value to the one shared instance. In many languages this is a few lines, and sometimes the runtime already does it for you (string interning, enums, small integer caches). ## Structure ```mermaid classDiagram class CellStyle { -String font -int sizePt -boolean bold -String numberFormat +format(value) String } class StyleRegistry { -Map pool +get(font, sizePt, bold, numberFormat) CellStyle +size() int } class Cell { -int row -int col -double value -CellStyle style } class Sheet { -Cell[] cells +set(row, col, value, style) void } StyleRegistry o-- CellStyle : pool Cell --> CellStyle : shares Sheet *-- Cell Sheet ..> StyleRegistry ``` ### Participants - **Flyweight** (`CellStyle`): immutable intrinsic state. Methods take extrinsic state as arguments (`format(value)`). - **Flyweight factory** (`StyleRegistry`): returns the existing instance for an equal key, or creates and stores a new one. - **Context** (`Cell`): holds the extrinsic state (position, value) and a reference to a shared flyweight. - **Client** (`Sheet`): creates contexts and gets flyweights only through the factory, never with `new`. ## Use it when - A profiler or heap dump shows millions of objects with duplicated field values dominating memory. - Most of each object's state can be made immutable and is shared by many objects: styles, glyph metrics, tile types, map-marker icons, parsed schema fragments. - The remaining per-object state is small or can be computed from context (position, index). - Object identity does not matter for the shared part; equal values may be the same instance. ## Do not use it when - You have not measured memory pressure. Thousands, or even a few hundred thousand, small objects are cheap on modern runtimes. Profile first; flyweight without a measured problem is complexity for nothing. - The language or runtime already deduplicates the value. Use string interning (`sys.intern`, `String.intern()` with care), an `enum`, or a frozen constant table instead. - The shared part is mutable or would need per-owner changes. Copy-on-write or plain separate objects are safer. - A columnar layout fits better. Storing a `uint16` style index per cell in a parallel array (struct of arrays) saves more memory than object references and needs no pattern vocabulary. - You only need to cache expensive *results*. That is memoization (`functools.cache`, a `Map`), not object sharing. ## Application grill **Inspect first:** the measured object counts and heap profile first - this pattern is for the millions, not the thousands; the state that repeats (immutable strings, geometry, icons, parsed configs); the language's built-in interning (string pools, `Symbol.for`, constant objects). **Missing pieces:** none beyond the language: an intern map keyed by the shared state. Measure instance count and memory before and after; if the numbers are not in the millions, the map is overhead. **Ask before you build:** - **Scale:** what is the measured instance count and per-instance memory today? - **Performance:** does the intern lookup (a hash on the hot path) cost more than the memory it saves? - **Reliability:** the shared state must stay immutable - who enforces that as the code evolves? - **Budget:** engineering time on a profile-driven optimization versus the memory it saves? - **Implementation:** what is the key that proves two instances can share (equality semantics), and where is it documented? ## Implementation steps 1. Measure. Capture a heap profile and identify the class whose instances and duplicated fields dominate. 2. Split that class's fields into intrinsic (shared, never changes after creation) and extrinsic (per instance or computed from context). 3. Create the flyweight class with only intrinsic fields, make it immutable, and give it value-based equality. 4. Change methods that used extrinsic fields to take them as parameters. 5. Write the factory: a map keyed by the intrinsic value. Make it thread-safe if it is shared across threads. 6. Replace every `new` of the old class with a factory call, and measure again to confirm the saving. ## Example A spreadsheet engine loads a 200,000-row export. Every cell had its own style object, and the heap profile showed styles taking more memory than the values. Only four distinct styles exist. ```ts title="cells.ts" type NumberFormat = "plain" | "currency" | "percent"; // Flyweight: intrinsic, shared, immutable. class CellStyle { constructor( readonly font: string, readonly sizePt: number, readonly bold: boolean, readonly numberFormat: NumberFormat, ) { Object.freeze(this); } // Extrinsic state (the value) is passed in, never stored. format(value: number): string { switch (this.numberFormat) { case "currency": return `$${value.toFixed(2)}`; case "percent": return `${(value * 100).toFixed(1)}%`; default: return String(value); } } } class StyleRegistry { private readonly pool = new Map(); get(font: string, sizePt: number, bold: boolean, numberFormat: NumberFormat): CellStyle { const key = `${font}|${sizePt}|${bold}|${numberFormat}`; let style = this.pool.get(key); if (!style) { style = new CellStyle(font, sizePt, bold, numberFormat); this.pool.set(key, style); } return style; } get size(): number { return this.pool.size; } } // Context: per-cell state plus a reference to a shared style. interface Cell { row: number; col: number; value: number; style: CellStyle } const styles = new StyleRegistry(); const sheet: Cell[] = []; for (let row = 0; row < 200_000; row++) { sheet.push({ row, col: 0, value: 19.99 + row, style: styles.get("Inter", 11, false, "currency") }); sheet.push({ row, col: 1, value: 0.08, style: styles.get("Inter", 11, false, "percent") }); sheet.push({ row, col: 2, value: row % 7, style: styles.get("Inter", 11, row % 7 === 0, "plain") }); } console.log(`${sheet.length} cells share ${styles.size} style objects`); // 600000 cells share 4 const cell = sheet[3]; console.log(cell.style.format(cell.value), cell.style === sheet[0].style); // $20.99 true ``` ```python title="cells.py" from dataclasses import dataclass from typing import Literal NumberFormat = Literal["plain", "currency", "percent"] @dataclass(frozen=True, slots=True) class CellStyle: """Flyweight: frozen, hashable, shared.""" font: str size_pt: int bold: bool number_format: NumberFormat def format(self, value: float) -> str: if self.number_format == "currency": return f"${value:.2f}" if self.number_format == "percent": return f"{value:.1%}" return str(value) class StyleRegistry: def __init__(self) -> None: self._pool: dict[CellStyle, CellStyle] = {} def get(self, font: str, size_pt: int, bold: bool, number_format: NumberFormat) -> CellStyle: candidate = CellStyle(font, size_pt, bold, number_format) # The frozen dataclass is its own key; the throwaway candidate is collected right away. return self._pool.setdefault(candidate, candidate) def __len__(self) -> int: return len(self._pool) @dataclass(slots=True) class Cell: row: int col: int value: float style: CellStyle if __name__ == "__main__": styles = StyleRegistry() sheet: list[Cell] = [] for row in range(200_000): sheet.append(Cell(row, 0, 19.99 + row, styles.get("Inter", 11, False, "currency"))) sheet.append(Cell(row, 1, 0.08, styles.get("Inter", 11, False, "percent"))) sheet.append(Cell(row, 2, row % 7, styles.get("Inter", 11, row % 7 == 0, "plain"))) print(f"{len(sheet)} cells share {len(styles)} style objects") # 600000 cells share 4 cell = sheet[3] print(cell.style.format(cell.value), cell.style is sheet[0].style) # $20.99 True ``` ```go title="cells.go" package main import "fmt" type NumberFormat uint8 const ( Plain NumberFormat = iota Currency Percent ) // CellStyle is the flyweight. Its fields are never modified after creation. type CellStyle struct { Font string SizePt int Bold bool Format NumberFormat } // Render takes the extrinsic value as an argument. func (s *CellStyle) Render(value float64) string { switch s.Format { case Currency: return fmt.Sprintf("$%.2f", value) case Percent: return fmt.Sprintf("%.1f%%", value*100) default: return fmt.Sprint(value) } } // StyleRegistry uses the comparable struct itself as the map key. type StyleRegistry struct{ pool map[CellStyle]*CellStyle } func NewStyleRegistry() *StyleRegistry { return &StyleRegistry{pool: map[CellStyle]*CellStyle{}} } func (r *StyleRegistry) Get(key CellStyle) *CellStyle { if s, ok := r.pool[key]; ok { return s } s := &key // key is a copy, safe to share r.pool[key] = s return s } // Cell is the context: small extrinsic fields plus one pointer. type Cell struct { Row, Col int32 Value float64 Style *CellStyle } func main() { styles := NewStyleRegistry() sheet := make([]Cell, 0, 600_000) for row := int32(0); row < 200_000; row++ { sheet = append(sheet, Cell{row, 0, 19.99 + float64(row), styles.Get(CellStyle{"Inter", 11, false, Currency})}, Cell{row, 1, 0.08, styles.Get(CellStyle{"Inter", 11, false, Percent})}, Cell{row, 2, float64(row % 7), styles.Get(CellStyle{"Inter", 11, row%7 == 0, Plain})}, ) } fmt.Printf("%d cells share %d style objects\n", len(sheet), len(styles.pool)) // 600000 cells share 4 c := sheet[3] fmt.Println(c.Style.Render(c.Value), c.Style == sheet[0].Style) // $20.99 true } ``` ```java title="Cells.java" import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class Cells { enum NumberFormat { PLAIN, CURRENCY, PERCENT } /** Flyweight: a record is immutable and has value-based equals/hashCode. */ record CellStyle(String font, int sizePt, boolean bold, NumberFormat numberFormat) { String format(double value) { return switch (numberFormat) { case CURRENCY -> String.format("$%.2f", value); case PERCENT -> String.format("%.1f%%", value * 100); case PLAIN -> String.valueOf(value); }; } } static final class StyleRegistry { private final Map pool = new HashMap<>(); CellStyle get(String font, int sizePt, boolean bold, NumberFormat format) { CellStyle candidate = new CellStyle(font, sizePt, bold, format); return pool.computeIfAbsent(candidate, k -> k); // short-lived candidate dies young } int size() { return pool.size(); } } /** Context: extrinsic state plus a shared reference. */ record Cell(int row, int col, double value, CellStyle style) {} public static void main(String[] args) { StyleRegistry styles = new StyleRegistry(); List sheet = new ArrayList<>(600_000); for (int row = 0; row < 200_000; row++) { sheet.add(new Cell(row, 0, 19.99 + row, styles.get("Inter", 11, false, NumberFormat.CURRENCY))); sheet.add(new Cell(row, 1, 0.08, styles.get("Inter", 11, false, NumberFormat.PERCENT))); sheet.add(new Cell(row, 2, row % 7, styles.get("Inter", 11, row % 7 == 0, NumberFormat.PLAIN))); } System.out.println(sheet.size() + " cells share " + styles.size() + " style objects"); // 600000 / 4 Cell cell = sheet.get(3); System.out.println(cell.style().format(cell.value()) + " " + (cell.style() == sheet.get(0).style())); // $20.99 true } } ``` In Go or any language with value types, consider going one step further: store a small style index in each cell and keep the styles in a slice. That is how spreadsheet file formats store styles on disk. ## Trade-offs **Gains** - Memory drops from one copy per object to one copy per distinct value. - Equality checks on flyweights can use identity, which is fast. - Fewer allocations reduce garbage-collector pressure in managed runtimes. **Costs** - Immutability is mandatory. Editing a cell's style now means asking the factory for a different flyweight, not mutating a field. - Extrinsic state must be passed around, which makes method signatures longer and code harder to follow. - The factory's pool is a cache that never shrinks unless you add eviction, and it needs locking if shared across threads. - The savings are invisible in code review and easy to break by a later `new CellStyle(...)`. ## Relations - [Composite](https://aipatterns.dev/design-patterns/composite.md) trees with many identical leaves can share them as flyweights, as long as leaves hold no parent pointer. - [Singleton](https://aipatterns.dev/design-patterns/singleton.md) has exactly one instance of a class. Flyweight has one instance per distinct intrinsic value, and many of them. - [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) is the natural home for the "return an existing instance" logic. - [Proxy](https://aipatterns.dev/design-patterns/proxy.md) can make a heavy object cheap to hold by deferring it. Flyweight makes many objects cheap by sharing them. - [State](https://aipatterns.dev/design-patterns/state.md) objects with no fields are often shared as flyweights across all contexts. ## Agent checklist - [ ] A heap profile or measurement shows the duplicated state is a real memory problem. - [ ] The flyweight class is immutable and has value-based equality and hashing. - [ ] Every construction goes through the factory; a search for direct constructor calls returns only the factory. - [ ] The factory is thread-safe if used from more than one thread. - [ ] You compared against simpler options: built-in interning, an enum, or an index into a table. - [ ] Memory was measured again after the change and the saving is recorded. --- # Proxy > Put a stand-in with the same interface in front of an object to control access to it: create it lazily, check permissions, or reach it remotely. - kind: pattern - category: structural - aliases: Surrogate, Stand-in - canonical: https://aipatterns.dev/design-patterns/proxy - inspired-by: https://refactoring.guru/design-patterns/proxy ## Intent Give clients an object that implements the same interface as the real one but controls when, whether and how the real one is reached. The proxy can delay creating an expensive subject until first use, refuse calls the caller is not allowed to make, forward calls over a network, or count references. Clients cannot tell the proxy from the subject. ## Forces - **Expensive setup** vs **fast startup**: loading a 400 MB model at boot slows every deploy, even though most requests never need it. - **Callers want a plain object** vs **access needs rules**: the code that classifies messages should not also manage loading, locking or permissions. - **Location transparency** vs **honest failure modes**: a remote object that looks local hides latency and network errors. - **One access rule** vs **many call sites**: when every caller checks "is it loaded yet" or "is this user allowed", one of them eventually forgets. ## Therefore **Implement the subject's interface in a proxy class that holds (or knows how to obtain) the real subject, and put the access-control logic in the proxy before delegating each call.** The proxy usually manages the subject's lifecycle. It creates the subject, or receives a way to create it, rather than being handed a ready instance by the client. That is the practical line between a proxy and a [Decorator](https://aipatterns.dev/design-patterns/decorator.md), which the client builds by wrapping an object it already has. ## Structure ```mermaid classDiagram class InboxFilter { -SpamClassifier classifier +isSpam(sender, body) boolean } class SpamClassifier { <> +score(message) double } class ModelClassifier { -Map weights +score(message) double } class LazyClassifier { -SpamClassifier real -Loader load +score(message) double } InboxFilter --> SpamClassifier SpamClassifier <|.. ModelClassifier SpamClassifier <|.. LazyClassifier LazyClassifier --> ModelClassifier : creates on first use ``` ### Participants - **Subject** (`SpamClassifier`): the interface shared by the proxy and the real object. - **Real subject** (`ModelClassifier`): does the actual work and is expensive, remote or sensitive. - **Proxy** (`LazyClassifier`): same interface; controls access, creation and lifetime of the real subject. - **Client** (`InboxFilter`): depends on the subject interface and is unaware which one it holds. Common kinds of proxy: *virtual* (lazy creation, as in the example), *protection* (permission checks), *remote* (local stand-in for a network object, such as a gRPC stub), and *smart reference* (reference counting, locking, or releasing the subject when idle). ## Use it when - An object is expensive to create and many code paths never use it. - Access needs a check (role, tenant, quota) that should be enforced in one place for every caller. - The real object lives in another process or service and you want callers to use a local interface (RPC client stubs). - You need to manage a resource's lifetime: open a connection on first use, close it after idle time, count users. - You need to intercept calls to an object you do not own, without changing its callers. ## Do not use it when - The object is cheap to create. Construct it eagerly; lazy loading adds locking and makes the first request slow. - You only need lazy initialization of one field. Use the language's built-in: `functools.cached_property` in Python, `sync.OnceValue` in Go, a memoized `Supplier` or holder class in Java, `??=` in TypeScript. - You want to intercept every property access on a dynamic object in JavaScript. Use the built-in `Proxy` object instead of a hand-written class. In Python, `__getattr__` or descriptors cover the same ground. - The interface is wide and the proxy only acts on construction. Hand-forwarding thirty methods invites drift; use a dynamic proxy (`java.lang.reflect.Proxy`), code generation, or a lazy factory instead. - You are adding optional behavior such as caching, logging or retries that the client stacks freely. That is [Decorator](https://aipatterns.dev/design-patterns/decorator.md). - The remote call is slow or fallible, and pretending it is local hides that. Expose an explicit async client with timeouts and errors in its signature. ## Application grill **Inspect first:** the access being controlled (lazy construction, permissions, remote reach); the proxies already in the stack (HTTP clients, API gateways, service meshes, CDNs, the language's `Proxy`, lazy getters); whether the subject is truly remote or just far away in the codebase. **Missing pieces:** in-process: the language's `Proxy` or a lazy getter - nothing to buy. Remote: reuse the existing mesh, gateway or service client before writing a custom remote proxy; a custom one is a network hop you now own. **Ask before you build:** - **Scale:** does the remote proxy add a per-call network hop that the latency budget survives? - **Performance:** when is the subject actually constructed under lazy loading - does the first caller absorb the stall? - **Reliability:** when the subject is unreachable, what does the proxy return - timeout, fallback, error? - **Budget:** is configuring an existing gateway cheaper than hand-rolled proxy code? - **Implementation:** does the proxy keep the interface identical (the same test suite passes against proxy and subject)? ## Implementation steps 1. Make sure clients depend on an interface. Extract one from the real class if needed ([Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md)). 2. Write the proxy class implementing that interface. Give it what it needs to reach the subject: a loader function, an address, or a reference plus a policy. 3. In each method, apply the control (create if missing, check permission, open connection), then delegate. 4. Make the control thread-safe if the proxy is shared: use a lock, `sync.Once`, or a synchronized accessor. 5. Wire the proxy at the composition root in place of the real subject. 6. Test both paths: the proxy before the subject exists and after, and allowed and denied calls for a protection proxy. ## Example A mail service scores incoming messages with a spam model that takes several seconds and hundreds of megabytes to load. Most mail comes from trusted senders and never reaches the model, so the service should start without loading it. ```ts title="spam.ts" interface SpamClassifier { score(message: string): number; // 0..1 } // Real subject: expensive to construct. class ModelClassifier implements SpamClassifier { private readonly weights: Map; constructor(path: string) { console.log(`loading model from ${path} ...`); // stands in for seconds of I/O this.weights = new Map([["winner", 0.6], ["crypto", 0.5], ["free", 0.3]]); } score(message: string): number { const words = message.toLowerCase().split(/\W+/); return Math.min(1, words.reduce((sum, w) => sum + (this.weights.get(w) ?? 0), 0)); } } // Virtual proxy: same interface, creates the subject on first use. // Single-threaded runtime, so no lock is needed. class LazyClassifier implements SpamClassifier { private real?: SpamClassifier; constructor(private readonly load: () => SpamClassifier) {} score(message: string): number { this.real ??= this.load(); return this.real.score(message); } } class InboxFilter { constructor(private readonly classifier: SpamClassifier, private readonly trusted: Set) {} isSpam(sender: string, body: string): boolean { if (this.trusted.has(sender)) return false; // most mail stops here return this.classifier.score(body) >= 0.7; } } const filter = new InboxFilter( new LazyClassifier(() => new ModelClassifier("models/spam-v3.bin")), new Set(["billing@acme.io", "ci@acme.io"]), ); console.log("service ready"); // printed before any model load console.log(filter.isSpam("billing@acme.io", "Your invoice is ready")); // false, no load console.log(filter.isSpam("promo@deals.biz", "WINNER: claim free crypto now")); // loads once, true console.log(filter.isSpam("friend@mail.com", "Lunch tomorrow?")); // false, already loaded ``` ```python title="spam.py" import re import threading from typing import Callable, Protocol class SpamClassifier(Protocol): def score(self, message: str) -> float: ... class ModelClassifier: def __init__(self, path: str) -> None: print(f"loading model from {path} ...") # stands in for seconds of I/O self._weights = {"winner": 0.6, "crypto": 0.5, "free": 0.3} def score(self, message: str) -> float: words = re.findall(r"\w+", message.lower()) return min(1.0, sum(self._weights.get(w, 0.0) for w in words)) class LazyClassifier: """Virtual proxy. Safe to share across threads.""" def __init__(self, load: Callable[[], SpamClassifier]) -> None: self._load = load self._real: SpamClassifier | None = None self._lock = threading.Lock() def _subject(self) -> SpamClassifier: if self._real is None: with self._lock: if self._real is None: self._real = self._load() return self._real def score(self, message: str) -> float: return self._subject().score(message) class InboxFilter: def __init__(self, classifier: SpamClassifier, trusted: set[str]) -> None: self._classifier, self._trusted = classifier, trusted def is_spam(self, sender: str, body: str) -> bool: if sender in self._trusted: return False # most mail stops here return self._classifier.score(body) >= 0.7 if __name__ == "__main__": spam_filter = InboxFilter( LazyClassifier(lambda: ModelClassifier("models/spam-v3.bin")), {"billing@acme.io", "ci@acme.io"}, ) print("service ready") print(spam_filter.is_spam("billing@acme.io", "Your invoice is ready")) # False, no load print(spam_filter.is_spam("promo@deals.biz", "WINNER: claim free crypto now")) # loads once, True print(spam_filter.is_spam("friend@mail.com", "Lunch tomorrow?")) # False ``` ```go title="spam.go" package main import ( "fmt" "strings" "sync" "unicode" ) type SpamClassifier interface { Score(message string) float64 } type ModelClassifier struct{ weights map[string]float64 } func LoadModel(path string) *ModelClassifier { fmt.Printf("loading model from %s ...\n", path) // stands in for seconds of I/O return &ModelClassifier{weights: map[string]float64{"winner": 0.6, "crypto": 0.5, "free": 0.3}} } func (m *ModelClassifier) Score(message string) float64 { words := strings.FieldsFunc(strings.ToLower(message), func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) }) sum := 0.0 for _, w := range words { sum += m.weights[w] } return min(1, sum) } // LazyClassifier is a virtual proxy. sync.Once makes it safe for concurrent use. type LazyClassifier struct { once sync.Once load func() SpamClassifier real SpamClassifier } func NewLazyClassifier(load func() SpamClassifier) *LazyClassifier { return &LazyClassifier{load: load} } func (l *LazyClassifier) Score(message string) float64 { l.once.Do(func() { l.real = l.load() }) return l.real.Score(message) } type InboxFilter struct { Classifier SpamClassifier Trusted map[string]bool } func (f InboxFilter) IsSpam(sender, body string) bool { if f.Trusted[sender] { return false // most mail stops here } return f.Classifier.Score(body) >= 0.7 } func main() { filter := InboxFilter{ Classifier: NewLazyClassifier(func() SpamClassifier { return LoadModel("models/spam-v3.bin") }), Trusted: map[string]bool{"billing@acme.io": true, "ci@acme.io": true}, } fmt.Println("service ready") fmt.Println(filter.IsSpam("billing@acme.io", "Your invoice is ready")) // false, no load fmt.Println(filter.IsSpam("promo@deals.biz", "WINNER: claim free crypto now")) // loads once, true fmt.Println(filter.IsSpam("friend@mail.com", "Lunch tomorrow?")) // false } ``` ```java title="Spam.java" import java.util.Map; import java.util.Set; import java.util.function.Supplier; public class Spam { interface SpamClassifier { double score(String message); } static final class ModelClassifier implements SpamClassifier { private final Map weights; ModelClassifier(String path) { System.out.println("loading model from " + path + " ..."); // stands in for seconds of I/O weights = Map.of("winner", 0.6, "crypto", 0.5, "free", 0.3); } public double score(String message) { double sum = 0; for (String w : message.toLowerCase().split("\\W+")) sum += weights.getOrDefault(w, 0.0); return Math.min(1.0, sum); } } /** Virtual proxy: creates the subject on first use, thread-safe. */ static final class LazyClassifier implements SpamClassifier { private final Supplier loader; private SpamClassifier real; LazyClassifier(Supplier loader) { this.loader = loader; } private synchronized SpamClassifier subject() { if (real == null) real = loader.get(); return real; } public double score(String message) { return subject().score(message); } } record InboxFilter(SpamClassifier classifier, Set trusted) { boolean isSpam(String sender, String body) { if (trusted.contains(sender)) return false; // most mail stops here return classifier.score(body) >= 0.7; } } public static void main(String[] args) { InboxFilter filter = new InboxFilter( new LazyClassifier(() -> new ModelClassifier("models/spam-v3.bin")), Set.of("billing@acme.io", "ci@acme.io")); System.out.println("service ready"); System.out.println(filter.isSpam("billing@acme.io", "Your invoice is ready")); // false, no load System.out.println(filter.isSpam("promo@deals.biz", "WINNER: claim free crypto now")); // loads once, true System.out.println(filter.isSpam("friend@mail.com", "Lunch tomorrow?")); // false } } ``` The output shows `service ready` before `loading model`, and the model loads exactly once. A protection proxy has the same shape: replace the `??=` line with a permission check that throws before delegating. ## Trade-offs **Gains** - Clients get access control, laziness or remoteness without any code change. - The rule is enforced in one place, so no call site can skip it. - Startup is faster and memory is spent only when the subject is actually used. - The real subject stays focused on its job. **Costs** - The first call pays the full setup cost, which can surprise latency budgets. Warm it in the background if that matters. - Lazy creation in concurrent code needs correct synchronization. - Errors move: a broken model file now fails on the first message, not at startup. Add a readiness check if you need fail-fast. - Remote proxies that look local hide latency and partial failure. ## Relations - [Decorator](https://aipatterns.dev/design-patterns/decorator.md) has the same structure. A decorator adds behavior and is stacked by the client around an object it already has; a proxy controls access and usually owns the subject's creation and lifetime. - [Adapter](https://aipatterns.dev/design-patterns/adapter.md) gives its wrapped object a different interface. A proxy keeps the interface identical. - [Facade](https://aipatterns.dev/design-patterns/facade.md) presents a new, simpler interface over several objects. A proxy stands in for one object with the same interface. - [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md) saves memory by sharing many objects. A virtual proxy saves memory by not creating one until needed. ## Agent checklist - [ ] The subject is measurably expensive, remote, or needs access control; otherwise use it directly. - [ ] You checked for a language-native option first: `cached_property`, `sync.OnceValue`, JavaScript `Proxy`, dynamic proxies. - [ ] Clients depend on the interface, and the proxy implements all of it. - [ ] Lazy creation is thread-safe where the proxy is shared, and creation errors are surfaced, not swallowed. - [ ] Tests cover the first call (creation), later calls (reuse), and denied calls for a protection proxy. --- # Extract Method > Move a coherent fragment of a long function into its own well-named function and call it from the original place. - kind: technique - category: composing-methods - aliases: Extract Function - canonical: https://aipatterns.dev/refactoring/techniques/extract-method - inspired-by: https://refactoring.guru/extract-method ## Problem A function does several things in sequence, and you have to read every line to learn what each block is for. Comments mark the blocks ("// compute tax", "// format lines"), which is a sign the blocks want names of their own. ## Solution **Move the fragment into a new function whose name says what it does, and replace the fragment with a call.** Variables the fragment reads become parameters. A single variable it writes and the caller still needs becomes the return value. ## Before ```ts function renderInvoice(invoice: Invoice): string { let out = `Invoice ${invoice.number}\n`; out += `Customer: ${invoice.customer.name}\n\n`; // line items let subtotal = 0; for (const line of invoice.lines) { const amount = line.quantity * line.unitPrice; subtotal += amount; out += `${line.sku.padEnd(12)} ${line.quantity} x ${line.unitPrice} = ${amount}\n`; } // tax and total const tax = Math.round(subtotal * invoice.taxRate); out += `\nSubtotal: ${subtotal}\nTax: ${tax}\nTotal: ${subtotal + tax}\n`; return out; } ``` ## After ```ts function renderInvoice(invoice: Invoice): string { const subtotal = lineTotal(invoice.lines); return ( renderHeader(invoice) + renderLines(invoice.lines) + renderTotals(subtotal, invoice.taxRate) ); } function renderHeader(invoice: Invoice): string { return `Invoice ${invoice.number}\nCustomer: ${invoice.customer.name}\n\n`; } function renderLines(lines: InvoiceLine[]): string { return lines .map((l) => `${l.sku.padEnd(12)} ${l.quantity} x ${l.unitPrice} = ${l.quantity * l.unitPrice}\n`) .join(""); } function lineTotal(lines: InvoiceLine[]): number { return lines.reduce((sum, l) => sum + l.quantity * l.unitPrice, 0); } function renderTotals(subtotal: number, taxRate: number): string { const tax = Math.round(subtotal * taxRate); return `\nSubtotal: ${subtotal}\nTax: ${tax}\nTotal: ${subtotal + tax}\n`; } ``` ## Why - The top-level function now reads as a summary. Readers drill into a helper only when they need its detail. - Each helper is testable alone, and `lineTotal` can be reused by the PDF renderer and the API serializer. - Names replace comments, and names stay accurate because the compiler sees them while it ignores comments. ## Steps 1. Pick a fragment that does one thing you can name. If you cannot name it, pick a smaller or different fragment. 2. Create an empty function with that name. Name it for *what* it does, not *how*. 3. Copy the fragment into it. List every variable the fragment reads from the outer scope; make each one a parameter. 4. Find variables the fragment assigns. If exactly one is used afterward, return it. If several are, extract a smaller fragment first, or use [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md). 5. Replace the original fragment with a call. Compile and run the tests. 6. Look for other copies of the same fragment and replace them with the call too. ## Watch out - Don't extract when the name you would give is no clearer than the code itself (`addOne(x)` for `x + 1`). The indirection costs more than it saves. - A fragment that writes many locals resists extraction. Use [Replace Method with Method Object](https://aipatterns.dev/refactoring/techniques/replace-method-with-method-object.md) instead of returning a tuple of five values. - Watch for early `return`, `break` or `continue` inside the fragment. They change meaning once moved; convert them to a returned flag or value first. - In hot loops, a function call is usually free after inlining by the JIT or compiler, but measure before arguing either way. - The inverse is [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md). Use it when a helper's body is as clear as its name. ## Related - Fixes [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md), [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md), [Comments](https://aipatterns.dev/refactoring/smells/comments.md) and [Feature Envy](https://aipatterns.dev/refactoring/smells/feature-envy.md) (extract, then [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md)). - Inverse: [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md). - [Extract Variable](https://aipatterns.dev/refactoring/techniques/extract-variable.md) is the lighter option for a single expression. - [Template Method](https://aipatterns.dev/design-patterns/template-method.md) often starts as a series of extracted steps. ## Agent checklist - [ ] The new function has a name that states intent, and the fragment had no clearer name already. - [ ] Every outer variable the fragment reads is now a parameter; no hidden shared mutable state was introduced. - [ ] At most one value is returned; early exits were handled before moving. - [ ] Other duplicates of the fragment now call the new function. - [ ] Tests pass unchanged after the extraction. --- # Inline Method > Replace calls to a function whose body is as clear as its name with the body itself, then delete the function. - kind: technique - category: composing-methods - aliases: Inline Function - canonical: https://aipatterns.dev/refactoring/techniques/inline-method - inspired-by: https://refactoring.guru/inline-method ## Problem A function adds a name but no meaning. Its body is one obvious line, or it only forwards to another call. Readers jump to the definition and find nothing they could not have read in place. ## Solution **Put the body at each call site and delete the function.** Do this also as a reset: inline a tangle of badly split helpers back into one function, then extract them again along better lines. ## Before ```ts class SessionPolicy { constructor(private readonly maxIdleMinutes: number) {} isExpired(session: Session, now: Date): boolean { return this.idleMinutes(session, now) > this.limit(); } private idleMinutes(session: Session, now: Date): number { return minutesBetween(session.lastSeenAt, now); } private limit(): number { return this.maxIdleMinutes; } } ``` ## After ```ts class SessionPolicy { constructor(private readonly maxIdleMinutes: number) {} isExpired(session: Session, now: Date): boolean { return minutesBetween(session.lastSeenAt, now) > this.maxIdleMinutes; } } ``` ## Why - Fewer hops to understand the code. The condition is visible in one line. - Removes indirection left over from earlier refactorings or from speculative "maybe we'll override this" hooks. - Inlining a cluster of small, poorly named helpers is often the fastest route to re-extracting good ones. ## Steps 1. Confirm the function is not polymorphic: no subclass overrides it and no interface requires it. If one does, stop. 2. Find every call site. Include reflection, string-based dispatch, templates and tests. 3. Replace one call with the body, substituting arguments for parameters. Run the tests. 4. Repeat for each remaining call site. 5. Delete the function. Run the tests again. ## Watch out - Don't inline a method that subclasses override or that is part of a public API. Callers outside your codebase break. - Don't inline when the body is long or used in many places. You trade one name for duplicated code; that is the [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) smell. - If arguments have side effects or are expensive, substituting them into the body more than once changes behavior. Assign them to a local first. - Recursive functions and those with several `return` statements need care; turning them into inline code may require restructuring. - The inverse is [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md). Extract when the body is less clear than a name would be. ## Related - Fixes [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md), [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md) and [Lazy Class](https://aipatterns.dev/refactoring/smells/lazy-class.md) when the class shrinks to forwarding methods. - Inverse: [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md). - [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md) and [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md) apply the same idea at class level. - [Inline Temp](https://aipatterns.dev/refactoring/techniques/inline-temp.md) is the variable-level counterpart. ## Agent checklist - [ ] No override, interface contract or external caller depends on the method. - [ ] All call sites were found, including dynamic and test references. - [ ] Arguments with side effects are evaluated exactly once after inlining. - [ ] The inlined code is no harder to read than the call was. - [ ] The method is deleted and tests pass. --- # Extract Variable > Name a hard-to-read expression or sub-expression by assigning it to a well-named local variable. - kind: technique - category: composing-methods - aliases: Introduce Explaining Variable - canonical: https://aipatterns.dev/refactoring/techniques/extract-variable - inspired-by: https://refactoring.guru/extract-variable ## Problem A condition or calculation packs several ideas into one expression. You have to evaluate it in your head to learn what it checks. ## Solution **Assign each meaningful part of the expression to a local variable whose name says what that part means.** Then build the original expression from those names. ## Before ```ts function canBookRoom(user: User, room: Room, slot: Slot, now: Date): boolean { return ( (user.role === "admin" || user.teamId === room.ownerTeamId) && slot.start.getTime() - now.getTime() >= 15 * 60_000 && slot.end.getTime() - slot.start.getTime() <= room.maxBookingMinutes * 60_000 && !room.maintenanceWindows.some((w) => w.start < slot.end && slot.start < w.end) ); } ``` ## After ```ts function canBookRoom(user: User, room: Room, slot: Slot, now: Date): boolean { const hasAccess = user.role === "admin" || user.teamId === room.ownerTeamId; const leadTimeMs = slot.start.getTime() - now.getTime(); const bookedFarEnoughAhead = leadTimeMs >= 15 * 60_000; const durationMs = slot.end.getTime() - slot.start.getTime(); const withinMaxDuration = durationMs <= room.maxBookingMinutes * 60_000; const clashesWithMaintenance = room.maintenanceWindows.some( (w) => w.start < slot.end && slot.start < w.end, ); return hasAccess && bookedFarEnoughAhead && withinMaxDuration && !clashesWithMaintenance; } ``` ## Why - The return line now reads as the business rule. - Debuggers and log statements can inspect each named part. - It is a cheap first step. If a name proves useful beyond this function, promote it to a method later. ## Steps 1. Pick a sub-expression that has a clear meaning and no side effects. 2. Declare a `const` with a descriptive name and assign the sub-expression to it, placed before the first use. 3. Replace the sub-expression with the variable. Run the tests. 4. Repeat for other parts, working from the innermost meaningful pieces outward. ## Watch out - Short-circuit evaluation changes. In the original, `!room.maintenanceWindows.some(...)` never ran if access failed. Hoisting it into a variable runs it every time. That is harmless for pure checks but wrong if a part throws, is expensive, or has side effects. Keep such parts inline or extract them as functions. - If the same name would help in other functions, prefer [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) or [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md) so the logic is shared. - Don't name things that are already obvious (`const sum = a + b`). - The inverse is [Inline Temp](https://aipatterns.dev/refactoring/techniques/inline-temp.md). ## Related - Fixes [Comments](https://aipatterns.dev/refactoring/smells/comments.md) that explain an expression and part of [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md). - Inverse: [Inline Temp](https://aipatterns.dev/refactoring/techniques/inline-temp.md). - Escalate to [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) when a name is needed in more than one place. ## Agent checklist - [ ] Each extracted part has no side effects, or its evaluation order and count are unchanged. - [ ] Short-circuit guards that protected a later part (null checks, expensive calls) still protect it. - [ ] Variables are `const` and named for meaning, not for type. - [ ] Tests pass with identical results. --- # Inline Temp > Replace a temporary variable that holds a simple expression with the expression itself when the name adds nothing. - kind: technique - category: composing-methods - aliases: Inline Variable - canonical: https://aipatterns.dev/refactoring/techniques/inline-temp - inspired-by: https://refactoring.guru/inline-temp ## Problem A local variable is assigned once from a simple expression and used once or twice. The name repeats what the expression already says, or the variable blocks another refactoring such as [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md). ## Solution **Replace every reference to the variable with its expression, then delete the declaration.** ## Before ```ts function shouldThrottle(client: ApiClient, window: RateWindow): boolean { const requestCount = window.countFor(client.id); const limit = client.plan.requestsPerMinute; const over = requestCount > limit; return over; } ``` ## After ```ts function shouldThrottle(client: ApiClient, window: RateWindow): boolean { return window.countFor(client.id) > client.plan.requestsPerMinute; } ``` ## Why - Less to read when the expression is already self-describing. - Removes locals that stand in the way of extracting or moving code. - Fewer names to keep in sync during later edits. ## Steps 1. Confirm the variable is assigned exactly once. Mark it `const` and compile to prove it. 2. Confirm the right-hand side has no side effects and returns the same value each time it is read in this scope. 3. Replace the first reference with the expression. Run the tests. 4. Replace the remaining references, then delete the declaration. Run the tests. ## Watch out - Don't inline when the expression is expensive (a query, a network call, a large loop) and the variable is read several times. You would repeat the work. - Don't inline when the value can change between reads, for example `Date.now()` or a mutable field another call updates. - Don't inline when the name explains something the expression does not. That variable is doing the job of [Extract Variable](https://aipatterns.dev/refactoring/techniques/extract-variable.md), the inverse technique. - If the expression is long and used in several functions, [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md) is a better destination. ## Related - Helps with [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md) by clearing the way for extraction. - Inverse: [Extract Variable](https://aipatterns.dev/refactoring/techniques/extract-variable.md). - [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md) is the function-level counterpart. ## Agent checklist - [ ] The variable had a single assignment. - [ ] The expression is pure and cheap, or is read only once. - [ ] No reader loses information that the variable name carried. - [ ] Tests pass. --- # Replace Temp with Query > Move the expression behind a temporary variable into its own method so every method in the class can ask for the value. - kind: technique - category: composing-methods - canonical: https://aipatterns.dev/refactoring/techniques/replace-temp-with-query - inspired-by: https://refactoring.guru/replace-temp-with-query ## Problem A method computes a value into a local variable and uses it later. Other methods in the class compute the same value their own way, and the local makes the long method hard to split because every fragment depends on it. ## Solution **Extract the expression into a method (a query) with no side effects, and replace the variable with calls to it.** The value is now available anywhere in the class and does not tie fragments together. ## Before ```ts class Shipment { constructor(private parcels: Parcel[], private zone: Zone) {} label(): string { const billableKg = Math.max( this.parcels.reduce((s, p) => s + p.weightKg, 0), this.parcels.reduce((s, p) => s + p.volumeCm3, 0) / 5000, ); const fee = billableKg * this.zone.ratePerKg; return `${this.zone.code} ${billableKg.toFixed(1)}kg ${fee.toFixed(2)}`; } needsFreightCarrier(): boolean { const actualKg = this.parcels.reduce((s, p) => s + p.weightKg, 0); const volumetricKg = this.parcels.reduce((s, p) => s + p.volumeCm3, 0) / 5000; return Math.max(actualKg, volumetricKg) > 70; } } ``` ## After ```ts class Shipment { constructor(private parcels: Parcel[], private zone: Zone) {} label(): string { return `${this.zone.code} ${this.billableKg().toFixed(1)}kg ${this.fee().toFixed(2)}`; } needsFreightCarrier(): boolean { return this.billableKg() > 70; } private billableKg(): number { const actualKg = this.parcels.reduce((s, p) => s + p.weightKg, 0); const volumetricKg = this.parcels.reduce((s, p) => s + p.volumeCm3, 0) / 5000; return Math.max(actualKg, volumetricKg); } private fee(): number { return this.billableKg() * this.zone.ratePerKg; } } ``` ## Why - The billable-weight rule lives in one place. A change to the volumetric divisor touches one line. - `label` lost its locals, so further extraction is trivial. - Queries document themselves in IDE outlines and can be tested. ## Steps 1. Check the variable is assigned once. If it is assigned in several places, apply [Split Temporary Variable](https://aipatterns.dev/refactoring/techniques/split-temporary-variable.md) first. 2. Extract the right-hand side into a private method. Make sure the method does not modify state. 3. Run the tests. 4. Replace each read of the variable with a call to the new method, or apply [Inline Temp](https://aipatterns.dev/refactoring/techniques/inline-temp.md). Run the tests. 5. Replace duplicate computations elsewhere in the class with the query. ## Watch out - Repeated calls repeat the computation. That is fine for cheap expressions. For costly ones (database reads, large reductions inside a loop), keep the variable or memoize, and measure first. - Don't apply when the variable captures a snapshot that must not change, such as a timestamp taken at the start of a transaction. - The query must be side-effect free. If the expression mutates anything, separate the mutation first. - The opposite move is to keep or reintroduce a local with [Extract Variable](https://aipatterns.dev/refactoring/techniques/extract-variable.md) when the value is only meaningful in one method. ## Related - Fixes [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md) and [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md). - Usually the step before [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md). - When locals are too entangled for queries, use [Replace Method with Method Object](https://aipatterns.dev/refactoring/techniques/replace-method-with-method-object.md). ## Agent checklist - [ ] The variable was assigned once and the query has no side effects. - [ ] The value cannot change between calls within the original method, or a change is acceptable. - [ ] Performance of repeated calls was considered for non-trivial expressions. - [ ] Duplicate computations elsewhere now use the query. - [ ] Tests pass. --- # Split Temporary Variable > Give each distinct purpose its own variable when one local is reassigned to hold unrelated intermediate values. - kind: technique - category: composing-methods - aliases: Split Variable - canonical: https://aipatterns.dev/refactoring/techniques/split-temporary-variable - inspired-by: https://refactoring.guru/split-temporary-variable ## Problem One local variable is reused for several unrelated values: first a count, later a percentage, later a label. Each reader has to track which meaning is live at which line, and extracting any fragment drags the shared variable along. ## Solution **Declare a separate, well-named `const` for each purpose.** Loop accumulators (`sum += x`) and collecting variables are the exception: they hold one meaning that grows. ## Before ```ts function summarizeFunnel(events: AnalyticsEvent[]): string { let temp = events.filter((e) => e.type === "visit").length; const visits = temp; temp = events.filter((e) => e.type === "signup").length; const signupRate = visits === 0 ? 0 : temp / visits; temp = events.filter((e) => e.type === "purchase").length; const purchaseRate = visits === 0 ? 0 : temp / visits; return `visits=${visits} signup=${(signupRate * 100).toFixed(1)}% purchase=${(purchaseRate * 100).toFixed(1)}%`; } ``` ## After ```ts function summarizeFunnel(events: AnalyticsEvent[]): string { const visits = events.filter((e) => e.type === "visit").length; const signups = events.filter((e) => e.type === "signup").length; const purchases = events.filter((e) => e.type === "purchase").length; const signupRate = visits === 0 ? 0 : signups / visits; const purchaseRate = visits === 0 ? 0 : purchases / visits; return `visits=${visits} signup=${(signupRate * 100).toFixed(1)}% purchase=${(purchaseRate * 100).toFixed(1)}%`; } ``` ## Why - Each name states one meaning for its whole lifetime. - `const` lets the compiler reject accidental reassignment. - Fragments become independent, so [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) and [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md) apply cleanly. ## Steps 1. Find the first assignment of the reused variable. Rename the variable at that assignment and its reads up to the next assignment. Declare it `const`. 2. Compile and run the tests. 3. At the next assignment, declare a new `const` with a name for that purpose and update the reads that follow it. 4. Repeat until each assignment has its own variable. Delete the original declaration if nothing uses it. ## Watch out - Don't split accumulators or loop counters whose reassignments all serve one meaning (`total += line.amount`). - Check closures. A callback created earlier may capture the original variable and observe later values; splitting changes what it sees. - If the variable is a function parameter, apply [Remove Assignments to Parameters](https://aipatterns.dev/refactoring/techniques/remove-assignments-to-parameters.md) instead. - There is no useful inverse. Merging unrelated values into one variable is the smell itself. ## Related - Helps with [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md). - Prepares for [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md) and [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md). - [Remove Assignments to Parameters](https://aipatterns.dev/refactoring/techniques/remove-assignments-to-parameters.md) is the same fix for parameters. ## Agent checklist - [ ] Every reassignment served a different purpose; accumulators were left alone. - [ ] Each new variable is `const` and named for its meaning. - [ ] No closure captured the old variable and relied on later values. - [ ] Tests pass. --- # Remove Assignments to Parameters > Stop reassigning a function parameter inside its body; copy it into a local variable and work on that instead. - kind: technique - category: composing-methods - canonical: https://aipatterns.dev/refactoring/techniques/remove-assignments-to-parameters - inspired-by: https://refactoring.guru/remove-assignments-to-parameters ## Problem A function changes the value of one of its parameters partway through. Below that line the name no longer means what the caller passed in. Readers misread it, and in languages with pass-by-reference parameters (C#'s `ref`, C++ references, PHP's `&`) the caller sees the change too. ## Solution **Treat parameters as read-only. Introduce a local variable for the changing value and leave the parameter untouched.** ## Before ```ts function nextRetryDelay(attempt: number, baseMs: number, maxMs: number): number { if (attempt > 10) attempt = 10; baseMs = baseMs * 2 ** attempt; if (baseMs > maxMs) baseMs = maxMs; baseMs = baseMs + Math.floor(Math.random() * 250); return baseMs; } ``` ## After ```ts function nextRetryDelay(attempt: number, baseMs: number, maxMs: number): number { const cappedAttempt = Math.min(attempt, 10); const exponential = baseMs * 2 ** cappedAttempt; const bounded = Math.min(exponential, maxMs); const jitter = Math.floor(Math.random() * 250); return bounded + jitter; } ``` ## Why - `baseMs` means the base delay everywhere in the function, as its name promises. - Each intermediate value has a name, which makes logging and testing each step straightforward. - Code that reads the original input later (a log line, a guard) still gets the original input. ## Steps 1. Declare a local variable initialized from the parameter, named for its new meaning. 2. Replace every assignment to the parameter, and every read after the first assignment, with the local. 3. Compile and run the tests. 4. If the local itself is reassigned for different purposes, apply [Split Temporary Variable](https://aipatterns.dev/refactoring/techniques/split-temporary-variable.md). 5. Mark parameters `readonly` where the language or a linter allows it (for example ESLint `no-param-reassign`). ## Watch out - Mutating the *contents* of an object parameter (`order.status = "paid"`) is a different issue. This technique is about rebinding the name. Mutating arguments may still be a design problem, but fix it separately. - If the language passes the parameter by reference on purpose (an `out` or `ref` parameter), the reassignment is the function's contract. Change that contract only together with its callers, typically by returning the value instead. - Default-value normalization at the very top (`limit = limit ?? 50`) is a common idiom. Converting it is still clearer, but it is low priority. ## Related - Helps with [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md). - [Split Temporary Variable](https://aipatterns.dev/refactoring/techniques/split-temporary-variable.md) is the same idea for locals. - Clears the way for [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md). ## Agent checklist - [ ] No parameter is reassigned after the change. - [ ] Every read that used the modified value now reads the local. - [ ] By-reference contracts (`ref`, `out`, `&`) were preserved or changed together with callers. - [ ] Tests pass. --- # Replace Method with Method Object > Turn a long method with tangled local variables into its own class, so the locals become fields and the body can be split freely. - kind: technique - category: composing-methods - aliases: Replace Function with Command - canonical: https://aipatterns.dev/refactoring/techniques/replace-method-with-method-object - inspired-by: https://refactoring.guru/replace-method-with-method-object ## Problem A long method uses many local variables that each fragment reads and writes. [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) would need helpers with six parameters and several return values, so nobody splits it. ## Solution **Create a class for the method. Make the original object, the parameters and every local variable fields of that class, and move the body into a single `run()` method.** Fragments can now be extracted into private methods that share state through fields instead of parameters. ## Before ```ts class PayrollService { constructor(private taxBands: TaxBand[]) {} netPay(employee: Employee, period: PayPeriod): number { let gross = employee.hourlyRate * period.regularHours; let overtime = 0; if (period.overtimeHours > 0) { overtime = employee.hourlyRate * 1.5 * period.overtimeHours; gross += overtime; } let taxable = gross - employee.pensionRate * gross; let tax = 0; for (const band of this.taxBands) { const inBand = Math.max(0, Math.min(taxable, band.upTo) - band.from); tax += inBand * band.rate; } const deductions = employee.benefits.reduce((s, b) => s + b.costPerPeriod, 0); return gross - tax - employee.pensionRate * gross - deductions; } } ``` ## After ```ts class PayrollService { constructor(private taxBands: TaxBand[]) {} netPay(employee: Employee, period: PayPeriod): number { return new NetPayCalculation(this.taxBands, employee, period).run(); } } class NetPayCalculation { private gross = 0; constructor( private readonly taxBands: TaxBand[], private readonly employee: Employee, private readonly period: PayPeriod, ) {} run(): number { this.gross = this.regularPay() + this.overtimePay(); return this.gross - this.incomeTax() - this.pension() - this.benefitDeductions(); } private regularPay() { return this.employee.hourlyRate * this.period.regularHours; } private overtimePay() { return this.employee.hourlyRate * 1.5 * this.period.overtimeHours; } private pension() { return this.employee.pensionRate * this.gross; } private incomeTax(): number { const taxable = this.gross - this.pension(); return this.taxBands.reduce( (tax, b) => tax + Math.max(0, Math.min(taxable, b.upTo) - b.from) * b.rate, 0, ); } private benefitDeductions(): number { return this.employee.benefits.reduce((s, b) => s + b.costPerPeriod, 0); } } ``` ## Why - Fields replace parameter lists between helpers, so the method splits along natural seams. - The calculation is testable without the rest of `PayrollService`. - The service shrinks to an entry point; the algorithm has a home it can grow in. ## Steps 1. Create a new class named after the method's job. 2. Give it a constructor that takes the original object (if the body uses it) and every parameter. Store each as a `readonly` field. 3. Declare a field for each local variable the body uses. 4. Add a `run()` method and copy the body into it. Replace parameter and local references with fields, and calls on `this` with calls on the stored original object. 5. Replace the original method body with `return new X(...).run()`. Run the tests. 6. Now apply [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) inside the new class, one fragment at a time, running tests after each. ## Watch out - Don't use this when plain [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) with [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md) works. A new class is heavier. - The method object is single-use. Don't cache it or call `run()` twice; mutable fields make a second run unreliable. - If the original method touched private members of its class, you must expose them or pass them in. Exposing too much creates [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md). - In languages with closures, a nested function sharing locals can achieve the same split with less ceremony. - To undo, inline `run()` back into the caller and delete the class ([Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md)). ## Related - Fixes [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md) and helps with [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md). - Structurally the same as [Command](https://aipatterns.dev/design-patterns/command.md), but used for readability rather than queuing or undo. - Inverse direction: [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md). ## Agent checklist - [ ] Plain extraction was tried first and failed because of shared locals. - [ ] The new class receives everything through its constructor and exposes only `run()`. - [ ] The original method delegates in one line and its signature is unchanged. - [ ] Extracted helpers inside the new class are small and named. - [ ] Tests pass after each step. --- # Substitute Algorithm > Replace the body of a function with a clearer or simpler algorithm that produces the same results. - kind: technique - category: composing-methods - aliases: Replace Algorithm - canonical: https://aipatterns.dev/refactoring/techniques/substitute-algorithm - inspired-by: https://refactoring.guru/substitute-algorithm ## Problem A function reaches its result through a convoluted path: nested loops, flags, manual bookkeeping. A simpler approach exists, perhaps a standard library call or a well-known algorithm, but the old code stays because nobody wants to touch it. ## Solution **Write the new algorithm in a fresh body and swap it in, keeping the signature. Verify with tests that both produce identical results for the same inputs.** ## Before ```ts function mergeBusyIntervals(slots: Interval[]): Interval[] { const result: Interval[] = []; const used: boolean[] = slots.map(() => false); for (let i = 0; i < slots.length; i++) { if (used[i]) continue; let start = slots[i].start; let end = slots[i].end; let changed = true; while (changed) { changed = false; for (let j = 0; j < slots.length; j++) { if (used[j] || j === i) continue; if (slots[j].start <= end && slots[j].end >= start) { start = Math.min(start, slots[j].start); end = Math.max(end, slots[j].end); used[j] = true; changed = true; } } } used[i] = true; result.push({ start, end }); } return result.sort((a, b) => a.start - b.start); } ``` ## After ```ts function mergeBusyIntervals(slots: Interval[]): Interval[] { const sorted = [...slots].sort((a, b) => a.start - b.start); const merged: Interval[] = []; for (const slot of sorted) { const last = merged[merged.length - 1]; if (last && slot.start <= last.end) { last.end = Math.max(last.end, slot.end); } else { merged.push({ ...slot }); } } return merged; } ``` ## Why - The sort-then-sweep version is a known pattern that readers recognize in seconds. - It runs in O(n log n) instead of worst-case O(n³). - Fewer flags and indices means fewer places for off-by-one bugs. ## Steps 1. Make sure the function is as small as possible. Use [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) to isolate the algorithm from unrelated work. 2. Pin the current behavior with tests, including edge cases: empty input, one element, touching boundaries, duplicates, unsorted input. 3. Write the new algorithm next to the old one (`mergeBusyIntervalsV2`). Run both on the same inputs, by test or by property-based comparison. 4. When they agree, replace the old body with the new one. Run the tests. 5. Delete the temporary copy. ## Watch out - "Same results" includes ordering, mutation of inputs, handling of `null`, error types and rounding. The new version above copies each slot so it does not mutate the caller's array; check such details explicitly. - Don't substitute when the old algorithm encodes a business rule you do not fully understand. Characterize it with tests first. - Performance can get worse as well as better. Check large-input behavior if the function sits on a hot path. - If several algorithms are all valid and must be selectable, that is a [Strategy](https://aipatterns.dev/design-patterns/strategy.md), not a substitution. - There is no inverse; you can always substitute back. ## Related - Fixes [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) when replacing hand-rolled code with a library call, and [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md). - [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) isolates the algorithm first. - [Strategy](https://aipatterns.dev/design-patterns/strategy.md) applies when you need several algorithms at once. ## Agent checklist - [ ] Tests covering edge cases existed or were written before the swap. - [ ] Old and new implementations were compared on the same inputs. - [ ] Input mutation, output order and error behavior match. - [ ] The signature did not change. - [ ] The old implementation is deleted. --- # Pull Up Field > Move a field that every subclass declares in the same way into their shared superclass so it is defined once. - kind: technique - category: dealing-with-generalization - canonical: https://aipatterns.dev/refactoring/techniques/pull-up-field - inspired-by: https://refactoring.guru/pull-up-field ## Problem Two or more subclasses declare the same field with the same meaning, often with different names. Code that uses the field is duplicated in each subclass because the superclass does not know it exists. ## Solution **Declare the field once in the superclass and delete it from the subclasses.** ## Before ```ts abstract class PaymentMethod { constructor(readonly customerId: string) {} abstract charge(amountCents: number): Promise; } class CardPayment extends PaymentMethod { protected lastUsedAt: Date | null = null; async charge(amountCents: number): Promise { this.lastUsedAt = new Date(); return stripe.charge(this.customerId, amountCents); } } class BankDebit extends PaymentMethod { protected lastChargedOn: Date | null = null; async charge(amountCents: number): Promise { this.lastChargedOn = new Date(); return sepa.debit(this.customerId, amountCents); } } ``` ## After ```ts abstract class PaymentMethod { protected lastUsedAt: Date | null = null; constructor(readonly customerId: string) {} abstract charge(amountCents: number): Promise; } class CardPayment extends PaymentMethod { async charge(amountCents: number): Promise { this.lastUsedAt = new Date(); return stripe.charge(this.customerId, amountCents); } } class BankDebit extends PaymentMethod { async charge(amountCents: number): Promise { this.lastUsedAt = new Date(); return sepa.debit(this.customerId, amountCents); } } ``` ## Why - One declaration replaces several, with one name. - Behavior that uses the field (for example, an `isDormant()` check) can now move up with [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md). ## Steps 1. Confirm each subclass field has the same type and the same meaning, not only a similar name. 2. If the names differ, rename them in each subclass to one common name using your editor's safe rename. Run the tests. 3. Declare the field in the superclass with the narrowest visibility the subclasses need (`protected`, or `private` with accessors). Run the tests. 4. Delete the field from each subclass. Run the tests. 5. Check for serialized names (JSON, database columns) that used the old subclass field names. ## Watch out - Do not pull up a field that only some subclasses use. The others inherit dead state. Use an intermediate superclass, or leave it down. - Fields that look alike may have different invariants (one nullable, one always set). Pulling up forces a single rule. - Exposing a field as `protected` couples all subclasses to it. Prefer a `private` field with methods if subclasses only need limited access. - Persistence mappings can break when a field moves classes, especially in table-per-class ORMs. - The inverse technique is [Push Down Field](https://aipatterns.dev/refactoring/techniques/push-down-field.md). ## Related - Inverse: [Push Down Field](https://aipatterns.dev/refactoring/techniques/push-down-field.md). - Fixes [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) across subclasses. - Usually followed by [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md); part of [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md). ## Agent checklist - [ ] Every subclass declares the field with the same type and meaning. - [ ] Names were unified before the move. - [ ] Visibility is as narrow as subclasses allow. - [ ] Serialized field names and ORM mappings still match. --- # Pull Up Method > Move a method that is identical across subclasses into their superclass so the logic lives and changes in one place. - kind: technique - category: dealing-with-generalization - canonical: https://aipatterns.dev/refactoring/techniques/pull-up-method - inspired-by: https://refactoring.guru/pull-up-method ## Problem Several subclasses contain the same method, copied or written independently. A fix in one copy does not reach the others, and the copies slowly diverge. ## Solution **Move the method into the superclass and delete the subclass copies.** ## Before ```ts abstract class Report { constructor(protected rows: Row[], protected generatedAt: Date) {} abstract render(): string; } class SalesReport extends Report { render(): string { return this.header() + this.rows.map(formatSale).join("\n"); } header(): string { return `Generated ${this.generatedAt.toISOString()} | ${this.rows.length} rows\n`; } } class InventoryReport extends Report { render(): string { return this.header() + this.rows.map(formatStock).join("\n"); } header(): string { return `Generated ${this.generatedAt.toISOString()} | ${this.rows.length} rows\n`; } } ``` ## After ```ts abstract class Report { constructor(protected rows: Row[], protected generatedAt: Date) {} abstract render(): string; protected header(): string { return `Generated ${this.generatedAt.toISOString()} | ${this.rows.length} rows\n`; } } class SalesReport extends Report { render(): string { return this.header() + this.rows.map(formatSale).join("\n"); } } class InventoryReport extends Report { render(): string { return this.header() + this.rows.map(formatStock).join("\n"); } } ``` ## Why - One implementation to fix and test. - New subclasses inherit the method instead of copying it again. ## Steps 1. Compare the copies line by line. If they differ only slightly, first make them identical with small refactorings in each subclass, running the tests in between. 2. Check that every field and method the body uses exists in the superclass. If not, pull those up first with [Pull Up Field](https://aipatterns.dev/refactoring/techniques/pull-up-field.md), or declare them `abstract` in the superclass. 3. Copy the method into the superclass. The subclass copies now override it identically. Run the tests. 4. Delete one subclass copy. Run the tests. Repeat for the others. 5. If the signatures differ, unify them before moving. ## Watch out - Do not pull up a method only some subclasses need. Others inherit behavior that makes no sense for them, which leads to [Refused Bequest](https://aipatterns.dev/refactoring/smells/refused-bequest.md). - If the copies are similar but differ in a step, pull up the skeleton and leave the steps in subclasses with [Form Template Method](https://aipatterns.dev/refactoring/techniques/form-template-method.md). - If the subclasses have no real "is-a" relationship, a shared helper function or a composed object is better than a deeper base class. - The inverse technique is [Push Down Method](https://aipatterns.dev/refactoring/techniques/push-down-method.md). ## Related - Inverse: [Push Down Method](https://aipatterns.dev/refactoring/techniques/push-down-method.md). - Fixes [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md). - Preceded by [Pull Up Field](https://aipatterns.dev/refactoring/techniques/pull-up-field.md); generalized by [Form Template Method](https://aipatterns.dev/refactoring/techniques/form-template-method.md). ## Agent checklist - [ ] All copies are behaviorally identical before the move. - [ ] Every member the method uses is available in the superclass. - [ ] Each subclass that inherits the method actually needs it. - [ ] Subclass copies were deleted one at a time with tests green. --- # Pull Up Constructor Body > Move initialization code shared by subclass constructors into a superclass constructor and call it with super(). - kind: technique - category: dealing-with-generalization - canonical: https://aipatterns.dev/refactoring/techniques/pull-up-constructor-body - inspired-by: https://refactoring.guru/pull-up-constructor-body ## Problem Subclass constructors repeat the same assignments and validation for fields that belong to the superclass. Constructors cannot be inherited, so the duplication cannot be removed with [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md). ## Solution **Create (or extend) a superclass constructor with the shared code, and call it from each subclass constructor with `super(...)`.** ## Before ```ts abstract class Job { protected id!: string; protected tenantId!: string; protected attempts!: number; abstract run(): Promise; } class InvoiceEmailJob extends Job { constructor(tenantId: string, private invoiceId: string) { super(); if (!tenantId) throw new Error("tenantId required"); this.id = crypto.randomUUID(); this.tenantId = tenantId; this.attempts = 0; } async run() { await sendInvoiceEmail(this.tenantId, this.invoiceId); } } class UsageRollupJob extends Job { constructor(tenantId: string, private day: string) { super(); if (!tenantId) throw new Error("tenantId required"); this.id = crypto.randomUUID(); this.tenantId = tenantId; this.attempts = 0; } async run() { await rollupUsage(this.tenantId, this.day); } } ``` ## After ```ts abstract class Job { protected readonly id = crypto.randomUUID(); protected attempts = 0; constructor(protected readonly tenantId: string) { if (!tenantId) throw new Error("tenantId required"); } abstract run(): Promise; } class InvoiceEmailJob extends Job { constructor(tenantId: string, private invoiceId: string) { super(tenantId); } async run() { await sendInvoiceEmail(this.tenantId, this.invoiceId); } } class UsageRollupJob extends Job { constructor(tenantId: string, private day: string) { super(tenantId); } async run() { await rollupUsage(this.tenantId, this.day); } } ``` ## Why - Validation of superclass fields happens in one place, so no subclass can skip it. - The superclass owns its own invariants, and the definite-assignment `!` hacks disappear. ## Steps 1. Identify statements that are identical in every subclass constructor and touch only superclass fields. 2. Add a superclass constructor that takes the needed values, or extend the existing one. Run the tests. 3. In one subclass, pass the values to `super(...)` and delete the duplicated statements. Run the tests. Repeat for each subclass. 4. Move shared code that runs *after* subclass-specific setup into a superclass method that each subclass constructor calls at the end, if it must stay last. ## Watch out - Order matters. In TypeScript, Java and C#, `super(...)` runs before subclass field initializers. Shared code that depends on subclass fields cannot move into the superclass constructor. - Do not call overridable methods from the superclass constructor. The subclass override runs before its own fields are initialized. - If constructors are getting complicated, consider [Replace Constructor with Factory Method](https://aipatterns.dev/refactoring/techniques/replace-constructor-with-factory-method.md) to centralize creation instead. - There is no named inverse. To push initialization back down, inline the superclass constructor into each subclass. ## Related - Fixes [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) in constructors. - Usually done with [Pull Up Field](https://aipatterns.dev/refactoring/techniques/pull-up-field.md); the constructor analog of [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md). ## Agent checklist - [ ] Moved statements touch only superclass fields. - [ ] No moved statement depends on subclass state that is set later. - [ ] The superclass constructor calls no overridable methods. - [ ] Every subclass passes the same values to `super` that it used to assign. --- # Push Down Method > Move a superclass method into the only subclasses that use it, so the superclass describes only what all subclasses share. - kind: technique - category: dealing-with-generalization - canonical: https://aipatterns.dev/refactoring/techniques/push-down-method - inspired-by: https://refactoring.guru/push-down-method ## Problem A method sits in the superclass, but only one subclass (or a few) uses it. The others inherit it, and some override it to throw or do nothing. Readers of the superclass assume every subclass supports the operation. ## Solution **Move the method into the subclasses that use it and delete it from the superclass.** ## Before ```ts abstract class Account { constructor(protected balanceCents: number) {} abstract monthlyFee(): number; accrueInterest(ratePerMonth: number): void { this.balanceCents += Math.round(this.balanceCents * ratePerMonth); } } class SavingsAccount extends Account { monthlyFee() { return 0; } } class CheckingAccount extends Account { monthlyFee() { return 500; } accrueInterest(): void { throw new Error("Checking accounts do not earn interest"); } } ``` ## After ```ts abstract class Account { constructor(protected balanceCents: number) {} abstract monthlyFee(): number; } class SavingsAccount extends Account { monthlyFee() { return 0; } accrueInterest(ratePerMonth: number): void { this.balanceCents += Math.round(this.balanceCents * ratePerMonth); } } class CheckingAccount extends Account { monthlyFee() { return 500; } } ``` ## Why - The superclass now promises only what every account can do. - Calling `accrueInterest` on a checking account becomes a compile error instead of a runtime exception. - Subclasses no longer need defensive overrides. ## Steps 1. Find all callers of the method. Note which concrete types they call it on. 2. Copy the method into each subclass that needs it. Run the tests. 3. Delete the method from the superclass. The compiler shows every caller that used it through the superclass type. 4. Fix those callers. Either change the variable's type to the subclass, or check the type first. If many callers need it generically, stop and reconsider. Run the tests. 5. Delete the throwing or empty overrides in subclasses that never needed the method. ## Watch out - Do not push down a method that callers invoke polymorphically through the superclass type. You would replace a virtual call with type checks scattered across callers. - If several subclasses would get identical copies, push it into an intermediate subclass instead, or keep it up. - If the method will be needed by most new subclasses, leaving it up with a sensible default may be simpler. - The inverse technique is [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md). ## Related - Inverse: [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md). - Fixes [Refused Bequest](https://aipatterns.dev/refactoring/smells/refused-bequest.md) and [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md). - Often paired with [Push Down Field](https://aipatterns.dev/refactoring/techniques/push-down-field.md) and [Extract Subclass](https://aipatterns.dev/refactoring/techniques/extract-subclass.md). ## Agent checklist - [ ] Only some subclasses use the method; others ignore, stub or throw. - [ ] No caller relies on calling it through the superclass type, or those callers were updated. - [ ] No duplicated copies were created across many subclasses. - [ ] Throwing or empty overrides were removed. --- # Push Down Field > Move a superclass field into the subclasses that use it, so other subclasses stop carrying state they never touch. - kind: technique - category: dealing-with-generalization - canonical: https://aipatterns.dev/refactoring/techniques/push-down-field - inspired-by: https://refactoring.guru/push-down-field ## Problem A field is declared in the superclass, but only one subclass reads or writes it. The other subclasses carry it around as `null` or a default, and readers cannot tell which subclasses it applies to. ## Solution **Declare the field in the subclasses that use it and remove it from the superclass.** ## Before ```ts abstract class Shipment { trackingNumber: string | null = null; pickupLocationId: string | null = null; // only used for click-and-collect constructor(readonly orderId: string) {} } class CourierShipment extends Shipment { dispatch(courier: Courier) { this.trackingNumber = courier.book(this.orderId); } } class StorePickup extends Shipment { assign(locationId: string) { this.pickupLocationId = locationId; } } ``` ## After ```ts abstract class Shipment { constructor(readonly orderId: string) {} } class CourierShipment extends Shipment { trackingNumber: string | null = null; dispatch(courier: Courier) { this.trackingNumber = courier.book(this.orderId); } } class StorePickup extends Shipment { pickupLocationId: string | null = null; assign(locationId: string) { this.pickupLocationId = locationId; } } ``` ## Why - Each class declares only the state it owns, which documents its purpose. - Couriers no longer carry a pickup location that is always `null`. - Code that reads the field must now know the subclass, which makes the dependency visible. ## Steps 1. Find every read and write of the field. Confirm they all happen on a specific subclass. 2. Declare the field in each subclass that uses it. Run the tests. 3. Remove the field from the superclass. Fix compiler errors where callers reach it through the superclass type. Run the tests. 4. Check serialization, database mappings and API responses that may emit the field for every subclass. ## Watch out - Do not push down a field that generic code reads through the superclass, such as a reporting job that lists every shipment's tracking number. You would add type checks there. - Single-table ORM mappings may still require the column on the base entity. Moving the field in code may need a mapping change too. - Removing a field from an API response that always included it (even as `null`) can break clients. - The inverse technique is [Pull Up Field](https://aipatterns.dev/refactoring/techniques/pull-up-field.md). ## Related - Inverse: [Pull Up Field](https://aipatterns.dev/refactoring/techniques/pull-up-field.md). - Fixes [Refused Bequest](https://aipatterns.dev/refactoring/smells/refused-bequest.md) and helps with [Temporary Field](https://aipatterns.dev/refactoring/smells/temporary-field.md). - Usually done alongside [Push Down Method](https://aipatterns.dev/refactoring/techniques/push-down-method.md). ## Agent checklist - [ ] Every access to the field is made on a specific subclass. - [ ] No generic code reads it through the superclass type. - [ ] ORM mappings and serialized output were checked. - [ ] Each subclass that receives the field actually uses it. --- # Extract Subclass > Move features used only by some instances of a class into a new subclass, replacing the flag or nullable fields that marked them. - kind: technique - category: dealing-with-generalization - canonical: https://aipatterns.dev/refactoring/techniques/extract-subclass - inspired-by: https://refactoring.guru/extract-subclass ## Problem A class has fields and methods that only apply to some of its instances. A flag or a nullable field marks those instances, and methods check that flag before doing the special work. ## Solution **Create a subclass for the special instances, and move the fields and methods that only they use into it.** Construct the subclass wherever the flag used to be set. ## Before ```ts class Employee { constructor( readonly name: string, readonly baseSalaryCents: number, readonly isContractor: boolean, readonly hourlyRateCents: number | null, ) {} monthlyCost(hoursWorked: number): number { if (this.isContractor) return (this.hourlyRateCents ?? 0) * hoursWorked; return this.baseSalaryCents / 12; } holidayAllowanceDays(): number { return this.isContractor ? 0 : 25; } } ``` ## After ```ts class Employee { constructor(readonly name: string, readonly baseSalaryCents: number) {} monthlyCost(_hoursWorked: number): number { return this.baseSalaryCents / 12; } holidayAllowanceDays(): number { return 25; } } class Contractor extends Employee { constructor(name: string, readonly hourlyRateCents: number) { super(name, 0); } monthlyCost(hoursWorked: number): number { return this.hourlyRateCents * hoursWorked; } holidayAllowanceDays(): number { return 0; } } ``` ## Why - The flag and its checks disappear; each class has straight-line logic. - `hourlyRateCents` is no longer nullable, because it exists only where it is set. - New contractor-only behavior has an obvious home. ## Steps 1. Create the empty subclass. Run the tests. 2. Add a constructor or factory that creates the subclass, and change the places that set the flag to use it. Keep the flag working for now. Run the tests. 3. Move fields used only by the special case into the subclass with [Push Down Field](https://aipatterns.dev/refactoring/techniques/push-down-field.md). Run the tests. 4. For each method that branches on the flag, override it in the subclass with the special branch, and remove that branch from the superclass. Run the tests after each method. 5. Delete the flag when no code reads it. ## Watch out - Do not use a subclass if an instance can switch categories during its life (an employee becoming a contractor). Objects cannot change class. Use [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md) or composition. - Do not use it if the variation is along more than one axis (contractor and remote and part-time). Subclasses multiply. Extract a component with [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) instead. - In the example, `Contractor` still inherits `baseSalaryCents`. If many such leftovers appear, the hierarchy is wrong; a shared superclass of two siblings may fit better. - The inverse is [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md). ## Related - Inverse: [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md). - Fixes [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md), [Temporary Field](https://aipatterns.dev/refactoring/smells/temporary-field.md) and flag-based [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md). - Alternatives: [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) for composition, [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md) when a type code drives the split. ## Agent checklist - [ ] The special case is fixed at construction and never changes. - [ ] Variation is along a single axis. - [ ] Each flag branch moved to an override with tests green in between. - [ ] The flag and its now-dead branches were deleted. --- # Extract Superclass > Create a shared superclass for two classes with overlapping fields and methods, and pull the common parts up into it. - kind: technique - category: dealing-with-generalization - canonical: https://aipatterns.dev/refactoring/techniques/extract-superclass - inspired-by: https://refactoring.guru/extract-superclass ## Problem Two unrelated classes have the same fields and some of the same methods. They were built separately, so the shared logic is duplicated and may already have drifted apart. ## Solution **Create a new superclass, make both classes extend it, and move the shared fields and methods up.** ## Before ```ts class WebhookEndpoint { constructor(readonly url: string, readonly secret: string) {} private failures = 0; recordFailure(): void { this.failures++; } isDisabled(): boolean { return this.failures >= 5; } deliver(event: DomainEvent) { return http.post(this.url, sign(event, this.secret)); } } class SlackIntegration { constructor(readonly webhookUrl: string, readonly channel: string) {} private errorCount = 0; recordFailure(): void { this.errorCount++; } isDisabled(): boolean { return this.errorCount >= 5; } deliver(event: DomainEvent) { return http.post(this.webhookUrl, { channel: this.channel, text: summarize(event) }); } } ``` ## After ```ts abstract class Subscriber { private failures = 0; recordFailure(): void { this.failures++; } isDisabled(): boolean { return this.failures >= 5; } abstract deliver(event: DomainEvent): Promise; } class WebhookEndpoint extends Subscriber { constructor(readonly url: string, readonly secret: string) { super(); } deliver(event: DomainEvent) { return http.post(this.url, sign(event, this.secret)); } } class SlackIntegration extends Subscriber { constructor(readonly webhookUrl: string, readonly channel: string) { super(); } deliver(event: DomainEvent) { return http.post(this.webhookUrl, { channel: this.channel, text: summarize(event) }); } } ``` ## Why - Failure tracking exists once, so the disable threshold cannot differ by accident. - Code that dispatches events can work with `Subscriber` and treat both kinds the same. - The shared concept gets a name. ## Steps 1. Create an empty abstract superclass and make both classes extend it. Run the tests. 2. Unify names of matching fields and methods in both classes (`errorCount` becomes `failures`). Run the tests. 3. Move common fields up with [Pull Up Field](https://aipatterns.dev/refactoring/techniques/pull-up-field.md). Run the tests. 4. Move identical methods up with [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md). Declare methods with the same signature but different bodies as `abstract` in the superclass. Run the tests. 5. Change clients that can now depend on the superclass instead of either concrete class. ## Watch out - Do not extract a superclass when either class already extends something else in a single-inheritance language. Use [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) and composition, or [Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md) for the shared contract. - Do not create a superclass for classes that share code by coincidence but not a concept. A shared helper function is lighter and couples less. - Shared state in a base class couples subclasses tightly. If the shared part is a self-contained concern (like failure tracking), a composed `FailureCounter` object may age better than inheritance. - The inverse is [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md). ## Related - Inverse: [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md). - Fixes [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) and [Alternative Classes with Different Interfaces](https://aipatterns.dev/refactoring/smells/alternative-classes-with-different-interfaces.md). - Alternatives: [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) (composition), [Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md) (contract only). ## Agent checklist - [ ] Both classes represent the same concept, not only similar code. - [ ] Neither class already needs a different base class. - [ ] Names were unified before members were pulled up. - [ ] Composition was considered for self-contained shared concerns. --- # Extract Interface > Declare the subset of a class's methods that clients actually use as an interface, so clients depend on the contract instead of the class. - kind: technique - category: dealing-with-generalization - canonical: https://aipatterns.dev/refactoring/techniques/extract-interface - inspired-by: https://refactoring.guru/extract-interface ## Problem Several clients use only part of a class, or several classes offer the same operations and clients should not care which one they get. Clients still name the concrete class, so they cannot be given a fake in tests or a different implementation in production. ## Solution **Declare an interface with exactly the methods the clients use, make the class implement it, and change the clients to depend on the interface.** ## Before ```ts class S3Storage { constructor(private client: S3Client, private bucket: string) {} async put(key: string, body: Buffer): Promise { /* ... */ } async get(key: string): Promise { /* ... */ } async presign(key: string, ttlSeconds: number): Promise { /* ... */ } async listVersions(key: string): Promise { /* ... */ } } class InvoiceArchive { constructor(private storage: S3Storage) {} async store(invoice: Invoice, pdf: Buffer): Promise { const key = `invoices/${invoice.tenantId}/${invoice.number}.pdf`; await this.storage.put(key, pdf); return this.storage.presign(key, 3600); } } ``` ## After ```ts interface BlobStore { put(key: string, body: Buffer): Promise; presign(key: string, ttlSeconds: number): Promise; } class S3Storage implements BlobStore { constructor(private client: S3Client, private bucket: string) {} async put(key: string, body: Buffer): Promise { /* ... */ } async get(key: string): Promise { /* ... */ } async presign(key: string, ttlSeconds: number): Promise { /* ... */ } async listVersions(key: string): Promise { /* ... */ } } class InvoiceArchive { constructor(private storage: BlobStore) {} async store(invoice: Invoice, pdf: Buffer): Promise { const key = `invoices/${invoice.tenantId}/${invoice.number}.pdf`; await this.storage.put(key, pdf); return this.storage.presign(key, 3600); } } ``` ## Why - `InvoiceArchive` can be tested with an in-memory `BlobStore` and no AWS SDK. - Swapping S3 for another store touches only the composition root. - The interface documents which capabilities the client relies on. ## Steps 1. List the methods the client actually calls on the class. 2. Declare an interface containing only those methods, with identical signatures. Run the compiler. 3. Mark the class as implementing the interface. Run the compiler and tests. 4. Change the client's field, parameter or variable type from the class to the interface. Run the tests. 5. Repeat for other clients. Create separate small interfaces if clients use different subsets. ## Watch out - Do not extract an interface with a single implementation and no test double or second implementation in sight. It adds a file and an indirection. In TypeScript, structural typing lets tests pass any object of the right shape without a declared interface. - Do not copy the whole public surface into the interface. A wide interface forces fakes and new implementations to stub methods no one uses. - Interfaces carry no code. If implementations share behavior, combine with [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md) or composition. - There is no named inverse. Inline the interface (replace it with the class) when it has one implementation and no test value. ## Related - Fixes [Alternative Classes with Different Interfaces](https://aipatterns.dev/refactoring/smells/alternative-classes-with-different-interfaces.md) and loosens [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md). - Compare with [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md), which shares implementation as well. - An extracted interface is often the seam for a [Strategy](https://aipatterns.dev/design-patterns/strategy.md). ## Agent checklist - [ ] The interface contains only methods a client actually calls. - [ ] There is a second implementation, a test double, or a module boundary that justifies it. - [ ] Clients now reference the interface type, not the class. - [ ] Construction of the concrete class happens in one place. --- # Collapse Hierarchy > Merge a subclass into its superclass (or the reverse) when the two have become nearly identical and the split no longer earns its cost. - kind: technique - category: dealing-with-generalization - canonical: https://aipatterns.dev/refactoring/techniques/collapse-hierarchy - inspired-by: https://refactoring.guru/collapse-hierarchy ## Problem A subclass adds almost nothing to its superclass. Perhaps features were moved away over time, or the subclass was created for a variation that never arrived. Readers have to open two files to understand one concept. ## Solution **Move any remaining members into one of the two classes, redirect all references to it, and delete the other.** ## Before ```ts class Notifier { constructor(protected transport: Transport) {} async notify(userId: string, message: string): Promise { const user = await users.find(userId); if (user.mutedUntil && user.mutedUntil > new Date()) return; await this.transport.send(user.contact, message); } } // Created for a planned "priority" feature that never shipped class DefaultNotifier extends Notifier { constructor(transport: Transport) { super(transport); } } const notifier: Notifier = new DefaultNotifier(smtpTransport); ``` ## After ```ts class Notifier { constructor(private transport: Transport) {} async notify(userId: string, message: string): Promise { const user = await users.find(userId); if (user.mutedUntil && user.mutedUntil > new Date()) return; await this.transport.send(user.contact, message); } } const notifier = new Notifier(smtpTransport); ``` ## Why - One class to read, with no question about which one to extend or instantiate. - `protected` members can become `private` once no subclass exists. ## Steps 1. Choose which class to keep. Usually keep the one whose name fits the concept and has more references. 2. Move the remaining members of the removed class into the kept one with [Pull Up Field](https://aipatterns.dev/refactoring/techniques/pull-up-field.md), [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md), or their push-down counterparts. Run the tests. 3. Replace every reference to the removed class (constructors, type annotations, `instanceof`, DI bindings) with the kept class. Run the tests. 4. Delete the empty class. Tighten visibility of members that were `protected` only for the subclass. Run the tests. ## Watch out - Do not collapse a hierarchy that other subclasses still extend, or that is part of a public extension point for library users. - Check `instanceof` checks and serialized type discriminators (`"type": "DefaultNotifier"` in stored JSON or queues). Old data may still reference the removed class. - If the subclass carries real behavior for some instances, collapsing reintroduces flags and branches. That is the case for [Extract Subclass](https://aipatterns.dev/refactoring/techniques/extract-subclass.md). - The inverse techniques are [Extract Subclass](https://aipatterns.dev/refactoring/techniques/extract-subclass.md) and [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md). ## Related - Inverse: [Extract Subclass](https://aipatterns.dev/refactoring/techniques/extract-subclass.md), [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md). - Fixes [Lazy Class](https://aipatterns.dev/refactoring/smells/lazy-class.md), [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md), and helps untangle [Parallel Inheritance Hierarchies](https://aipatterns.dev/refactoring/smells/parallel-inheritance-hierarchies.md). - The inheritance analog of [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md). ## Agent checklist - [ ] The removed class adds no meaningful behavior or state. - [ ] No other code, plugin or public API extends the removed class. - [ ] All references, including `instanceof`, DI bindings and stored type names, were redirected. - [ ] Visibility was tightened after the collapse. --- # Form Template Method > When subclass methods follow the same sequence of steps with different details, pull the sequence into the superclass and leave only the steps in subclasses. - kind: technique - category: dealing-with-generalization - canonical: https://aipatterns.dev/refactoring/techniques/form-template-method - inspired-by: https://refactoring.guru/form-template-method ## Problem Two or more subclasses have methods that do the same steps in the same order, but some steps differ in detail. You cannot pull the method up as-is, so the overall algorithm is duplicated and can drift. ## Solution **Split each method into steps with identical signatures, pull the step sequence up into a superclass method, and leave each subclass to override only the steps that differ.** ## Before ```ts class CsvImporter extends Importer { async run(file: Buffer): Promise { const rows = parseCsv(file.toString("utf8")); const valid = rows.filter((r) => r.sku && Number(r.qty) >= 0); await db.inventory.upsertMany(valid.map((r) => ({ sku: r.sku, qty: Number(r.qty) }))); return { imported: valid.length, rejected: rows.length - valid.length }; } } class JsonImporter extends Importer { async run(file: Buffer): Promise { const rows: { sku: string; quantity: number }[] = JSON.parse(file.toString("utf8")); const valid = rows.filter((r) => r.sku && r.quantity >= 0); await db.inventory.upsertMany(valid.map((r) => ({ sku: r.sku, qty: r.quantity }))); return { imported: valid.length, rejected: rows.length - valid.length }; } } ``` ## After ```ts type StockRow = { sku: string; qty: number }; abstract class Importer { async run(file: Buffer): Promise { const rows = this.parse(file.toString("utf8")); const valid = rows.filter((r) => r.sku && r.qty >= 0); await db.inventory.upsertMany(valid); return { imported: valid.length, rejected: rows.length - valid.length }; } protected abstract parse(text: string): StockRow[]; } class CsvImporter extends Importer { protected parse(text: string): StockRow[] { return parseCsv(text).map((r) => ({ sku: r.sku, qty: Number(r.qty) })); } } class JsonImporter extends Importer { protected parse(text: string): StockRow[] { const rows: { sku: string; quantity: number }[] = JSON.parse(text); return rows.map((r) => ({ sku: r.sku, qty: r.quantity })); } } ``` ## Why - Validation, persistence and the summary exist once. - A new format needs only a `parse` method. - The algorithm's order is fixed in one place, so subclasses cannot skip validation. ## Steps 1. In each subclass, extract the differing parts into methods with [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md). Give matching steps the same name and signature in every subclass. Run the tests. 2. Normalize the data passed between steps so all subclasses use the same types (here, `StockRow`). Run the tests. 3. The main methods should now be identical. Pull one up with [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md). Run the tests. 4. Declare the varying steps `abstract` in the superclass. Delete the duplicate main methods from subclasses. Run the tests. ## Watch out - Do not use inheritance when the steps could be passed in as functions or objects. A function `parse` parameter, or a [Strategy](https://aipatterns.dev/design-patterns/strategy.md), keeps the importer composable and avoids a hierarchy. Prefer that in languages with first-class functions. - Do not form a template from two methods that only look similar today. The skeleton becomes a constraint that later variants fight with hooks and flags. - Keep the number of overridable steps small. Many hooks make the flow hard to follow. - The inverse is to inline the template back into subclasses. To move from inheritance to composition, use [Replace Inheritance with Delegation](https://aipatterns.dev/refactoring/techniques/replace-inheritance-with-delegation.md). ## Related - Fixes [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) in near-identical subclass methods. - Produces the [Template Method](https://aipatterns.dev/design-patterns/template-method.md) pattern; [Strategy](https://aipatterns.dev/design-patterns/strategy.md) is the composition alternative. - Built from [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) and [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md). ## Agent checklist - [ ] The subclass methods share the same step order, differing only in step details. - [ ] Varying steps have identical names and signatures before the pull-up. - [ ] Function or strategy parameters were considered as an alternative to inheritance. - [ ] The superclass exposes only a few abstract steps. --- # Replace Inheritance with Delegation > Turn a subclass that uses only part of its superclass into a class that holds an instance of it and forwards just the calls it needs. - kind: technique - category: dealing-with-generalization - aliases: Replace Superclass with Delegate, Replace Subclass with Delegate - canonical: https://aipatterns.dev/refactoring/techniques/replace-inheritance-with-delegation - inspired-by: https://refactoring.guru/replace-inheritance-with-delegation ## Problem A class extends another to reuse some of its code, but it is not truly a kind of that class. It inherits methods that make no sense for it, and callers can use them to break its rules. ## Solution **Give the class a field holding an instance of the former superclass, forward the calls you actually need to it, and remove the `extends`.** ## Before ```ts class AuditLog extends Array { record(actorId: string, action: string, target: string): void { this.push({ actorId, action, target, at: new Date() }); } byActor(actorId: string): AuditEntry[] { return this.filter((e) => e.actorId === actorId); } } const log = new AuditLog(); log.record("u-7", "role.grant", "u-12"); log.splice(0, 1); // compiles: entries can be erased from an append-only log ``` ## After ```ts class AuditLog { private readonly entries: AuditEntry[] = []; record(actorId: string, action: string, target: string): void { this.entries.push({ actorId, action, target, at: new Date() }); } byActor(actorId: string): AuditEntry[] { return this.entries.filter((e) => e.actorId === actorId); } get size(): number { return this.entries.length; } } const log = new AuditLog(); log.record("u-7", "role.grant", "u-12"); // log.splice(...) no longer exists ``` ## Why - The class exposes only operations that keep its invariants: append-only stays append-only. - Changes to the former superclass no longer leak into the public API. - You can swap the delegate for another implementation, such as a database-backed store. ## Steps 1. Add a field of the superclass type to the subclass, initialized to `this`. Run the tests. 2. Change each method in the subclass that uses inherited members to call them through the field. Run the tests. 3. List the inherited methods callers actually use. Add a forwarding method for each one that should stay public. 4. Remove the `extends` clause and initialize the field with a new instance of the former superclass. Run the compiler and tests. 5. Fix callers that used the class as the superclass type (for example, passed it where an array was expected). Give them an explicit accessor or an interface. ## Watch out - Do not use delegation when the subclass truly is a specialization and uses most of the superclass interface. You will write many trivial forwarding methods. - Forwarding breaks `instanceof` checks and code that passes the object where the superclass type is expected. - Calls that the superclass made to overridden methods (self-calls) no longer reach the former subclass. Check any template-method style hooks. - The inverse technique is [Replace Delegation with Inheritance](https://aipatterns.dev/refactoring/techniques/replace-delegation-with-inheritance.md). ## Related - Inverse: [Replace Delegation with Inheritance](https://aipatterns.dev/refactoring/techniques/replace-delegation-with-inheritance.md). - Fixes [Refused Bequest](https://aipatterns.dev/refactoring/smells/refused-bequest.md) and reduces [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md) with the superclass's internals. - Often combined with [Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md). The resulting structure underlies [Decorator](https://aipatterns.dev/design-patterns/decorator.md) and [Strategy](https://aipatterns.dev/design-patterns/strategy.md). ## Agent checklist - [ ] The class uses only a small part of the superclass interface, or inherited methods break its rules. - [ ] Only methods that callers need are forwarded. - [ ] Callers relying on the superclass type or `instanceof` were updated. - [ ] Former self-calls from the superclass into overrides were checked. --- # Replace Delegation with Inheritance > When a class forwards nearly every call to one delegate it truly is a kind of, make it a subclass of that delegate and delete the forwarding. - kind: technique - category: dealing-with-generalization - canonical: https://aipatterns.dev/refactoring/techniques/replace-delegation-with-inheritance - inspired-by: https://refactoring.guru/replace-delegation-with-inheritance ## Problem A class holds an instance of another class and forwards almost every method to it unchanged. Each new method on the delegate needs another forwarding method. The class is really a specialization of the delegate, but it is written as a wrapper. ## Solution **Make the class extend the delegate's class, remove the delegate field, and delete the forwarding methods.** Keep only the methods that add or change behavior. ## Before ```ts class HttpClient { constructor(protected baseUrl: string) {} get(path: string): Promise { /* ... */ } post(path: string, body: unknown): Promise { /* ... */ } put(path: string, body: unknown): Promise { /* ... */ } delete(path: string): Promise { /* ... */ } } class BillingApiClient { private http = new HttpClient("https://billing.internal/v2"); get(path: string) { return this.http.get(path); } post(path: string, body: unknown) { return this.http.post(path, body); } put(path: string, body: unknown) { return this.http.put(path, body); } delete(path: string) { return this.http.delete(path); } invoice(id: string) { return this.http.get(`/invoices/${id}`); } } ``` ## After ```ts class HttpClient { constructor(protected baseUrl: string) {} get(path: string): Promise { /* ... */ } post(path: string, body: unknown): Promise { /* ... */ } put(path: string, body: unknown): Promise { /* ... */ } delete(path: string): Promise { /* ... */ } } class BillingApiClient extends HttpClient { constructor() { super("https://billing.internal/v2"); } invoice(id: string) { return this.get(`/invoices/${id}`); } } ``` ## Why - The forwarding boilerplate disappears, along with the work of keeping it in sync. - New methods on the base class are available automatically. - The class shrinks to what makes it different. ## Steps 1. Confirm the class uses the whole delegate interface and that nothing else holds a reference to the same delegate instance. 2. Make the class extend the delegate's class. If a forwarding method has the same name, it now overrides the inherited one and still works. Run the tests. 3. Initialize the superclass through `super(...)` with what the delegate was constructed with. Point the delegate field at `this`. Run the tests. 4. Delete forwarding methods one at a time; the inherited versions take over. Run the tests after each. 5. Replace remaining uses of the field with `this`, then delete the field. Run the tests. ## Watch out - Do not do this if the class uses only part of the delegate. Inheritance exposes the whole interface, which is the [Refused Bequest](https://aipatterns.dev/refactoring/smells/refused-bequest.md) problem the inverse technique exists to fix. - Do not do this when the delegate is shared with other objects or swapped at runtime. A superclass cannot be shared or replaced. - If the class already extends something else, single-inheritance languages block this. Consider [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md) so clients call the delegate directly. - Inheritance couples you to the base class's protected internals and future changes. In the example, a `BillingApiClient` now publicly exposes generic `delete(path)`; decide whether that is acceptable. - The inverse technique is [Replace Inheritance with Delegation](https://aipatterns.dev/refactoring/techniques/replace-inheritance-with-delegation.md). ## Related - Inverse: [Replace Inheritance with Delegation](https://aipatterns.dev/refactoring/techniques/replace-inheritance-with-delegation.md). - Fixes [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md). - Alternative: [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md). ## Agent checklist - [ ] The class forwards nearly all of the delegate's public methods unchanged. - [ ] The delegate is not shared with other objects or replaced at runtime. - [ ] The class does not already need a different superclass. - [ ] Exposing the full inherited interface is acceptable for callers. --- # Move Method > Move a method to the class whose data it uses most, and leave a delegating call or update the callers. - kind: technique - category: moving-features-between-objects - aliases: Move Function - canonical: https://aipatterns.dev/refactoring/techniques/move-method - inspired-by: https://refactoring.guru/move-method ## Problem A method lives in one class but reads mostly another class's fields. Every change to that other class's data ripples back to it, and the class it lives in has to know far more than it should. ## Solution **Move the method to the class whose data it uses, passing in whatever it still needs from the old class.** Then either delete the original or keep it as a one-line delegate. ## Before ```ts class BillingService { constructor(private mailer: Mailer) {} lateFee(invoice: Invoice, today: Date): number { if (invoice.paidAt) return 0; const daysLate = daysBetween(invoice.dueDate, today); if (daysLate <= invoice.terms.graceDays) return 0; const fee = invoice.balance * invoice.terms.dailyLateRate * daysLate; return Math.min(fee, invoice.terms.maxLateFee); } sendReminder(invoice: Invoice, today: Date): void { const fee = this.lateFee(invoice, today); this.mailer.send(invoice.customerEmail, `Balance ${invoice.balance}, late fee ${fee}`); } } ``` ## After ```ts class Invoice { // ...existing fields: paidAt, dueDate, balance, terms, customerEmail lateFee(today: Date): number { if (this.paidAt) return 0; const daysLate = daysBetween(this.dueDate, today); if (daysLate <= this.terms.graceDays) return 0; const fee = this.balance * this.terms.dailyLateRate * daysLate; return Math.min(fee, this.terms.maxLateFee); } } class BillingService { constructor(private mailer: Mailer) {} sendReminder(invoice: Invoice, today: Date): void { const fee = invoice.lateFee(today); this.mailer.send(invoice.customerEmail, `Balance ${invoice.balance}, late fee ${fee}`); } } ``` ## Why - The fee rule sits next to the data it depends on. Changing `terms` and the fee logic happens in one class. - `BillingService` stops reaching into invoice internals, which reduces coupling. - Other callers (the dashboard, the export job) can ask the invoice directly. ## Steps 1. Look at everything the method uses: fields, other methods, parameters. Decide whether related members should move with it. 2. Check subclasses and superclasses of the source for overrides or declarations. Polymorphic methods need extra care. 3. Declare the method in the target class. Copy the body and rewrite references: target fields become `this.x`; source members it still needs become parameters (or pass the source object). 4. Compile the target. Make the original method delegate to the new one. Run the tests. 5. Replace callers one by one to call the target directly, or keep the delegate if many callers exist. Run the tests. 6. Delete the delegate when it has no callers. ## Watch out - Don't move a method that uses the source class's data as much as the target's. Split it with [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) first and move only the envious part. - Moving behavior onto a plain data type (a DTO, a generated API model, a database row type) can be impossible or wrong. Those types may be regenerated or shared across services. - Watch for overrides. If subclasses override the method, all variants must move together or you break dispatch. - There is no dedicated inverse; you move it back the same way. If a whole class empties out, consider [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md). ## Related - Fixes [Feature Envy](https://aipatterns.dev/refactoring/smells/feature-envy.md), [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md), [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md), [Data Class](https://aipatterns.dev/refactoring/smells/data-class.md) and [Message Chains](https://aipatterns.dev/refactoring/smells/message-chains.md). - Often paired with [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md). - [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) is a series of Move Field and Move Method steps. ## Agent checklist - [ ] The method uses more of the target's data than the source's. - [ ] Overrides and interface declarations in the source hierarchy were checked. - [ ] Source members still needed are passed as parameters, not reached through new back-references. - [ ] All callers updated or delegating; the delegate is removed when unused. - [ ] Tests pass after each step. --- # Move Field > Move a field to the class that uses it most, updating every reader and writer to reach it there. - kind: technique - category: moving-features-between-objects - canonical: https://aipatterns.dev/refactoring/techniques/move-field - inspired-by: https://refactoring.guru/move-field ## Problem A field lives in one class but is read and written mostly by another. Every change to how it is used touches both classes, and the owning class carries data it does not care about. ## Solution **Create the field in the target class, route all access through it, and remove it from the source.** ## Before ```ts class Tenant { constructor( public id: string, public name: string, public monthlyApiQuota: number, // only the billing plan cares public plan: BillingPlan, ) {} } class BillingPlan { constructor(public tier: "free" | "pro" | "enterprise", public pricePerMonth: number) {} } function remainingQuota(tenant: Tenant, used: number): number { return tenant.monthlyApiQuota - used; } function upgrade(tenant: Tenant): void { tenant.plan = new BillingPlan("pro", 49); tenant.monthlyApiQuota = 100_000; } ``` ## After ```ts class Tenant { constructor(public id: string, public name: string, public plan: BillingPlan) {} } class BillingPlan { constructor( public tier: "free" | "pro" | "enterprise", public pricePerMonth: number, public monthlyApiQuota: number, ) {} } function remainingQuota(tenant: Tenant, used: number): number { return tenant.plan.monthlyApiQuota - used; } function upgrade(tenant: Tenant): void { tenant.plan = new BillingPlan("pro", 49, 100_000); } ``` ## Why - Quota changes with the plan, so it belongs to the plan. Upgrading can no longer leave a stale quota behind. - `Tenant` describes the tenant; `BillingPlan` describes what they pay for. - Methods that use the field can now follow it with [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md). ## Steps 1. If the field is public, apply [Encapsulate Field](https://aipatterns.dev/refactoring/techniques/encapsulate-field.md) first so all access goes through a getter and setter. 2. Create the field (and its accessors) in the target class. 3. Decide how the source reaches the target: an existing reference, a new field, or a parameter. 4. Change the source accessors to read and write the target's field. Run the tests. 5. Update callers to use the target directly. Run the tests. 6. Remove the field and accessors from the source. ## Watch out - Persistence mapping: moving a field between classes often means moving a database column or a JSON key. Plan the data migration and keep old payloads readable. - If several source objects share one target, the moved field becomes shared. Make sure that is the intended meaning. - Don't move a field that the source's own invariants depend on. Move the methods that enforce those invariants too, or leave it. - No inverse exists; move it back the same way. If the target becomes a thin holder, consider [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md). ## Related - Fixes [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md), [Feature Envy](https://aipatterns.dev/refactoring/smells/feature-envy.md) and [Temporary Field](https://aipatterns.dev/refactoring/smells/temporary-field.md). - Pairs with [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md); together they make up [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md). - [Self Encapsulate Field](https://aipatterns.dev/refactoring/techniques/self-encapsulate-field.md) makes internal access easier to redirect. ## Agent checklist - [ ] All reads and writes of the field went through accessors before moving. - [ ] The source has a reliable path to the target object. - [ ] Persisted data, serialization and API shapes were migrated or kept compatible. - [ ] The source no longer declares the field. - [ ] Tests pass. --- # Extract Class > Split a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to. - kind: technique - category: moving-features-between-objects - canonical: https://aipatterns.dev/refactoring/techniques/extract-class - inspired-by: https://refactoring.guru/extract-class ## Problem One class has grown two responsibilities. A subset of its fields and methods change together and are used together, while the rest of the class ignores them. The class is long and changes for unrelated reasons. ## Solution **Create a new class for the cohesive subset, give the original a field that holds an instance, and move the fields and methods over one at a time.** ## Before ```ts class User { constructor( public id: string, public email: string, public displayName: string, private passwordHash: string, private failedLogins: number, private lockedUntil: Date | null, ) {} rename(name: string) { this.displayName = name; } recordFailedLogin(now: Date): void { this.failedLogins += 1; if (this.failedLogins >= 5) this.lockedUntil = addMinutes(now, 15); } isLocked(now: Date): boolean { return this.lockedUntil !== null && now < this.lockedUntil; } resetLoginAttempts(): void { this.failedLogins = 0; this.lockedUntil = null; } } ``` ## After ```ts class User { constructor( public id: string, public email: string, public displayName: string, private passwordHash: string, readonly lockout: LoginLockout, ) {} rename(name: string) { this.displayName = name; } } class LoginLockout { constructor(private failedLogins = 0, private lockedUntil: Date | null = null) {} recordFailure(now: Date): void { this.failedLogins += 1; if (this.failedLogins >= 5) this.lockedUntil = addMinutes(now, 15); } isLocked(now: Date): boolean { return this.lockedUntil !== null && now < this.lockedUntil; } reset(): void { this.failedLogins = 0; this.lockedUntil = null; } } ``` ## Why - Lockout policy changes (thresholds, backoff) no longer touch the profile class. - `LoginLockout` is testable without building a full user. - The same lockout object can later protect API keys or one-time codes. ## Steps 1. Name the responsibility you are splitting off. If the name is vague ("UserHelper"), reconsider the split. 2. Create the new class, empty. Add a field in the original that holds an instance, created in the constructor. 3. Move one field with [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md). Run the tests. 4. Move methods with [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md), starting with those that only use moved fields. Run the tests after each. 5. Review both interfaces. Rename methods that now read oddly (`recordFailedLogin` became `recordFailure`). 6. Decide whether to expose the new object (`user.lockout`) or keep delegating methods on the original. ## Watch out - Don't extract when the subset is two fields and one method with no prospect of growth. The new class adds indirection without hiding anything. - Aliasing: if you expose the new object, callers can mutate it directly. Expose it read-only, or keep delegating methods, if the original must control changes. - Persistence often stores the object as one row or document. Map the extracted class as an embedded value rather than a new table unless you need it. - The inverse is [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md). ## Related - Fixes [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md), [Divergent Change](https://aipatterns.dev/refactoring/smells/divergent-change.md), [Data Clumps](https://aipatterns.dev/refactoring/smells/data-clumps.md) and [Temporary Field](https://aipatterns.dev/refactoring/smells/temporary-field.md). - Inverse: [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md). - Built from [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) and [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md). - [Replace Data Value with Object](https://aipatterns.dev/refactoring/techniques/replace-data-value-with-object.md) is the special case for one field. ## Agent checklist - [ ] The extracted class has a clear, specific name and a single reason to change. - [ ] Fields moved first, then methods, with tests green after each move. - [ ] The original class's public API is either preserved via delegation or callers were updated. - [ ] Mutability of the exposed new object was decided deliberately. - [ ] Persistence and serialization still produce the same shape, or a migration exists. --- # Inline Class > Fold a class that does too little into the class that uses it, moving its fields and methods across and deleting it. - kind: technique - category: moving-features-between-objects - canonical: https://aipatterns.dev/refactoring/techniques/inline-class - inspired-by: https://refactoring.guru/inline-class ## Problem A class holds one or two fields and a method or two, and only one other class uses it. It may be a leftover from an earlier split or a placeholder for growth that never came. Every reader has to open a second file to find almost nothing. ## Solution **Move all of the small class's features into its single user, then delete it.** ## Before ```ts class ReportSchedule { constructor(private cron: CronSpec, private timezone: string) {} nextRunAfter(now: Date): Date { return this.cron.next(now, this.timezone); } } class ScheduledReport { constructor( public id: string, public query: string, private schedule: ReportSchedule, ) {} nextRunAfter(now: Date): Date { return this.schedule.nextRunAfter(now); } } ``` ## After ```ts class ScheduledReport { constructor( public id: string, public query: string, private cron: CronSpec, private timezone: string, ) {} nextRunAfter(now: Date): Date { return this.cron.next(now, this.timezone); } } ``` ## Why - One less type and file to navigate. The schedule logic sits where it is used. - Removes a delegating method that added no behavior. - Inlining two poorly divided classes can also be the first step toward re-splitting them along a better line. ## Steps 1. Confirm the class has one user, or that every user can be redirected. Check for uses in DI configuration, serialization and tests. 2. In the absorbing class, create public members for each public member of the small class. Make them delegate. Run the tests. 3. Redirect external callers of the small class to the absorbing class. Run the tests. 4. Move fields and methods across with [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) and [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md), one at a time. 5. Delete the small class. Run the tests. ## Watch out - Don't inline a class that several unrelated classes use, or that models a real domain concept with invariants (a `Money`, an `EmailAddress`). Small is not the same as useless. - Don't inline when the class sits on a module or package boundary that exists for a reason, such as dependency direction or a published API. - Inlining can bloat the receiver. If it is already a [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md), look for a better home. - The inverse is [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md). ## Related - Fixes [Lazy Class](https://aipatterns.dev/refactoring/smells/lazy-class.md) and [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md). - Inverse: [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md). - [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md) is the method-level counterpart; [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md) handles classes that only delegate. ## Agent checklist - [ ] The class has no independent domain meaning or invariants worth naming. - [ ] Every user, including configuration, serializers and tests, was found and redirected. - [ ] Features moved one at a time with tests green after each. - [ ] The absorbing class did not become a Large Class. - [ ] The inlined class is deleted. --- # Hide Delegate > Add a method on the server object that forwards to its delegate, so clients stop navigating through it to reach the delegate. - kind: technique - category: moving-features-between-objects - canonical: https://aipatterns.dev/refactoring/techniques/hide-delegate - inspired-by: https://refactoring.guru/hide-delegate ## Problem Clients call `a.getB().getC().doThing()`. They depend on the structure between `a`, `b` and `c`, so restructuring any link breaks every chain in the codebase. ## Solution **Give the first object a method that does the navigation internally, and have clients call that method instead.** The client then knows one object, not the path behind it. ## Before ```ts // In several controllers and jobs: const approverEmail = order.getAccount().getBillingContact().email; const region = order.getAccount().getBillingContact().address.region; class Order { constructor(private account: Account) {} getAccount(): Account { return this.account; } } ``` ## After ```ts const approverEmail = order.billingContactEmail(); const region = order.billingRegion(); class Order { constructor(private account: Account) {} billingContactEmail(): string { return this.account.getBillingContact().email; } billingRegion(): string { return this.account.getBillingContact().address.region; } } ``` ## Why - If billing contacts move from `Account` to a separate `BillingProfile`, only `Order` changes. - Clients express intent (`billingRegion()`) instead of navigation. - Fewer types appear in client imports, so tests need fewer mocks. ## Steps 1. Find a chain clients repeat. Pick the object they start from as the server. 2. Add a method on the server that performs the navigation and returns the needed value or performs the action. 3. Replace one client chain with the new method. Run the tests. 4. Replace the remaining chains. 5. If no client still needs the delegate itself, remove or narrow the server's accessor (`getAccount()`). ## Watch out - Don't hide every delegate. Each forwarding method enlarges the server. When most of the server's methods are forwarders, it has become a [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md). - Chains through plain data structures or fluent builders (`query.where(...).orderBy(...)`) are not this smell. Leave them. - Forwarding methods should stay thin. If logic accumulates in them, it may belong in the delegate. - The inverse is [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md). Use it when forwarding has taken over. ## Related - Fixes [Message Chains](https://aipatterns.dev/refactoring/smells/message-chains.md) and [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md). - Inverse: [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md). - [Facade](https://aipatterns.dev/design-patterns/facade.md) applies the same idea to a whole subsystem. ## Agent checklist - [ ] The chain appears in more than one client, or its structure is likely to change. - [ ] The new method is named for what the client wants, not the path it takes. - [ ] All occurrences of the chain were replaced. - [ ] The server did not turn into a class of mostly forwarders. - [ ] Tests pass. --- # Remove Middle Man > Let clients call the delegate directly when a class does little more than forward calls to it. - kind: technique - category: moving-features-between-objects - canonical: https://aipatterns.dev/refactoring/techniques/remove-middle-man - inspired-by: https://refactoring.guru/remove-middle-man ## Problem A class has grown a forwarding method for every method of its delegate. Each new feature on the delegate requires a matching pass-through, and the class adds nothing but a hop. ## Solution **Expose the delegate and have clients call it directly. Delete the forwarding methods.** ## Before ```ts class Warehouse { constructor(private stock: StockLedger) {} quantityOf(sku: string) { return this.stock.quantityOf(sku); } reserve(sku: string, qty: number) { return this.stock.reserve(sku, qty); } release(sku: string, qty: number) { return this.stock.release(sku, qty); } receive(sku: string, qty: number) { return this.stock.receive(sku, qty); } adjust(sku: string, delta: number, reason: string) { return this.stock.adjust(sku, delta, reason); } } // client warehouse.reserve("SKU-1182", 3); ``` ## After ```ts class Warehouse { constructor(readonly stock: StockLedger) {} } // client warehouse.stock.reserve("SKU-1182", 3); ``` ## Why - Adding a ledger operation no longer needs a twin method on `Warehouse`. - Readers see which object actually does the work. - Less code to test and keep in sync. ## Steps 1. Add an accessor (or a `readonly` field) that exposes the delegate. 2. For one forwarding method, replace each client call with a direct call on the delegate. Run the tests. 3. Delete that forwarding method. Run the tests. 4. Repeat for the remaining forwarders. 5. If the server is now empty of behavior, consider [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md). ## Watch out - Keep forwarders that add something: access control, logging, validation, transactions, or translation between models. Those are a [Proxy](https://aipatterns.dev/design-patterns/proxy.md) or [Facade](https://aipatterns.dev/design-patterns/facade.md), not a middle man. - Keep the hop if the delegate is an implementation detail you may swap, or if exposing it would let clients bypass invariants the server enforces. - Removing forwarders from a published API is a breaking change. Deprecate first. - The inverse is [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md). Balance the two: hide what is likely to change, expose what is stable. ## Related - Fixes [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md). - Inverse: [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md). - [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md) removes single forwarders; [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md) removes an empty server. ## Agent checklist - [ ] The forwarders add no logic, checks or translation. - [ ] Exposing the delegate does not let clients break the server's invariants. - [ ] All clients of each forwarder were redirected before deleting it. - [ ] Public API removals were deprecated or versioned. - [ ] Tests pass. --- # Introduce Foreign Method > Add a helper in your own code that takes an instance of a class you cannot change and provides the method it lacks. - kind: technique - category: moving-features-between-objects - canonical: https://aipatterns.dev/refactoring/techniques/introduce-foreign-method - inspired-by: https://refactoring.guru/introduce-foreign-method ## Problem You need an operation on a library or framework class, such as `Date` or a vendor SDK type, but you cannot add methods to it. The same few lines of workaround are copied into several places. ## Solution **Write one function in your code that takes the foreign object as its first parameter and does the work.** Name it as if it were a method on that class, and mark it as a stand-in for a missing method. ## Before ```ts // in the scheduler const start = new Date(meeting.start); while (start.getDay() === 0 || start.getDay() === 6) { start.setDate(start.getDate() + 1); } // in invoicing const due = new Date(invoice.issuedAt); due.setDate(due.getDate() + 30); while (due.getDay() === 0 || due.getDay() === 6) { due.setDate(due.getDate() + 1); } ``` ## After ```ts // dates.ts - foreign method: belongs on Date export function nextBusinessDay(date: Date): Date { const result = new Date(date); while (result.getDay() === 0 || result.getDay() === 6) { result.setDate(result.getDate() + 1); } return result; } // in the scheduler const start = nextBusinessDay(new Date(meeting.start)); // in invoicing const due = new Date(invoice.issuedAt); due.setDate(due.getDate() + 30); const dueOnBusinessDay = nextBusinessDay(due); ``` ## Why - The rule exists once. A holiday calendar later changes one function. - Call sites read as intent. - Returning a new `Date` removes the mutation hazard of shared instances. ## Steps 1. Identify the repeated workaround and the foreign class it operates on. 2. Create a function in a shared module of your own. Its first parameter is the foreign object; other inputs follow. 3. Move the workaround body into it. Avoid mutating the argument; return a new value where you can. 4. Replace one call site. Run the tests. Replace the rest. 5. Add a short comment that it is a foreign method, so it can move into the class if you ever own it. ## Watch out - In TypeScript, don't patch `Date.prototype` or other built-ins. It leaks to every module and to third-party code. A plain function is safer. - If you find yourself writing many foreign methods for the same class, collect them with [Introduce Local Extension](https://aipatterns.dev/refactoring/techniques/introduce-local-extension.md). - Languages with extension methods (C#, Kotlin, Swift) let you write the same helper with method-call syntax. That is the idiomatic form there, not a different refactoring. - If you own the class after all, add the method to it with [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) instead. - No inverse; inline the helper back with [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md) if it proves pointless. ## Related - Fixes [Incomplete Library Class](https://aipatterns.dev/refactoring/smells/incomplete-library-class.md) and [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md). - Escalate to [Introduce Local Extension](https://aipatterns.dev/refactoring/techniques/introduce-local-extension.md) when helpers multiply. ## Agent checklist - [ ] The class really cannot be modified (third-party, built-in or generated). - [ ] The helper takes the foreign object as its first parameter and does not mutate it. - [ ] All duplicate workarounds now call the helper. - [ ] No global prototype was patched. - [ ] Tests pass. --- # Introduce Local Extension > Wrap or subclass a class you cannot change in your own class that adds the methods you need. - kind: technique - category: moving-features-between-objects - canonical: https://aipatterns.dev/refactoring/techniques/introduce-local-extension - inspired-by: https://refactoring.guru/introduce-local-extension ## Problem A vendor or framework class lacks several operations you need. You have a growing pile of loose helper functions for it, scattered across modules, each taking the foreign object as a parameter. ## Solution **Create a class of your own that extends or wraps the foreign class, and put the missing methods there.** Use a subclass when the foreign class allows it and its instances are created by you; use a wrapper when instances come from elsewhere or the class is final. ## Before ```ts // helpers scattered across the codebase function isRefundable(charge: Stripe.Charge, now: Date): boolean { return !charge.refunded && now.getTime() / 1000 - charge.created < 180 * 86_400; } function amountInMajorUnits(charge: Stripe.Charge): number { return charge.amount / 100; } function customerId(charge: Stripe.Charge): string | null { return typeof charge.customer === "string" ? charge.customer : charge.customer?.id ?? null; } ``` ## After ```ts // charge.ts - local extension of a vendor type we do not control export class Charge { constructor(readonly raw: Stripe.Charge) {} get id() { return this.raw.id; } isRefundable(now: Date): boolean { return !this.raw.refunded && now.getTime() / 1000 - this.raw.created < 180 * 86_400; } amountInMajorUnits(): number { return this.raw.amount / 100; } customerId(): string | null { const c = this.raw.customer; return typeof c === "string" ? c : c?.id ?? null; } } // at the boundary where charges arrive const charge = new Charge(await stripe.charges.retrieve(id)); ``` ## Why - All behavior for the vendor type has one home and one test file. - Callers discover operations through autocomplete instead of grepping for helpers. - The wrapper isolates you from vendor shape changes. A field rename in the SDK touches one class. ## Steps 1. Choose subclass or wrapper. Prefer a wrapper for data you receive from SDKs, APIs or ORMs, since you do not control their construction. 2. Create the extension class. For a wrapper, store the original as a field and add accessors for what callers need. 3. Move each existing foreign method into the class with [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md). Run the tests after each. 4. Convert at the boundary: wrap foreign objects where they enter your code. 5. Update callers to use the extension's methods. Delete the loose helpers. ## Watch out - Wrappers break identity: `new Charge(x) !== new Charge(x)`, and code that expects the vendor type will not accept the wrapper. Keep `raw` available for those calls. - Don't forward every method of a large vendor class. Expose what you use; the rest stays on `raw`. - Subclassing a class you don't control couples you to its internals, and some SDKs return plain objects, so a subclass never gets instantiated. - One or two helpers don't justify a class. Use [Introduce Foreign Method](https://aipatterns.dev/refactoring/techniques/introduce-foreign-method.md). - No direct inverse; to undo, turn methods back into functions and delete the wrapper. ## Related - Fixes [Incomplete Library Class](https://aipatterns.dev/refactoring/smells/incomplete-library-class.md) and [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md). - Grows out of [Introduce Foreign Method](https://aipatterns.dev/refactoring/techniques/introduce-foreign-method.md). - A wrapper that adds behavior while keeping the interface is a [Decorator](https://aipatterns.dev/design-patterns/decorator.md); one that changes the interface is an [Adapter](https://aipatterns.dev/design-patterns/adapter.md). ## Agent checklist - [ ] Subclass vs wrapper was chosen based on who constructs the instances. - [ ] Foreign objects are wrapped once, at the boundary, not throughout the code. - [ ] The extension exposes only members callers actually use, plus the raw object. - [ ] Loose helper functions were moved in and deleted. - [ ] Tests pass. --- # Self Encapsulate Field > Access a field through its own getter and setter even inside the class, so subclasses or later changes can intercept the access. - kind: technique - category: organizing-data - aliases: Self-Encapsulate Variable - canonical: https://aipatterns.dev/refactoring/techniques/self-encapsulate-field - inspired-by: https://refactoring.guru/self-encapsulate-field ## Problem Methods in a class read and write a field directly. Now you need to add logic whenever that field is accessed: lazy loading, a derived value in a subclass, change tracking, or a move to another object. Every direct access is a place you must edit. ## Solution **Create a private (or protected) getter and setter for the field and route every access inside the class through them.** Later changes then happen in one place. ## Before ```ts class Subscription { private seats: number; constructor(seats: number) { this.seats = seats; } monthlyCost(pricePerSeat: number): number { return this.seats * pricePerSeat; } addSeats(n: number): void { this.seats = this.seats + n; } canInvite(currentMembers: number): boolean { return currentMembers < this.seats; } } ``` ## After ```ts class Subscription { private _seats!: number; // assigned through the setter below constructor(seats: number) { this.seats = seats; } protected get seats(): number { return this._seats; } protected set seats(value: number) { if (!Number.isInteger(value) || value < 1) throw new RangeError(`invalid seat count ${value}`); this._seats = value; } monthlyCost(pricePerSeat: number): number { return this.seats * pricePerSeat; } addSeats(n: number): void { this.seats = this.seats + n; } canInvite(currentMembers: number): boolean { return currentMembers < this.seats; } } class TrialSubscription extends Subscription { protected override get seats(): number { return Math.min(super.seats, 3); // trials cap seats regardless of purchase } } ``` ## Why - Validation now runs on every write, including the constructor. - A subclass changes how seats are computed by overriding one accessor. - Moving the field later ([Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md)) means editing two accessors, not every method. ## Steps 1. Rename the field (for example to `_seats`) so direct uses stand out. 2. Add a getter and setter with the original name. Keep their visibility as narrow as the use requires. 3. Replace each direct read and write inside the class with the accessors. Run the tests. 4. Decide whether the constructor should use the setter. Use it if the setter validates; avoid it if a subclass override could run before the subclass is initialized. ## Watch out - Don't do this for every field by default. Direct access inside a small class is clearer. Apply it when you need the hook now. - Overridable accessors called from a constructor can run subclass code against a half-built object. That is a real bug source in Java, C# and TypeScript. - TypeScript accessors that override in a subclass need the base declared as an accessor too; a plain field cannot be overridden by an accessor. - Inverse: remove the accessors and use the field directly when the hook is no longer needed. ## Related - Helps with [Data Class](https://aipatterns.dev/refactoring/smells/data-class.md) by giving behavior a place to attach. - [Encapsulate Field](https://aipatterns.dev/refactoring/techniques/encapsulate-field.md) is the external counterpart for public fields. - Prepares for [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) and [Replace Data Value with Object](https://aipatterns.dev/refactoring/techniques/replace-data-value-with-object.md). ## Agent checklist - [ ] There is a concrete need for interception (validation, override, lazy load, move). - [ ] Every in-class access now goes through the accessors. - [ ] Constructor use of overridable accessors was checked for initialization-order bugs. - [ ] Accessor visibility is no wider than needed. - [ ] Tests pass. --- # Replace Data Value with Object > Turn a primitive field that has grown rules and companions into a small class that owns its data and behavior. - kind: technique - category: organizing-data - aliases: Replace Primitive with Object - canonical: https://aipatterns.dev/refactoring/techniques/replace-data-value-with-object - inspired-by: https://refactoring.guru/replace-data-value-with-object ## Problem A field started as a `string` or `number`, but now it has formatting rules, validation and related data. The logic for it is copied across the codebase, and nothing stops an invalid value from being stored. ## Solution **Create a class for the value, move its data and rules into it, and change the field's type to that class.** Make the class immutable so it behaves like the primitive it replaces. ## Before ```ts class Contact { constructor(public name: string, public phone: string) {} } function smsTarget(c: Contact): string { const digits = c.phone.replace(/[^\d+]/g, ""); if (!/^\+\d{8,15}$/.test(digits)) throw new Error(`bad phone ${c.phone}`); return digits; } function displayPhone(c: Contact): string { const digits = c.phone.replace(/[^\d+]/g, ""); return digits.startsWith("+1") ? `(${digits.slice(2, 5)}) ${digits.slice(5, 8)}-${digits.slice(8)}` : digits; } ``` ## After ```ts class PhoneNumber { private constructor(readonly e164: string) {} static parse(input: string): PhoneNumber { const digits = input.replace(/[^\d+]/g, ""); if (!/^\+\d{8,15}$/.test(digits)) throw new Error(`bad phone ${input}`); return new PhoneNumber(digits); } display(): string { const d = this.e164; return d.startsWith("+1") ? `(${d.slice(2, 5)}) ${d.slice(5, 8)}-${d.slice(8)}` : d; } equals(other: PhoneNumber): boolean { return this.e164 === other.e164; } } class Contact { constructor(public name: string, public phone: PhoneNumber) {} } const smsTarget = (c: Contact) => c.phone.e164; const displayPhone = (c: Contact) => c.phone.display(); ``` ## Why - Invalid phone numbers cannot exist past the parse step. - Normalization happens once, not at every use. - The type documents intent; a function taking `PhoneNumber` cannot be handed an email by mistake. ## Steps 1. Create the class with one immutable field holding the current primitive. Add a constructor or factory. 2. Change the field's type in the owner. Wrap values where they are assigned; unwrap where they are read (`.e164`). Run the tests. 3. Move duplicated logic (validation, formatting, comparison) into the new class one function at a time. Run the tests after each. 4. Add `equals` and, if needed, `toString` or `toJSON` so comparisons and serialization behave as before. 5. Push the new type outward to parameters and return values where it helps. ## Watch out - Behavior change: the old code accepted bad phone numbers until they were used. Validating at construction can reject data that loads today. Check stored data, or add a lenient path for legacy records. - Equality: `===` on two objects compares identity, not value. Replace every `a.phone === b.phone` with `equals`. - Serialization: `JSON.stringify` now emits `{"e164":"..."}` instead of a string unless you add `toJSON`. Check APIs and caches. - Don't wrap a primitive that has no rules and no companions. A type alias or branded type may be enough in TypeScript. - Inverse: inline the class back into a primitive field ([Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md)). ## Related - Fixes [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md) and [Data Clumps](https://aipatterns.dev/refactoring/smells/data-clumps.md). - A special case of [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md). - Follow with [Change Value to Reference](https://aipatterns.dev/refactoring/techniques/change-value-to-reference.md) if instances must be shared. ## Agent checklist - [ ] The new type is immutable and has value equality. - [ ] All identity comparisons on the old field were replaced. - [ ] Serialization output is unchanged or the change is intended. - [ ] Existing persisted values pass the new validation, or a migration path exists. - [ ] Tests pass. --- # Change Value to Reference > Replace many identical copies of an entity with one shared instance looked up from a registry, so updates are seen everywhere. - kind: technique - category: organizing-data - canonical: https://aipatterns.dev/refactoring/techniques/change-value-to-reference - inspired-by: https://refactoring.guru/change-value-to-reference ## Problem Each order holds its own copy of a `Customer` object built from the same data. When one copy changes (a new credit limit, a renamed company), the others stay stale. The data is really one entity, but the code treats it as many values. ## Solution **Keep one instance per identity in a repository or registry, and have every owner hold a reference to that instance instead of creating its own.** ## Before ```ts class Customer { constructor(readonly id: string, public creditLimit: number) {} } class Order { readonly customer: Customer; constructor(customerId: string, creditLimit: number, public total: number) { this.customer = new Customer(customerId, creditLimit); } } const a = new Order("C-77", 5_000, 1_200); const b = new Order("C-77", 5_000, 3_900); a.customer.creditLimit = 10_000; // b still sees 5_000 ``` ## After ```ts class Customer { constructor(readonly id: string, public creditLimit: number) {} } class CustomerRegistry { private byId = new Map(); get(id: string, creditLimit: number): Customer { let c = this.byId.get(id); if (!c) { c = new Customer(id, creditLimit); this.byId.set(id, c); } return c; } } class Order { constructor(readonly customer: Customer, public total: number) {} } const customers = new CustomerRegistry(); const a = new Order(customers.get("C-77", 5_000), 1_200); const b = new Order(customers.get("C-77", 5_000), 3_900); a.customer.creditLimit = 10_000; // b sees 10_000 ``` ## Why - One update reaches every holder. No sync code, no stale copies. - Less memory when many objects share the same entity. - Identity checks (`a.customer === b.customer`) now mean "same customer". ## Steps 1. Decide where shared instances live: an existing repository, a unit-of-work, or a new registry. Avoid a global singleton if a scoped object will do. 2. Add a lookup that returns the existing instance for an id or creates it once. 3. Change the owner's constructor to accept a `Customer` instead of building one. Update callers to fetch it from the registry. Run the tests. 4. Remove the old creation path. ## Watch out - Shared mutable state means any holder can change the entity for everyone. Make sure that is the intended semantics. - The registry's lifetime matters. A process-wide cache in a server leaks data between requests and tenants; scope it to a request or transaction. - In distributed systems, a reference is often an id plus a fetch, not a pointer. Don't hold stale shared objects across network boundaries. - If the object never changes, keep it as a value; sharing gives no benefit. - The inverse is [Change Reference to Value](https://aipatterns.dev/refactoring/techniques/change-reference-to-value.md). ## Related - Fixes [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) in the form of duplicated entity data and the [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md) of keeping copies in sync. - Inverse: [Change Reference to Value](https://aipatterns.dev/refactoring/techniques/change-reference-to-value.md). - The registry is often a [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md); sharing immutable instances is [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md). ## Agent checklist - [ ] The object is an entity with identity and changes that all holders must see. - [ ] The registry's scope (request, transaction, process) matches the data's ownership. - [ ] Owners receive instances instead of constructing them. - [ ] No caller still builds its own copy. - [ ] Tests pass, including one that shows an update is visible through every holder. --- # Change Reference to Value > Make a small shared object immutable and compare it by content, so it can be copied freely instead of tracked by identity. - kind: technique - category: organizing-data - canonical: https://aipatterns.dev/refactoring/techniques/change-reference-to-value - inspired-by: https://refactoring.guru/change-reference-to-value ## Problem A small object, such as a currency, a date range or a geo point, is shared by reference and mutated in place. Mutating it through one holder silently changes it for every other holder, and code has to defensively copy it or look it up in a registry. ## Solution **Make the object immutable, give it value equality, and replace in-place mutation with methods that return a new instance.** ## Before ```ts class DateRange { constructor(public start: Date, public end: Date) {} extendBy(days: number): void { this.end = new Date(this.end.getTime() + days * 86_400_000); } } const promoWindow = new DateRange(new Date("2026-11-01"), new Date("2026-11-30")); const blackFriday = { name: "BF", window: promoWindow }; const cyberMonday = { name: "CM", window: promoWindow }; cyberMonday.window.extendBy(2); // blackFriday's window also moved ``` ## After ```ts class DateRange { constructor(readonly start: Date, readonly end: Date) { Object.freeze(this); } extendBy(days: number): DateRange { return new DateRange(this.start, new Date(this.end.getTime() + days * 86_400_000)); } equals(other: DateRange): boolean { return ( this.start.getTime() === other.start.getTime() && this.end.getTime() === other.end.getTime() ); } } const promoWindow = new DateRange(new Date("2026-11-01"), new Date("2026-11-30")); const blackFriday = { name: "BF", window: promoWindow }; const cyberMonday = { name: "CM", window: promoWindow.extendBy(2) }; ``` ## Why - Sharing is safe. Nothing can change the range under another holder. - No registry or defensive copies needed; equality is by content. - Values are easier to reason about in concurrent code and to cache. ## Steps 1. Check the object has no identity that matters: two instances with the same data should be interchangeable. 2. Remove setters one at a time. Replace each mutation with construction of a new instance, and reassign at the caller. Run the tests after each. 3. Mark fields `readonly` and freeze the instance if your language supports it. 4. Add `equals` (and `hashCode` in Java-like languages). Replace identity comparisons with it. 5. Remove any registry or lookup used to share instances. ## Watch out - Fields holding mutable objects defeat immutability. `Date` is mutable in JavaScript; `readonly` stops reassignment, not `range.start.setDate(...)`. Copy on the way in or use an immutable date type. - Don't convert real entities (a customer, an account) whose changes must be visible everywhere. Those need identity. - Frequent changes to a large value object allocate many copies. Usually negligible; measure if it sits in a hot loop. - The inverse is [Change Value to Reference](https://aipatterns.dev/refactoring/techniques/change-value-to-reference.md). ## Related - Reduces [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md) through shared mutable state. - Inverse: [Change Value to Reference](https://aipatterns.dev/refactoring/techniques/change-value-to-reference.md). - Often follows [Replace Data Value with Object](https://aipatterns.dev/refactoring/techniques/replace-data-value-with-object.md). Shared immutable values are the basis of [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md). ## Agent checklist - [ ] The object has no meaningful identity. - [ ] All setters and mutating methods were replaced with methods returning new instances. - [ ] Nested mutable fields are copied or replaced with immutable types. - [ ] Equality is by value and identity comparisons were updated. - [ ] Tests pass. --- # Replace Array with Object > Replace an array whose positions hold different kinds of data with an object that has a named field for each. - kind: technique - category: organizing-data - canonical: https://aipatterns.dev/refactoring/techniques/replace-array-with-object - inspired-by: https://refactoring.guru/replace-array-with-object ## Problem An array (or tuple) carries unrelated values by position: `row[0]` is a SKU, `row[1]` a quantity, `row[2]` a warehouse code. Readers must remember the positions, and inserting a column shifts every index in the codebase. ## Solution **Define a type with a named field for each position, convert the array at the point it is created, and read fields by name.** ## Before ```ts // parsed from a CSV export: [sku, qty, warehouse, reorderPoint] function lowStock(rows: [string, number, string, number][]): string[] { return rows .filter((r) => r[1] <= r[3]) .map((r) => `${r[0]} at ${r[2]}: ${r[1]} left (reorder at ${r[3]})`); } function parseRow(line: string): [string, number, string, number] { const cols = line.split(","); return [cols[0], Number(cols[1]), cols[2], Number(cols[3])]; } ``` ## After ```ts interface StockLevel { sku: string; quantity: number; warehouse: string; reorderPoint: number; } function parseRow(line: string): StockLevel { const [sku, quantity, warehouse, reorderPoint] = line.split(","); return { sku, quantity: Number(quantity), warehouse, reorderPoint: Number(reorderPoint), }; } function lowStock(levels: StockLevel[]): string[] { return levels .filter((s) => s.quantity <= s.reorderPoint) .map((s) => `${s.sku} at ${s.warehouse}: ${s.quantity} left (reorder at ${s.reorderPoint})`); } ``` ## Why - Code states meaning (`s.reorderPoint`), not position (`r[3]`). - Adding a column changes the parser and the type, not every consumer. - The compiler catches misspelled fields; it cannot tell `r[1]` from `r[3]`. ## Steps 1. Define the type (interface, record or class) with a field per position. 2. At the place the array is created, build the object instead. If many consumers remain, temporarily keep both, or add a converter. 3. Change consumers one at a time from index access to field access. Run the tests after each. 4. Change function signatures from the array type to the new type. 5. Delete the converter and the tuple type when nothing uses them. ## Watch out - Don't apply to arrays that hold many items of the same kind. A list of SKUs is a proper array. - Short, local tuples are idiomatic in some places, such as `[key, value]` pairs from `Object.entries` or a React `useState` return. Destructure them into names instead of converting. - Wire formats and CSV column order still matter at the boundary. Keep the positional mapping in exactly one parser. - Some hot-path numeric code uses typed arrays for memory layout. Don't convert those without measuring. - Inverse: none worth using; positional arrays are a regression except at serialization boundaries. ## Related - Fixes [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md) and [Data Clumps](https://aipatterns.dev/refactoring/smells/data-clumps.md). - If behavior gathers around the new type, continue with [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) or [Replace Data Value with Object](https://aipatterns.dev/refactoring/techniques/replace-data-value-with-object.md). ## Agent checklist - [ ] Positions held different meanings, not a homogeneous list. - [ ] The positional mapping lives in one parser or adapter. - [ ] All index accesses were replaced with named fields. - [ ] Signatures use the new type. - [ ] Tests pass. --- # Duplicate Observed Data > Move domain data out of a UI class into a domain object, and keep the UI in sync with it through observation. - kind: technique - category: organizing-data - canonical: https://aipatterns.dev/refactoring/techniques/duplicate-observed-data - inspired-by: https://refactoring.guru/duplicate-observed-data ## Problem A UI component stores business data in its own fields and computes business rules in its event handlers. The rules can't be tested without rendering, can't be reused by an API or a background job, and every UI change risks breaking them. ## Solution **Create a domain class that owns the data and rules. Have the UI hold a copy of what it displays and subscribe to the domain object for changes.** The UI keeps its fields, but the domain object is the source of truth. ## Before ```ts class CartWidget { private items: { sku: string; price: number; qty: number }[] = []; private totalLabel = ""; onAdd(sku: string, price: number): void { const existing = this.items.find((i) => i.sku === sku); if (existing) existing.qty += 1; else this.items.push({ sku, price, qty: 1 }); const subtotal = this.items.reduce((s, i) => s + i.price * i.qty, 0); const discount = subtotal > 100 ? subtotal * 0.1 : 0; this.totalLabel = `$${(subtotal - discount).toFixed(2)}`; this.render(); } render(): void { /* draws items and totalLabel */ } } ``` ## After ```ts type Listener = () => void; class Cart { private items = new Map(); private listeners: Listener[] = []; subscribe(fn: Listener): void { this.listeners.push(fn); } add(sku: string, price: number): void { const item = this.items.get(sku) ?? { price, qty: 0 }; item.qty += 1; this.items.set(sku, item); this.listeners.forEach((fn) => fn()); } total(): number { const subtotal = [...this.items.values()].reduce((s, i) => s + i.price * i.qty, 0); return subtotal > 100 ? subtotal * 0.9 : subtotal; } } class CartWidget { private totalLabel = ""; constructor(private cart: Cart) { cart.subscribe(() => this.update()); } onAdd(sku: string, price: number): void { this.cart.add(sku, price); } private update(): void { this.totalLabel = `$${this.cart.total().toFixed(2)}`; this.render(); } render(): void { /* draws items and totalLabel */ } } ``` ## Why - The discount rule is testable with no UI. - An API endpoint or a second view can use `Cart` directly. - UI and domain change for different reasons; now they live in different classes. ## Steps 1. Create the domain class, empty, and give the UI a reference to it. 2. Add a subscription mechanism to the domain class (callback list, event emitter, or your framework's store). 3. For one field, add it to the domain class. Make the UI's handler write to the domain object, and make the UI's update callback copy the value back into its own field. Run the tests. 4. Move the business logic that computes from that field into the domain class with [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md). Run the tests. 5. Repeat field by field. Finally, remove UI fields that no longer need a local copy. ## Watch out - Modern UI frameworks already separate state from view: React state and stores, Vue reactivity, signals, MVVM bindings. Use the framework's mechanism instead of writing an observer by hand. - Unsubscribe when the UI is destroyed, or the domain object keeps it alive and calls into dead views. - Notifications fired inside notifications can loop. Keep update callbacks read-only toward the domain. - Don't apply when the "data" is purely presentational (which tab is open, scroll position). That belongs in the UI. - No direct inverse; collapsing domain back into a UI class is the smell. ## Related - Fixes [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md) and [Divergent Change](https://aipatterns.dev/refactoring/smells/divergent-change.md) in UI classes. - The sync mechanism is [Observer](https://aipatterns.dev/design-patterns/observer.md). - Built from [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) and [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md). ## Agent checklist - [ ] Business rules are in the domain class and have tests without rendering. - [ ] The UI writes through the domain object and reads back via notifications. - [ ] Subscriptions are removed when the UI component is disposed. - [ ] The framework's own state mechanism was preferred where one exists. - [ ] Tests pass. --- # Change Unidirectional Association to Bidirectional > Add a back-reference when each of two classes needs to navigate to the other, and keep both sides in sync through one owner. - kind: technique - category: organizing-data - canonical: https://aipatterns.dev/refactoring/techniques/change-unidirectional-association-to-bidirectional - inspired-by: https://refactoring.guru/change-unidirectional-association-to-bidirectional ## Problem A `Project` knows its `Task`s, but a `Task` does not know its project. Code that holds a task and needs the project has to search all projects, or every caller has to pass the project alongside the task. ## Solution **Add a back-reference field on the other class, and make one side responsible for updating both links.** Clients change the association only through that controlling side. ## Before ```ts class Task { constructor(readonly id: string, public title: string) {} } class Project { private tasks: Task[] = []; constructor(readonly id: string, readonly ownerId: string) {} add(task: Task): void { this.tasks.push(task); } has(task: Task): boolean { return this.tasks.includes(task); } } function canEdit(task: Task, userId: string, projects: Project[]): boolean { const project = projects.find((p) => p.has(task)); return project?.ownerId === userId; } ``` ## After ```ts class Task { private _project: Project | null = null; constructor(readonly id: string, public title: string) {} get project(): Project | null { return this._project; } /** Only Project calls this. */ _setProject(project: Project | null): void { this._project = project; } } class Project { private tasks = new Set(); constructor(readonly id: string, readonly ownerId: string) {} add(task: Task): void { task.project?.remove(task); this.tasks.add(task); task._setProject(this); } remove(task: Task): void { if (this.tasks.delete(task)) task._setProject(null); } } function canEdit(task: Task, userId: string): boolean { return task.project?.ownerId === userId; } ``` ## Why - Navigation from task to project is O(1) and needs no extra parameters. - `canEdit` no longer takes the whole project list. - One controlling side keeps the two links consistent. ## Steps 1. Add the back-reference field on the class that lacks it. 2. Choose the controlling side. In one-to-many associations it is usually the "one" side (`Project`). In many-to-many, pick either. 3. On the non-controlling side, add a narrow setter for the back-reference that only the controller calls. 4. Update the controller's modifying methods to set the back-reference. Handle moving an object from one owner to another. Run the tests. 5. Replace lookups and extra parameters in clients with the new navigation. ## Watch out - Bidirectional links double the bookkeeping. A bug in sync leaves the graph inconsistent, which is hard to debug. - They create reference cycles. Garbage-collected languages cope; reference-counted ones (Swift, `Rc` in Rust, C++ `shared_ptr`) need a weak side. - Serializers that walk object graphs (`JSON.stringify`, many ORMs) loop forever or fail on cycles. Exclude the back-reference. - Don't add one when the reverse navigation is rare. A lookup or a parameter is cheaper to maintain. - The inverse is [Change Bidirectional Association to Unidirectional](https://aipatterns.dev/refactoring/techniques/change-bidirectional-association-to-unidirectional.md). ## Related - Helps with [Feature Envy](https://aipatterns.dev/refactoring/smells/feature-envy.md) and [Message Chains](https://aipatterns.dev/refactoring/smells/message-chains.md) that come from roundabout lookups. - Inverse: [Change Bidirectional Association to Unidirectional](https://aipatterns.dev/refactoring/techniques/change-bidirectional-association-to-unidirectional.md). - [Encapsulate Collection](https://aipatterns.dev/refactoring/techniques/encapsulate-collection.md) keeps clients from bypassing the controller. ## Agent checklist - [ ] Both directions of navigation are actually needed. - [ ] Exactly one side controls the association; the other exposes only a narrow setter. - [ ] Moving an item between owners updates all three objects. - [ ] Serializers and reference-counted memory were checked for cycle problems. - [ ] Tests cover add, remove and move. --- # Change Bidirectional Association to Unidirectional > Remove a back-reference that no code needs any more, so two classes stop depending on each other. - kind: technique - category: organizing-data - canonical: https://aipatterns.dev/refactoring/techniques/change-bidirectional-association-to-unidirectional - inspired-by: https://refactoring.guru/change-bidirectional-association-to-unidirectional ## Problem Two classes point to each other, but only one direction is used in practice. The unused direction still costs sync code, creates a dependency cycle between modules, complicates serialization and keeps objects alive longer than necessary. ## Solution **Find the code that uses the unwanted direction, give it another way to get what it needs, then delete the back-reference and its sync logic.** ## Before ```ts class Device { owner: Account | null = null; constructor(readonly serial: string) {} notificationEmail(): string | null { return this.owner?.email ?? null; } } class Account { private devices = new Set(); constructor(readonly id: string, readonly email: string) {} register(device: Device): void { this.devices.add(device); device.owner = this; } firmwareAlert(version: string): string[] { return [...this.devices].map((d) => `${d.notificationEmail()}: ${d.serial} needs ${version}`); } } ``` ## After ```ts class Device { constructor(readonly serial: string) {} } class Account { private devices = new Set(); constructor(readonly id: string, readonly email: string) {} register(device: Device): void { this.devices.add(device); } firmwareAlert(version: string): string[] { return [...this.devices].map((d) => `${this.email}: ${d.serial} needs ${version}`); } } ``` ## Why - `Device` no longer imports `Account`, which breaks a module cycle. - `register` can't leave the two sides out of sync. - Device objects serialize cleanly and can be reused for unowned stock. ## Steps 1. Find every reader of the back-reference (`device.owner`). Include templates, serializers and tests. 2. For each reader, supply the value another way: pass it as a parameter, read it from the controlling side, or look it up through a repository. Run the tests after each. 3. When no reader remains, remove the code that assigns the back-reference. Run the tests. 4. Remove the field. ## Watch out - Hidden readers are common: ORM mappings, GraphQL resolvers, admin panels, logging. Search for the field name as a string too. - If removing the reference forces a slow lookup on a hot path, keep it. - Persistence: in a relational schema, the foreign key usually lives on the "many" side. Removing the object reference does not mean dropping the column. - The inverse is [Change Unidirectional Association to Bidirectional](https://aipatterns.dev/refactoring/techniques/change-unidirectional-association-to-bidirectional.md). ## Related - Fixes [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md) and removes [Dead Code](https://aipatterns.dev/refactoring/smells/dead-code.md) when the link is unused. - Inverse: [Change Unidirectional Association to Bidirectional](https://aipatterns.dev/refactoring/techniques/change-unidirectional-association-to-bidirectional.md). ## Agent checklist - [ ] Every reader of the back-reference was found and given an alternative. - [ ] No hot path now does an expensive lookup instead. - [ ] Sync code that assigned the back-reference is deleted. - [ ] Database columns and ORM mappings were handled deliberately. - [ ] Tests pass. --- # Replace Magic Number with Symbolic Constant > Give a literal number or string with special meaning a named constant, and use the name everywhere the value appears. - kind: technique - category: organizing-data - aliases: Replace Magic Literal - canonical: https://aipatterns.dev/refactoring/techniques/replace-magic-number-with-symbolic-constant - inspired-by: https://refactoring.guru/replace-magic-number-with-symbolic-constant ## Problem Literals such as `86400`, `0.2`, `429` or `"PENDING_REVIEW"` appear in the code with no explanation. Readers guess their meaning, and when a value changes you must find every copy, telling apart the copies of *this* `30` from unrelated `30`s. ## Solution **Declare a constant with a name that states the value's meaning, and replace each occurrence that has that meaning with the name.** ## Before ```ts async function fetchWithRetry(url: string): Promise { for (let attempt = 0; attempt < 4; attempt++) { const res = await fetch(url, { signal: AbortSignal.timeout(8000) }); if (res.status !== 429 && res.status < 500) return res; await sleep(Math.min(250 * 2 ** attempt, 5000)); } throw new Error(`gave up on ${url} after 4 attempts`); } ``` ## After ```ts const MAX_ATTEMPTS = 4; const REQUEST_TIMEOUT_MS = 8_000; const BASE_BACKOFF_MS = 250; const MAX_BACKOFF_MS = 5_000; const HTTP_TOO_MANY_REQUESTS = 429; const HTTP_SERVER_ERROR_MIN = 500; async function fetchWithRetry(url: string): Promise { for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { const res = await fetch(url, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }); const retryable = res.status === HTTP_TOO_MANY_REQUESTS || res.status >= HTTP_SERVER_ERROR_MIN; if (!retryable) return res; await sleep(Math.min(BASE_BACKOFF_MS * 2 ** attempt, MAX_BACKOFF_MS)); } throw new Error(`gave up on ${url} after ${MAX_ATTEMPTS} attempts`); } ``` ## Why - Each number explains itself, and the retry policy reads as a policy. - Changing the attempt count updates the loop and the error message together; the two can no longer drift. - Constants are searchable. `MAX_ATTEMPTS` finds every use; `4` finds noise. ## Steps 1. Pick a literal and decide its meaning. Name the meaning, not the value (`MAX_ATTEMPTS`, not `FOUR`). 2. Declare the constant in the narrowest scope that covers all its uses: function, module, or a shared config module. 3. Find every occurrence of the literal. Replace only those with the same meaning. Run the tests. 4. If a set of related literals form an enumeration (statuses, roles), consider an `enum`, a union type, or [Replace Type Code with Class](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-class.md) instead. ## Watch out - Don't name obvious values: `0` as a starting index, `1` in `n + 1`, `100` in a percentage formula, `2` in "halve this". - Two equal literals with different meanings must get two constants. Merging them couples unrelated rules. - If a value differs between environments or tenants, it is configuration, not a constant. Load it from config. - If the literal needs behavior (validation, formatting, units), it wants a type: [Replace Data Value with Object](https://aipatterns.dev/refactoring/techniques/replace-data-value-with-object.md). - Inverse: inlining a constant back is rarely useful, except for a constant used once whose name repeats the value. ## Related - Fixes [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md), [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) and [Comments](https://aipatterns.dev/refactoring/smells/comments.md) that explain numbers. - [Extract Variable](https://aipatterns.dev/refactoring/techniques/extract-variable.md) is the local, computed equivalent. - [Replace Type Code with Class](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-class.md) handles sets of related codes. ## Agent checklist - [ ] Each constant name describes meaning, not value. - [ ] Only occurrences with the same meaning were replaced. - [ ] Constants live in the narrowest sensible scope. - [ ] Environment-dependent values were moved to configuration instead. - [ ] Tests pass. --- # Encapsulate Field > Make a public field private and give clients accessor methods, so the class controls how its data is read and changed. - kind: technique - category: organizing-data - aliases: Encapsulate Variable - canonical: https://aipatterns.dev/refactoring/techniques/encapsulate-field - inspired-by: https://refactoring.guru/encapsulate-field ## Problem A class exposes a public field. Any code can read it or assign anything to it, so the class cannot enforce rules, react to changes, or later change how the value is stored. ## Solution **Make the field private, add a getter and (only if needed) a setter or an intention-revealing method, and change all clients to use them.** ## Before ```ts class ApiKey { public scopes: string[]; public expiresAt: Date; public revoked = false; constructor(readonly id: string, scopes: string[], expiresAt: Date) { this.scopes = scopes; this.expiresAt = expiresAt; } } // elsewhere key.revoked = true; key.expiresAt = new Date(0); if (!key.revoked && key.expiresAt > new Date()) authorize(); ``` ## After ```ts class ApiKey { private _revokedAt: Date | null = null; constructor( readonly id: string, readonly scopes: readonly string[], private readonly _expiresAt: Date, ) {} get expiresAt(): Date { return new Date(this._expiresAt); } get revoked(): boolean { return this._revokedAt !== null; } revoke(now: Date): void { this._revokedAt ??= now; } isActive(now: Date): boolean { return !this.revoked && this._expiresAt > now; } } // elsewhere key.revoke(new Date()); if (key.isActive(new Date())) authorize(); ``` ## Why - A key can no longer be un-revoked or have its expiry silently rewritten. - The class now records when revocation happened, a change invisible to clients. - Behavior that used the fields (`isActive`) can move into the class. ## Steps 1. Add a getter (and setter, if clients assign the field) with the same name or a clear one. 2. Find every client read and replace it with the getter. Run the tests. 3. Find every client write. Replace it with the setter or, better, with a method named for the intent (`revoke()`, not `setRevoked(true)`). Run the tests. 4. Make the field private. Compile to catch any remaining direct access. 5. Remove the setter if no client needs it. ## Watch out - Getters that return mutable objects (arrays, dates, maps) still let clients modify internals. Return copies or read-only views; for collections use [Encapsulate Collection](https://aipatterns.dev/refactoring/techniques/encapsulate-collection.md). - Plain data carriers (DTOs, API request types, config records) don't need encapsulation. Public `readonly` fields are fine there. - In TypeScript and C#, property accessors keep the `obj.field` syntax, so clients may not need to change for reads. - Frameworks that bind to fields by reflection (ORMs, serializers, form libraries) may need configuration to use accessors or private fields. - Inverse: exposing the field again. Do so only for true data carriers. ## Related - Fixes [Data Class](https://aipatterns.dev/refactoring/smells/data-class.md) and [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md). - [Self Encapsulate Field](https://aipatterns.dev/refactoring/techniques/self-encapsulate-field.md) applies the same idea inside the class. - After encapsulating, use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to pull client logic into the class. ## Agent checklist - [ ] The field is private and the compiler confirms no direct access remains. - [ ] Setters exist only where clients need to write, and prefer intent-named methods. - [ ] Getters don't leak mutable internals. - [ ] Reflection-based frameworks still read and write the data correctly. - [ ] Tests pass. --- # Encapsulate Collection > Stop a getter from handing out a mutable internal collection; return a read-only view and add methods to add and remove items. - kind: technique - category: organizing-data - canonical: https://aipatterns.dev/refactoring/techniques/encapsulate-collection - inspired-by: https://refactoring.guru/encapsulate-collection ## Problem A class keeps a list or set and its getter returns the internal instance. Clients push, splice and clear it directly, bypassing any rules the class should enforce, and the class can't tell when its contents change. ## Solution **Return a read-only view or a copy from the getter, and add explicit `add` and `remove` methods that enforce the rules.** Remove any setter that replaces the whole collection. ## Before ```ts class Shift { public staff: string[] = []; constructor(readonly id: string, readonly maxStaff: number) {} } // scheduling UI shift.staff.push(nurseId); // importer shift.staff = rows.map((r) => r.employeeId); // report const overbooked = shift.staff.length > shift.maxStaff; ``` ## After ```ts class Shift { private readonly _staff = new Set(); constructor(readonly id: string, readonly maxStaff: number) {} get staff(): ReadonlySet { return this._staff; } assign(employeeId: string): void { if (this._staff.size >= this.maxStaff) { throw new Error(`shift ${this.id} is full`); } this._staff.add(employeeId); } unassign(employeeId: string): void { this._staff.delete(employeeId); } } // scheduling UI shift.assign(nurseId); // importer rows.forEach((r) => shift.assign(r.employeeId)); ``` ## Why - The staffing limit is enforced on every path, including bulk import. - Overbooking is impossible, so the report check is gone. - The class can add logging, events or audit trails in one place. ## Steps 1. Add `add` and `remove` methods (named for the domain: `assign`, `unassign`) that modify the internal collection. 2. Initialize the field to an empty collection so the methods never see `undefined`. 3. Replace client mutations (`push`, `splice`, `delete`) with the new methods. Run the tests. 4. Replace whole-collection assignment with a loop over `add`, or a dedicated method (`replaceStaff(ids)`) that validates. Remove the setter. 5. Change the getter to return a read-only type, a frozen copy, or an iterator. Run the tests. ## Watch out - A `ReadonlySet` or `ReadonlyArray` type in TypeScript is a compile-time guard only. A determined caller can cast it. Return a copy if the boundary is untrusted or crosses plugin code. - Returning a copy on each call costs allocations; avoid calling the getter in tight loops. - Behavior change: the old code allowed states the new methods reject, such as an overbooked shift. Check existing data and imports. - ORMs often need a mutable collection field to map relations. Keep the mapped field private and expose the read-only view. - Inverse: none; exposing the raw collection again is the smell. ## Related - Fixes [Data Class](https://aipatterns.dev/refactoring/smells/data-class.md) and [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md). - A special case of [Encapsulate Field](https://aipatterns.dev/refactoring/techniques/encapsulate-field.md). - Required for keeping both sides consistent in [Change Unidirectional Association to Bidirectional](https://aipatterns.dev/refactoring/techniques/change-unidirectional-association-to-bidirectional.md). ## Agent checklist - [ ] No code outside the class mutates the collection directly. - [ ] The getter returns a read-only view or copy. - [ ] Add and remove methods enforce the class's invariants. - [ ] Whole-collection setters are removed or validated. - [ ] Existing data that violates new invariants was found and handled. --- # Replace Type Code with Class > Replace a set of string or number codes that don't affect behavior with a dedicated type, so only valid codes can be used. - kind: technique - category: organizing-data - canonical: https://aipatterns.dev/refactoring/techniques/replace-type-code-with-class - inspired-by: https://refactoring.guru/replace-type-code-with-class ## Problem A field holds one of a fixed set of codes, such as `"low" | "normal" | "urgent"` stored as a plain `string`, or `1`, `2`, `3` as `number`. Any string compiles, typos slip through, and facts about each code (its label, its sort order) are scattered across the codebase. ## Solution **Create a type whose only instances are the valid codes, and move the data attached to each code into it.** Use this variant when the code does not drive conditional behavior; if it does, see the subclass or State/Strategy variants. ## Before ```ts class Ticket { constructor(public title: string, public priority: string) {} } function sortQueue(tickets: Ticket[]): Ticket[] { const rank: Record = { urgent: 0, normal: 1, low: 2 }; return [...tickets].sort((a, b) => rank[a.priority] - rank[b.priority]); } function badge(t: Ticket): string { return t.priority === "urgent" ? "P1" : t.priority === "normal" ? "P2" : "P3"; } new Ticket("Login broken", "urgnet"); // compiles ``` ## After ```ts class Priority { static readonly Urgent = new Priority("urgent", 0, "P1"); static readonly Normal = new Priority("normal", 1, "P2"); static readonly Low = new Priority("low", 2, "P3"); private static readonly all = [Priority.Urgent, Priority.Normal, Priority.Low]; private constructor(readonly code: string, readonly rank: number, readonly badge: string) {} static fromCode(code: string): Priority { const p = Priority.all.find((x) => x.code === code); if (!p) throw new Error(`unknown priority ${code}`); return p; } } class Ticket { constructor(public title: string, public priority: Priority) {} } const sortQueue = (tickets: Ticket[]) => [...tickets].sort((a, b) => a.priority.rank - b.priority.rank); const badge = (t: Ticket) => t.priority.badge; ``` ## Why - Invalid codes are rejected at the boundary (`fromCode`) and impossible in code. - Rank and badge live with the code. Adding a priority means one new line. - Signatures say `Priority`, not `string`. ## Steps 1. Create the class with a private constructor and one static instance per code. Keep the raw `code` as a field for persistence. 2. Add a `fromCode` factory that validates. 3. Change the owner's field type. Convert at the boundaries: `fromCode` when loading, `.code` when saving. Run the tests. 4. Move per-code data (labels, ranks, colors) from lookup tables into the class. Run the tests after each move. 5. Replace remaining string comparisons with instance comparisons (`=== Priority.Urgent`). ## Watch out - In TypeScript, a string literal union (`type Priority = "urgent" | "normal" | "low"`) already gives compile-time safety with no runtime cost. Prefer it when there is no attached data. Other languages have `enum` with fields (Java, Kotlin, Rust, Swift); use those. - If code branches on the type to *behave* differently, this is only a first step. Continue with [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md) or [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md). - Persisted values must stay the same strings or numbers. Serialize `code`, never the object. - Inverse: go back to a union type or plain constants with [Replace Magic Number with Symbolic Constant](https://aipatterns.dev/refactoring/techniques/replace-magic-number-with-symbolic-constant.md) when the class carries no data. ## Related - Fixes [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md) and the [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md) of scattered lookup tables. - Siblings: [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md) and [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md). ## Agent checklist - [ ] The codes do not drive behavior through conditionals; otherwise a sibling technique fits. - [ ] A language-native enum or union type was considered first. - [ ] Only valid instances can be constructed; parsing validates. - [ ] Persistence still stores the original code values. - [ ] Tests pass. --- # Replace Type Code with Subclasses > Turn each value of an immutable type code that drives conditionals into a subclass, and move the branch logic into overrides. - kind: technique - category: organizing-data - aliases: Replace Type Code with Polymorphism - canonical: https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses - inspired-by: https://refactoring.guru/replace-type-code-with-subclasses ## Problem A class has a type field, and several methods `switch` on it. Each new type means editing every switch. Some fields only make sense for one type and sit unused for the others. ## Solution **Create a subclass for each type value, move each branch into an override in its subclass, and create instances through a factory.** Use this when the type of an object never changes after creation. ## Before ```ts class Notification { constructor( readonly channel: "email" | "sms" | "push", readonly recipient: string, readonly body: string, readonly deviceToken?: string, // push only ) {} maxLength(): number { switch (this.channel) { case "email": return 100_000; case "sms": return 160; case "push": return 240; } } send(gateway: Gateway): Promise { switch (this.channel) { case "email": return gateway.email(this.recipient, this.body); case "sms": return gateway.sms(this.recipient, this.body.slice(0, 160)); case "push": return gateway.push(this.deviceToken!, this.body); } } } ``` ## After ```ts abstract class Notification { constructor(readonly recipient: string, readonly body: string) {} abstract maxLength(): number; abstract send(gateway: Gateway): Promise; static create(channel: string, recipient: string, body: string, deviceToken?: string): Notification { switch (channel) { case "email": return new EmailNotification(recipient, body); case "sms": return new SmsNotification(recipient, body); case "push": return new PushNotification(recipient, body, deviceToken!); default: throw new Error(`unknown channel ${channel}`); } } } class EmailNotification extends Notification { maxLength() { return 100_000; } send(g: Gateway) { return g.email(this.recipient, this.body); } } class SmsNotification extends Notification { maxLength() { return 160; } send(g: Gateway) { return g.sms(this.recipient, this.body.slice(0, this.maxLength())); } } class PushNotification extends Notification { constructor(recipient: string, body: string, readonly deviceToken: string) { super(recipient, body); } maxLength() { return 240; } send(g: Gateway) { return g.push(this.deviceToken, this.body); } } ``` ## Why - Adding a channel is a new subclass plus one factory line; existing classes stay untouched. - `deviceToken` exists only where it means something, and is no longer optional. - Each channel's rules can be read and tested on their own. ## Steps 1. Wrap construction in a factory method (`create`) so callers stop calling the constructor with a type code. Run the tests. 2. Self-encapsulate the type field: replace direct reads with a getter. 3. Create one subclass per type value. Override the getter to return its constant. Make the factory return the subclass. Run the tests. 4. For each method that switches on the type, copy each branch into an override in the matching subclass. Make the base method abstract. Run the tests after each method. 5. Move type-specific fields down into their subclass. 6. Remove the type field from the base if nothing reads it any more. ## Watch out - Requires the type to be fixed for the object's lifetime. If an order can go from `"draft"` to `"paid"`, use [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md). - Requires that the class is not already subclassed along another axis. If it is, composition (State/Strategy) avoids a combinatorial explosion. - One switch over three stable values may be clearer than three classes. In TypeScript, a discriminated union with exhaustive `switch` is often the idiomatic alternative, and the compiler checks completeness. - The factory still has a switch. That is fine; it is the one place that maps codes to types. - The inverse is [Replace Subclass with Fields](https://aipatterns.dev/refactoring/techniques/replace-subclass-with-fields.md). ## Related - Fixes [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md), [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md) and [Temporary Field](https://aipatterns.dev/refactoring/smells/temporary-field.md). - Inverse: [Replace Subclass with Fields](https://aipatterns.dev/refactoring/techniques/replace-subclass-with-fields.md). - Uses [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md) for each switch, and a [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) for creation. ## Agent checklist - [ ] The type code never changes after construction. - [ ] The class has no other subclass hierarchy that would multiply. - [ ] Construction goes through one factory; no caller passes type codes to constructors. - [ ] Every switch on the type was replaced with overrides, and type-only fields moved down. - [ ] A discriminated union was considered as a lighter alternative. --- # Replace Type Code with State/Strategy > Move behavior that depends on a type code into separate state or strategy objects that the host holds and can swap at runtime. - kind: technique - category: organizing-data - canonical: https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy - inspired-by: https://refactoring.guru/replace-type-code-with-state-strategy ## Problem A class branches on a type code in several methods, and the code changes during the object's life, or the class already has subclasses. [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md) can't work, because an object cannot change its class. ## Solution **Create an interface for the type-dependent behavior, one implementation per code, and give the host a field holding the current implementation.** Changing the type code becomes replacing that object. ## Before ```ts class PurchaseOrder { status: "draft" | "submitted" | "approved" = "draft"; lines: PoLine[] = []; addLine(line: PoLine): void { if (this.status !== "draft") throw new Error("only drafts can be edited"); this.lines.push(line); } submit(): void { if (this.status !== "draft") throw new Error(`cannot submit from ${this.status}`); if (this.lines.length === 0) throw new Error("empty order"); this.status = "submitted"; } approve(): void { if (this.status !== "submitted") throw new Error(`cannot approve from ${this.status}`); this.status = "approved"; } } ``` ## After ```ts interface PoState { readonly name: string; addLine(po: PurchaseOrder, line: PoLine): void; submit(po: PurchaseOrder): void; approve(po: PurchaseOrder): void; } const reject = (from: string, action: string) => () => { throw new Error(`cannot ${action} from ${from}`); }; const Draft: PoState = { name: "draft", addLine: (po, line) => po.lines.push(line), submit: (po) => { if (po.lines.length === 0) throw new Error("empty order"); po.state = Submitted; }, approve: reject("draft", "approve"), }; const Submitted: PoState = { name: "submitted", addLine: () => { throw new Error("only drafts can be edited"); }, submit: reject("submitted", "submit"), approve: (po) => { po.state = Approved; }, }; const Approved: PoState = { name: "approved", addLine: () => { throw new Error("only drafts can be edited"); }, submit: reject("approved", "submit"), approve: reject("approved", "approve"), }; class PurchaseOrder { state: PoState = Draft; lines: PoLine[] = []; get status() { return this.state.name; } addLine(line: PoLine) { this.state.addLine(this, line); } submit() { this.state.submit(this); } approve() { this.state.approve(this); } } ``` ## Why - Each status lists what it allows in one place. Adding `"rejected"` is a new object, not edits to every method. - The host keeps its identity while its behavior changes. - Works even if `PurchaseOrder` already has subclasses. ## Steps 1. Self-encapsulate the type code so reads and writes go through accessors. 2. Define the state or strategy interface with one method per type-dependent operation. 3. Create one implementation per code. Each starts by returning or throwing what the matching branch did. 4. Add a field on the host for the current object. Make the type-code setter assign the matching implementation, and the getter read its `name`. Run the tests. 5. For each method, move the branch bodies into the implementations and make the host delegate. Run the tests after each method. 6. Remove the raw type-code field. ## Watch out - State or Strategy? If implementations trigger transitions to each other, as here, it is [State](https://aipatterns.dev/design-patterns/state.md). If an outside caller picks one and it rarely changes, it is [Strategy](https://aipatterns.dev/design-patterns/strategy.md). The mechanics are the same. - Persist the state's `name`, not the object. Rebuild the state object when loading. - For two or three states with simple rules, a transition table (`Record`) or a discriminated union may be clearer than objects. - If the type never changes and the class has no other subclasses, [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md) is simpler. - Inverse: collapse states back into a code and conditionals when the variants shrink to trivial differences. ## Related - Fixes [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md), [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md) and [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md). - Patterns: [State](https://aipatterns.dev/design-patterns/state.md) and [Strategy](https://aipatterns.dev/design-patterns/strategy.md). - Siblings: [Replace Type Code with Class](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-class.md) and [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md). ## Agent checklist - [ ] The type changes at runtime, or the host already has subclasses. - [ ] The interface covers every operation that branched on the type. - [ ] Transitions happen in exactly one place per state. - [ ] Persistence stores the code and rebuilds the state on load. - [ ] Tests cover every allowed and forbidden transition. --- # Replace Subclass with Fields > Collapse subclasses that differ only in the constants their methods return into fields on the parent, then delete the subclasses. - kind: technique - category: organizing-data - aliases: Remove Subclass - canonical: https://aipatterns.dev/refactoring/techniques/replace-subclass-with-fields - inspired-by: https://refactoring.guru/replace-subclass-with-fields ## Problem A hierarchy has several subclasses whose only job is to return different constant values: a label, a rate, a flag. There is no real behavior in them, yet each new variant needs a new class. ## Solution **Add fields to the parent for the values, have the parent's methods return the fields, replace each subclass with a factory that sets those fields, and delete the subclasses.** ## Before ```ts abstract class StoragePlan { abstract name(): string; abstract quotaGb(): number; abstract versioning(): boolean; } class StarterPlan extends StoragePlan { name() { return "Starter"; } quotaGb() { return 50; } versioning() { return false; } } class TeamPlan extends StoragePlan { name() { return "Team"; } quotaGb() { return 1_000; } versioning() { return true; } } class ArchivePlan extends StoragePlan { name() { return "Archive"; } quotaGb() { return 10_000; } versioning() { return true; } } ``` ## After ```ts class StoragePlan { private constructor( readonly name: string, readonly quotaGb: number, readonly versioning: boolean, ) {} static starter() { return new StoragePlan("Starter", 50, false); } static team() { return new StoragePlan("Team", 1_000, true); } static archive() { return new StoragePlan("Archive", 10_000, true); } } ``` ## Why - Plans become data. A new tier is one line, and could come from configuration. - Four types collapse into one; readers see every plan's values side by side. - Values are comparable and serializable without type checks. ## Steps 1. Add a factory method on the parent for each subclass, and replace every `new Subclass()` with the factory. Run the tests. 2. Replace any `instanceof Subclass` checks with a field or method that answers the same question. 3. Declare fields on the parent for each constant-returning method. Add a constructor that takes them. 4. Change each subclass to pass its constants to the parent constructor. Make the parent methods return the fields. Run the tests. 5. Remove the overrides from the subclasses, then change each factory to construct the parent directly. Run the tests. 6. Delete the subclasses and make the parent concrete. ## Watch out - Only apply when subclasses differ in *data*. If any override contains real logic, keep that subclass or use [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md). - `instanceof` checks, type guards, DI registrations and serializers keyed on class names will break. Search for them first. - Changing from methods (`plan.quotaGb()`) to fields (`plan.quotaGb`) changes call syntax. Keep getter methods if you want to avoid touching callers. - The inverse is [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md). Go back when variants start needing different behavior. ## Related - Fixes [Lazy Class](https://aipatterns.dev/refactoring/smells/lazy-class.md) and [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md). - Inverse: [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md). - [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md) applies when a single subclass is redundant. ## Agent checklist - [ ] Every subclass override returns a constant; none contains logic. - [ ] All construction goes through factories before subclasses are removed. - [ ] `instanceof` checks and class-name-based registrations were replaced. - [ ] The parent is concrete and the subclasses are deleted. - [ ] Tests pass. --- # Decompose Conditional > Move a complex condition and each of its branches into well-named functions so the decision reads as intent instead of arithmetic. - kind: technique - category: simplifying-conditional-expressions - canonical: https://aipatterns.dev/refactoring/techniques/decompose-conditional - inspired-by: https://refactoring.guru/decompose-conditional ## Problem An `if` statement has a condition built from several comparisons, and each branch holds a block of calculation. You have to evaluate the boolean logic in your head to learn *why* the branch exists, and the branches are too long to see both at once. ## Solution **Extract the condition into a function named for the question it answers, and extract each branch into a function named for what it does.** The `if` statement stays, but it now reads as one line of intent per part. ## Before ```ts function lateFee(invoice: Invoice, today: Date): number { let fee: number; if ( today.getTime() > invoice.dueDate.getTime() + 3 * DAY_MS && invoice.status !== "disputed" && !invoice.customer.feeWaiver ) { const daysLate = Math.floor((today.getTime() - invoice.dueDate.getTime()) / DAY_MS); fee = Math.min(invoice.balance * 0.015 * Math.ceil(daysLate / 30), invoice.balance * 0.1); } else { fee = 0; } return fee; } ``` ## After ```ts function lateFee(invoice: Invoice, today: Date): number { return isPastGracePeriod(invoice, today) ? accruedPenalty(invoice, today) : 0; } function isPastGracePeriod(invoice: Invoice, today: Date): boolean { const graceEnds = invoice.dueDate.getTime() + 3 * DAY_MS; return today.getTime() > graceEnds && invoice.status !== "disputed" && !invoice.customer.feeWaiver; } function accruedPenalty(invoice: Invoice, today: Date): number { const daysLate = Math.floor((today.getTime() - invoice.dueDate.getTime()) / DAY_MS); const monthsLate = Math.ceil(daysLate / 30); return Math.min(invoice.balance * 0.015 * monthsLate, invoice.balance * 0.1); } ``` ## Why - The condition gets a name, so the reader learns the business rule without decoding three comparisons. - Each branch can be read, tested and changed on its own. - The top-level function shrinks to the decision, which is usually what a reviewer wants to check. ## Steps 1. Extract the whole condition into a function that returns `boolean`. Name it for the question, not the mechanics (`isPastGracePeriod`, not `checkDateAndStatus`). Run the tests. 2. Extract the `then` branch into its own function with [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md). Run the tests. 3. Extract the `else` branch if it is more than a trivial value. Run the tests. 4. If both branches now return a value, collapse the `if` into a ternary or early return. Run the tests. ## Watch out - Do not decompose a condition that is already one clear comparison (`if (cart.isEmpty())`). A wrapper named `cartIsEmpty` adds a jump and no meaning. - Extracted predicates must stay side-effect free. If the original condition called something that mutates state, keep that call ordering visible. - Watch short-circuit evaluation. Moving `a && b()` into a function is safe; splitting it into two separately evaluated variables may call `b()` when it was previously skipped. - If the branches pick between whole *types* of behavior and the same `if` recurs elsewhere, go further with [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md). - There is no inverse technique; inline the functions with [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md) if a name turns out to add nothing. ## Related - Fixes [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md) and replaces explanatory [Comments](https://aipatterns.dev/refactoring/smells/comments.md) with names. - Built from [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md). - Pair with [Consolidate Conditional Expression](https://aipatterns.dev/refactoring/techniques/consolidate-conditional-expression.md) when several `if`s share one outcome. - Follow with [Replace Nested Conditional with Guard Clauses](https://aipatterns.dev/refactoring/techniques/replace-nested-conditional-with-guard-clauses.md) when decomposed branches are nested. ## Agent checklist - [ ] The condition has at least two clauses or a non-obvious meaning; otherwise leave it inline. - [ ] Each extracted predicate is pure and named as a yes/no question. - [ ] Short-circuit order and evaluation count of every call in the condition are unchanged. - [ ] Tests pass after each extraction, not only at the end. --- # Consolidate Conditional Expression > Merge a run of separate checks that all lead to the same result into one condition with a name that states the shared rule. - kind: technique - category: simplifying-conditional-expressions - canonical: https://aipatterns.dev/refactoring/techniques/consolidate-conditional-expression - inspired-by: https://refactoring.guru/consolidate-conditional-expression ## Problem A function has several `if` statements in a row, and every one of them returns the same value or performs the same action. The checks look independent, so a reader cannot tell they express one rule, and a new check gets added in the wrong place. ## Solution **Combine the checks into a single boolean expression with `||` (or `&&` for nested checks), then extract that expression into a function named for the rule.** ## Before ```ts function canIssueRefund(order: Order, now: Date): boolean { if (order.paymentMethod === "gift-card") return false; if (order.refundedAt !== null) return false; if (now.getTime() - order.deliveredAt.getTime() > 30 * DAY_MS) return false; if (order.items.every((item) => item.finalSale)) return false; return order.total > 0; } ``` ## After ```ts function canIssueRefund(order: Order, now: Date): boolean { if (isRefundIneligible(order, now)) return false; return order.total > 0; } function isRefundIneligible(order: Order, now: Date): boolean { const outsideWindow = now.getTime() - order.deliveredAt.getTime() > 30 * DAY_MS; return ( order.paymentMethod === "gift-card" || order.refundedAt !== null || outsideWindow || order.items.every((item) => item.finalSale) ); } ``` ## Why - The list of disqualifiers now lives in one place with one name, so adding a fifth is obvious. - The calling function reads as policy: ineligible orders get `false`. - The consolidated predicate is a natural unit to test with a table of cases. ## Steps 1. Confirm every check in the run has no side effects and produces the identical outcome. If any check logs, mutates or returns a different value, stop. 2. Join sequential checks with `||`, preserving their original order. For nested `if`s with one outcome, join with `&&`. Run the tests. 3. Extract the combined expression into a function with [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md). Run the tests. 4. Rename the function until it describes the rule without restating the clauses. ## Watch out - Do not consolidate checks that are truly independent concerns that happen to share a return value today, such as a permissions check and a validation check. They will diverge, and you will split them again. - Keep clause order. `||` short-circuits left to right, and a later clause may depend on an earlier one guarding against `null`. - If one check needs its own error message or log line, it is not the same outcome. Leave it separate. - The rough inverse is [Decompose Conditional](https://aipatterns.dev/refactoring/techniques/decompose-conditional.md) applied to a single large condition; use both together, consolidate first, then name. ## Related - Fixes [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) in the form of repeated outcomes, and shortens [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md). - Often followed by [Decompose Conditional](https://aipatterns.dev/refactoring/techniques/decompose-conditional.md) or [Replace Nested Conditional with Guard Clauses](https://aipatterns.dev/refactoring/techniques/replace-nested-conditional-with-guard-clauses.md). ## Agent checklist - [ ] Every merged check returns or does exactly the same thing. - [ ] No merged clause has side effects. - [ ] Clause order is preserved so null guards still protect later clauses. - [ ] The extracted predicate's name states the rule, not the list of checks. --- # Consolidate Duplicate Conditional Fragments > Move code that runs identically in every branch of a conditional to before or after the conditional so each branch holds only what differs. - kind: technique - category: simplifying-conditional-expressions - canonical: https://aipatterns.dev/refactoring/techniques/consolidate-duplicate-conditional-fragments - inspired-by: https://refactoring.guru/consolidate-duplicate-conditional-fragments ## Problem Every branch of an `if/else` or `switch` ends (or starts) with the same statements. The duplication hides what actually varies between branches, and a fix applied to one copy is easily forgotten in the others. ## Solution **Hoist the shared statements out of the conditional: leading duplicates go before it, trailing duplicates go after it.** Each branch keeps only its unique work. ## Before ```ts async function reserveStock(sku: string, qty: number, warehouse: Warehouse): Promise { const item = await warehouse.find(sku); if (item.onHand >= qty) { item.onHand -= qty; item.reserved += qty; await warehouse.save(item); audit.log("reserve", sku, qty); } else if (item.backorderable) { item.backordered += qty - item.onHand; item.reserved += item.onHand; item.onHand = 0; await warehouse.save(item); audit.log("reserve", sku, qty); } else { throw new OutOfStockError(sku); } } ``` ## After ```ts async function reserveStock(sku: string, qty: number, warehouse: Warehouse): Promise { const item = await warehouse.find(sku); if (item.onHand >= qty) { item.onHand -= qty; item.reserved += qty; } else if (item.backorderable) { item.backordered += qty - item.onHand; item.reserved += item.onHand; item.onHand = 0; } else { throw new OutOfStockError(sku); } await warehouse.save(item); audit.log("reserve", sku, qty); } ``` ## Why - The save and the audit line exist once, so they cannot drift apart. - Each branch now shows only the stock arithmetic that differs. - The function's shape (decide, then persist) is visible. ## Steps 1. Find statements that are textually and semantically identical at the start or end of every branch, including any `else` and `default`. 2. Check that a branch which exits early (by `throw`, `return`, `continue`) does not receive code it previously skipped. Here the `throw` branch never saved, and after the move it still never reaches the save. 3. Move trailing duplicates below the conditional, or leading duplicates above it. Run the tests. 4. If the duplicate sits in the middle of each branch, check whether the statements around it can be reordered safely. If not, extract it into a function instead. 5. For duplicates inside `try` and `catch`, move them to `finally` only if they must also run on failure. ## Watch out - Do not hoist code past a branch that exits early unless that branch also ran it before. - Leading duplicates moved above the conditional run before the condition is evaluated. If the condition reads state those statements change, the result changes. - Statements that look identical may reference variables assigned differently per branch. Compare the values, not only the text. - If only some branches share the fragment, extract a function rather than restructuring the conditional. ## Related - Fixes [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) inside conditionals. - Combine with [Consolidate Conditional Expression](https://aipatterns.dev/refactoring/techniques/consolidate-conditional-expression.md) when the branches also share a condition. - Use [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) for a shared fragment that cannot move. ## Agent checklist - [ ] The moved code appeared in every branch, including the `else`/`default`. - [ ] No branch that previously exited early now executes the moved code. - [ ] Moved leading code does not affect the condition it now precedes. - [ ] Tests cover each branch, and they pass after the move. --- # Remove Control Flag > Replace a boolean variable that steers a loop or function with break, continue or return so control flow is explicit. - kind: technique - category: simplifying-conditional-expressions - aliases: Replace Control Flag with Break - canonical: https://aipatterns.dev/refactoring/techniques/remove-control-flag - inspired-by: https://refactoring.guru/remove-control-flag ## Problem A loop or function uses a boolean such as `found`, `done` or `keepGoing` to decide whether to continue. The flag is set in one place and tested in another, so you have to trace every assignment to know when the code actually stops. ## Solution **Delete the flag and exit at the point where it would have been set, using `break`, `continue` or `return`.** If the flag also carried a result, return that result directly. ## Before ```ts function firstConflict(slot: TimeSlot, bookings: Booking[]): Booking | null { let conflict: Booking | null = null; let searching = true; for (let i = 0; i < bookings.length && searching; i++) { const b = bookings[i]; if (b.status === "cancelled") { continue; } if (b.start < slot.end && slot.start < b.end) { conflict = b; searching = false; } } return conflict; } ``` ## After ```ts function firstConflict(slot: TimeSlot, bookings: Booking[]): Booking | null { for (const b of bookings) { if (b.status === "cancelled") continue; if (b.start < slot.end && slot.start < b.end) return b; } return null; } ``` ## Why - The exit point is where the decision is made, so a reader sees it at once. - The loop header goes back to a plain iteration with no hidden second condition. - Fewer mutable variables means fewer states to reason about and test. ## Steps 1. Find every assignment to the flag. Confirm what each one means: stop the loop, skip the rest of this iteration, or stop the function. 2. At the assignment that ends the work, add the matching `break`, `continue` or `return` right after it. Keep the flag for now. Run the tests. 3. If the flag also held a result, change that exit to `return result` directly. Run the tests. 4. Remove the flag from the loop condition, then delete its declaration and remaining assignments. Run the tests. 5. If code after the loop only ran when the flag was set, move it to the exit point or extract the loop into a function so `return` is available. ## Watch out - A `break` inside a nested loop only exits the inner loop. Extract the loops into a function and `return`, or use a labeled `break` if your team accepts labels. - If the code after the flag assignment did meaningful work in the same iteration (logging, counting), an early exit skips it. Move that work above the exit. - In languages or style guides that require a single exit point, extract the search into its own small function so early returns stay local. - Do not remove a flag that is part of an object's real state, such as `isCancelled` on a job. That is data, not control flow. - There is no inverse technique. ## Related - Shortens [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md). - Usually followed by [Replace Nested Conditional with Guard Clauses](https://aipatterns.dev/refactoring/techniques/replace-nested-conditional-with-guard-clauses.md). - Use [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) first when the loop sits in the middle of a larger function. ## Agent checklist - [ ] Every flag assignment has been mapped to `break`, `continue` or `return`. - [ ] Work that ran after the flag assignment in the same iteration still runs. - [ ] Nested-loop exits leave the correct loop. - [ ] The flag variable no longer exists, and tests pass. --- # Replace Nested Conditional with Guard Clauses > Handle special and invalid cases first with early returns so the normal path runs flat at the end of the function. - kind: technique - category: simplifying-conditional-expressions - aliases: Early Return - canonical: https://aipatterns.dev/refactoring/techniques/replace-nested-conditional-with-guard-clauses - inspired-by: https://refactoring.guru/replace-nested-conditional-with-guard-clauses ## Problem A function wraps its main work in several levels of `if/else`. The normal case is buried at the deepest indentation, and the `else` branches that handle edge cases are far from the conditions that trigger them. ## Solution **Check each special case at the top of the function and return (or throw) immediately. Leave the normal case as unindented code at the bottom.** ## Before ```ts function issueToken(req: LoginRequest, user: User | null): AuthResult { let result: AuthResult; if (user) { if (!user.locked) { if (verifyPassword(req.password, user.passwordHash)) { if (user.mfaEnabled && !req.otp) { result = { kind: "mfa-required" }; } else { result = { kind: "ok", token: signJwt(user.id) }; } } else { result = { kind: "denied", reason: "bad-credentials" }; } } else { result = { kind: "denied", reason: "locked" }; } } else { result = { kind: "denied", reason: "bad-credentials" }; } return result; } ``` ## After ```ts function issueToken(req: LoginRequest, user: User | null): AuthResult { if (!user) return { kind: "denied", reason: "bad-credentials" }; if (user.locked) return { kind: "denied", reason: "locked" }; if (!verifyPassword(req.password, user.passwordHash)) { return { kind: "denied", reason: "bad-credentials" }; } if (user.mfaEnabled && !req.otp) return { kind: "mfa-required" }; return { kind: "ok", token: signJwt(user.id) }; } ``` ## Why - Each precondition sits next to its outcome, so you read the rules top to bottom. - The success path is the last line, at the lowest indentation, which is where readers look for it. - Adding a new rejection rule is one new line, not another nesting level. ## Steps 1. Pick the outermost condition whose `else` branch is a special case. Invert the condition, return the `else` result at the top, and unwrap the `then` body by one level. Run the tests. 2. Repeat for the next level. Keep the original order of checks, because later checks may rely on earlier ones (here `user` must be non-null before `user.locked`). 3. When the only remaining code is the normal path, remove the result variable and return directly. Run the tests. 4. If several guards return the same value, consider [Consolidate Conditional Expression](https://aipatterns.dev/refactoring/techniques/consolidate-conditional-expression.md). Keep them separate when the reasons differ. ## Watch out - Do not use guards when both branches are equally normal, such as choosing between two pricing formulas. A plain `if/else` (or a ternary) states that the cases are peers. - Cleanup code at the end of the function will be skipped by early returns. Move it into `finally`, a `using` block or a wrapper before adding guards. - Inverting conditions is error prone. `!(a && b)` is `!a || !b`; check each inversion with a test. - Timing-sensitive checks (password comparison, user lookup) may need to run in the same order and count for security reasons. Preserve order exactly. - The inverse direction, wrapping returns back into nested blocks, has no named technique and is rarely useful. ## Related - Shortens [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md). - Often applied after [Remove Control Flag](https://aipatterns.dev/refactoring/techniques/remove-control-flag.md) and [Decompose Conditional](https://aipatterns.dev/refactoring/techniques/decompose-conditional.md). ## Agent checklist - [ ] The guarded cases are exceptional; the remaining code is the single normal path. - [ ] Every inverted condition is logically equivalent to the original `else` trigger. - [ ] Check order is unchanged, so null guards still protect later checks. - [ ] No cleanup or logging at the function's end is now skipped. --- # Replace Conditional with Polymorphism > Replace a switch on an object's type or kind with one class per case that overrides a shared method, so new cases add code instead of editing it. - kind: technique - category: simplifying-conditional-expressions - canonical: https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism - inspired-by: https://refactoring.guru/replace-conditional-with-polymorphism ## Problem A conditional selects behavior by checking a type code, a `kind` field or `instanceof`. The same switch, with the same cases, appears in several functions. Each new kind forces you to find and edit every one of them. ## Solution **Create a class (or an object implementing a shared interface) for each case, move each branch into an overriding method on its class, and replace the conditional with a single method call.** ## Before ```ts type Notification = { channel: "email" | "sms" | "push"; to: string; body: string }; function send(n: Notification): Promise { switch (n.channel) { case "email": return mailer.send({ to: n.to, subject: n.body.slice(0, 60), html: n.body }); case "sms": return smsGateway.post(n.to, n.body.slice(0, 160)); case "push": return pushService.notify(n.to, { alert: n.body }); } } function costCents(n: Notification): number { switch (n.channel) { case "email": return 0; case "sms": return Math.ceil(n.body.length / 160) * 4; case "push": return 0; } } ``` ## After ```ts interface Channel { send(to: string, body: string): Promise; costCents(body: string): number; } const email: Channel = { send: (to, body) => mailer.send({ to, subject: body.slice(0, 60), html: body }), costCents: () => 0, }; const sms: Channel = { send: (to, body) => smsGateway.post(to, body.slice(0, 160)), costCents: (body) => Math.ceil(body.length / 160) * 4, }; const push: Channel = { send: (to, body) => pushService.notify(to, { alert: body }), costCents: () => 0, }; const channels = { email, sms, push } satisfies Record; // Callers: channels[n.channel].send(n.to, n.body) ``` ## Why - Everything about SMS lives in one object. Adding WhatsApp is one new object and one map entry. - Duplicated switches disappear, so no case can be forgotten in one of them. - Each variant can be tested alone. ## Steps 1. Find all conditionals that switch on the same discriminator. If there is only one, and it is short, stop here. 2. Define the interface from the methods those conditionals implement. 3. Create one class or object per case. Start with all methods delegating back to the old function, so behavior is unchanged. Run the tests. 4. Move one branch body into its class method. Run the tests. Repeat branch by branch. 5. Add the lookup (map, factory or constructor parameter) that picks the right implementation once, at the edge. 6. Replace each switch with a call through the interface. Delete the old function when it has no callers. Run the tests. ## Watch out - Do not apply this to a single switch that nothing else repeats. A switch with exhaustiveness checking (TypeScript `never`, Rust `match`, Java sealed types) is clear and safe. - If operations change more often than kinds, polymorphism makes things worse: each new operation touches every class. Keep a discriminated union and pattern matching, or use [Visitor](https://aipatterns.dev/design-patterns/visitor.md). - The switch moves, it does not vanish. Something still maps `"sms"` to the SMS object; keep that in one place. - When the kind of an object changes at runtime, use [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md) rather than subclasses. - No direct inverse technique exists. Collapsing polymorphic classes back into a switch is done manually with [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md) and [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md). ## Related - Fixes [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md) and the [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md) that repeated switches cause. - Prepared by [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md) or [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md). - Results in [Strategy](https://aipatterns.dev/design-patterns/strategy.md) or [State](https://aipatterns.dev/design-patterns/state.md). ## Agent checklist - [ ] The same discriminator is switched on in more than one place, or the single switch is long and growing. - [ ] Kinds change more often than operations. - [ ] The kind-to-implementation mapping exists in exactly one place. - [ ] Each branch moved separately with tests green between moves. --- # Introduce Null Object > Replace repeated null checks for a missing collaborator with an object that implements the same interface and does the safe default. - kind: technique - category: simplifying-conditional-expressions - aliases: Null Object - canonical: https://aipatterns.dev/refactoring/techniques/introduce-null-object - inspired-by: https://refactoring.guru/introduce-null-object ## Problem Many callers check whether an optional collaborator exists before using it, and each supplies the same fallback: skip the call, use zero, use "Guest". The checks are copied across the codebase, and one missing check is a crash. ## Solution **Create a class that implements the collaborator's interface with neutral behavior, and return or inject it wherever `null` was used.** Callers then call methods without checking. ## Before ```ts interface Analytics { track(event: string, props: Record): void; flush(): Promise; } class CheckoutService { constructor(private analytics: Analytics | null) {} async complete(order: Order): Promise { await this.payments.capture(order); if (this.analytics) { this.analytics.track("checkout_completed", { total: order.total }); } await this.emails.sendReceipt(order); if (this.analytics) { await this.analytics.flush(); } } } ``` ## After ```ts class NoopAnalytics implements Analytics { track(): void {} async flush(): Promise {} } class CheckoutService { constructor(private analytics: Analytics = new NoopAnalytics()) {} async complete(order: Order): Promise { await this.payments.capture(order); this.analytics.track("checkout_completed", { total: order.total }); await this.emails.sendReceipt(order); await this.analytics.flush(); } } ``` ## Why - The default behavior is defined once, inside the null object, instead of at every call site. - Callers become straight-line code. - Tests can pass the null object explicitly to switch off a dependency. ## Steps 1. Confirm that callers handle the missing case the same way everywhere. Note the fallback value or action. 2. Create the null class implementing the same interface, with methods that return the fallback. Run the tests. 3. Change the source of the value (constructor default, factory, repository lookup) to return the null object instead of `null`. Keep the old checks in place; they now see a non-null object and still work. Run the tests. 4. Remove the checks one call site at a time. Run the tests after each. 5. If some caller needs to know the object is "missing", add an `isNull()` method or keep that caller's check against the type, but try to avoid it. ## Watch out - Do not use a null object when absence is an error. A missing payment gateway should fail loudly; a silent no-op hides misconfiguration. - Do not use it when callers react differently to absence. Then the null object cannot pick one right default, and an `Option`/`undefined` with explicit handling is clearer. - In TypeScript, Kotlin, Swift or C#, optional chaining (`analytics?.track(...)`) removes most of the noise for a few call sites. Use a null object when the fallback is more than "do nothing" or appears in many places. - Make null objects stateless and shareable, or each instance quietly accumulates data. - No inverse technique exists; to undo, reinstate `null` and the checks. ## Related - Fixes [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md) in their `if (x == null)` form, and [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) in repeated fallbacks. - A special case of [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md). - A null object is often a do-nothing [Strategy](https://aipatterns.dev/design-patterns/strategy.md). - When absence must never happen, use [Introduce Assertion](https://aipatterns.dev/refactoring/techniques/introduce-assertion.md) instead. ## Agent checklist - [ ] Every replaced null check used the same fallback. - [ ] Absence is a legitimate state, not a configuration error. - [ ] The null object implements the full interface with neutral return values. - [ ] Remaining `null` returns from the source are gone, and tests pass. --- # Introduce Assertion > State an assumption the code already relies on as an explicit check that fails fast, instead of leaving it in a comment or in the reader's head. - kind: technique - category: simplifying-conditional-expressions - canonical: https://aipatterns.dev/refactoring/techniques/introduce-assertion - inspired-by: https://refactoring.guru/introduce-assertion ## Problem A block of code only works if some condition holds: a list is non-empty, a total is non-negative, a field was set earlier. Nothing checks it. When the assumption breaks, the code fails later and somewhere else, or silently produces wrong numbers. ## Solution **Add an assertion at the point where the assumption must be true.** It documents the requirement and stops execution with a clear message when it is violated. ## Before ```ts function allocatePayment(payment: Payment, invoices: Invoice[]): Allocation[] { // invoices are sorted oldest first and none are already paid const allocations: Allocation[] = []; let remaining = payment.amountCents; for (const inv of invoices) { const applied = Math.min(remaining, inv.balanceCents); allocations.push({ invoiceId: inv.id, amountCents: applied }); remaining -= applied; if (remaining === 0) break; } return allocations; } ``` ## After ```ts function assert(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(`Invariant failed: ${message}`); } function allocatePayment(payment: Payment, invoices: Invoice[]): Allocation[] { assert(invoices.every((inv) => inv.balanceCents > 0), "invoices must be unpaid"); assert( invoices.every((inv, i) => i === 0 || invoices[i - 1].dueDate <= inv.dueDate), "invoices must be sorted by due date", ); const allocations: Allocation[] = []; let remaining = payment.amountCents; for (const inv of invoices) { const applied = Math.min(remaining, inv.balanceCents); allocations.push({ invoiceId: inv.id, amountCents: applied }); remaining -= applied; if (remaining === 0) break; } return allocations; } ``` ## Why - The comment becomes executable and cannot go stale. - A violation fails at its source with a message that names the broken rule. - TypeScript's `asserts condition` signature also narrows types after the call. ## Steps 1. Find an assumption stated in a comment, in a name, or implied by the code (division by a count, `[0]` access, non-null `!`). 2. Write the assertion right where the assumption is first needed. Make the message name the rule. Run the tests; they should still pass because the assumption held. 3. If a test now fails, you found a real bug or a wrong assumption. Fix one of them before going on. 4. Delete the comment the assertion replaced. ## Watch out - Assertions are for programmer errors, not for input validation. User input, API payloads and file contents can legitimately be wrong; validate them and return errors or throw domain exceptions. - Do not assert things the type system already guarantees. - Keep the check cheap, or guard it behind a debug flag. The sort check above is O(n) on a hot path; drop it in production if profiling shows a cost. - Some runtimes strip assertions in production (`python -O`, Java without `-ea`). Do not put side effects inside them, and do not rely on them for security. - Removing an assertion has no named technique. Remove it only when the assumption is no longer needed. ## Related - Replaces explanatory [Comments](https://aipatterns.dev/refactoring/smells/comments.md). - When absence is a valid state rather than a broken assumption, use [Introduce Null Object](https://aipatterns.dev/refactoring/techniques/introduce-null-object.md). - For expected failure cases, see [Replace Error Code with Exception](https://aipatterns.dev/refactoring/techniques/replace-error-code-with-exception.md). ## Agent checklist - [ ] The asserted condition is an internal invariant, not untrusted input. - [ ] The assertion has no side effects. - [ ] Its message names the rule that was broken. - [ ] Existing tests still pass, proving the assumption held. --- # Rename Method > Change a method's name to say what it does, using a forwarding alias when callers cannot all be updated at once. - kind: technique - category: simplifying-method-calls - aliases: Change Function Declaration, Rename Function - canonical: https://aipatterns.dev/refactoring/techniques/rename-method - inspired-by: https://refactoring.guru/rename-method ## Problem A method's name does not describe what it does. It may describe how it works, use a stale term from an old version of the domain, or be so vague (`process`, `handle`, `doIt`) that every reader opens the body to find out. Comments at call sites try to compensate. ## Solution **Give the method a name that states its effect or result in domain terms, and update every caller.** For published APIs, keep the old name as a deprecated alias for a transition period. ## Before ```ts class SubscriptionService { // Moves the plan to the next tier and prorates the charge async chk(sub: Subscription, plan: PlanId): Promise { const credit = this.unusedCredit(sub); sub.plan = plan; return this.billing.charge(sub.customerId, this.priceOf(plan) - credit); } } await subscriptions.chk(sub, "pro"); ``` ## After ```ts class SubscriptionService { async upgradeWithProration(sub: Subscription, plan: PlanId): Promise { const credit = this.unusedCredit(sub); sub.plan = plan; return this.billing.charge(sub.customerId, this.priceOf(plan) - credit); } /** @deprecated Use upgradeWithProration. Remove after v4. */ chk(sub: Subscription, plan: PlanId): Promise { return this.upgradeWithProration(sub, plan); } } await subscriptions.upgradeWithProration(sub, "pro"); ``` ## Why - Code is read far more than written. A good name removes a trip into the body for every reader. - The explanatory comment becomes unnecessary. - Consistent names across classes reveal duplication and let you unify interfaces. ## Steps 1. Check whether the method is declared in a superclass, subclass or interface. A rename must cover every declaration in the hierarchy. 2. If your tooling supports a safe rename (an IDE or language server rename), use it and run the tests. You are done. 3. Otherwise, add a new method with the new name and copy the body into it. Make the old method delegate to the new one. Run the tests. 4. Update callers to the new name, a few at a time, running the tests as you go. 5. When nothing calls the old name, delete it. For a public API, mark it deprecated and delete it in a later release. ## Watch out - Text search misses dynamic calls: string-based dispatch, reflection, RPC method names, serialized event names, template bindings. Search for the string, not only the symbol. - Renaming a method that is part of a public package, REST route or message contract is a breaking change. Keep the alias. - Do not rename to describe the implementation (`loopAndSumCharges`). Name the outcome (`totalCharges`). - If you cannot find a good name, the method may do two things. Try [Separate Query from Modifier](https://aipatterns.dev/refactoring/techniques/separate-query-from-modifier.md) or [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) first. - Rename is its own inverse. ## Related - Helps with [Alternative Classes with Different Interfaces](https://aipatterns.dev/refactoring/smells/alternative-classes-with-different-interfaces.md) by aligning names, and removes [Comments](https://aipatterns.dev/refactoring/smells/comments.md) that explain bad names. - Often done together with [Add Parameter](https://aipatterns.dev/refactoring/techniques/add-parameter.md) or [Remove Parameter](https://aipatterns.dev/refactoring/techniques/remove-parameter.md). ## Agent checklist - [ ] All declarations in the class hierarchy and interfaces were renamed together. - [ ] String-based references (reflection, routes, configs, mocks) were searched for and updated. - [ ] Public APIs keep a deprecated alias. - [ ] The new name describes the result, not the mechanics. --- # Add Parameter > Give a method a new parameter when it needs data it cannot reach, migrating callers through a temporary overload or default value. - kind: technique - category: simplifying-method-calls - aliases: Change Function Declaration - canonical: https://aipatterns.dev/refactoring/techniques/add-parameter - inspired-by: https://refactoring.guru/add-parameter ## Problem A method needs information it does not have. It reaches for a global, reads a field that only some callers set, or silently assumes a default that is wrong for some of them. ## Solution **Add a parameter that carries the needed value and pass it from every caller.** Use a default value or an overload to migrate callers safely, then remove the default if every caller should choose explicitly. ## Before ```ts function formatInvoiceTotal(invoice: Invoice): string { // Always formatted for the US, even for EU customers return new Intl.NumberFormat("en-US", { style: "currency", currency: invoice.currency, }).format(invoice.totalCents / 100); } renderPdf({ total: formatInvoiceTotal(invoice) }); ``` ## After ```ts function formatInvoiceTotal(invoice: Invoice, locale: string): string { return new Intl.NumberFormat(locale, { style: "currency", currency: invoice.currency, }).format(invoice.totalCents / 100); } renderPdf({ total: formatInvoiceTotal(invoice, customer.locale) }); ``` ## Why - The dependency becomes explicit at the call site, so readers and tests can see and control it. - It avoids hidden coupling to globals or ambient state. ## Steps 1. Check whether the method can compute the value itself from what it already has. If so, do that instead; see [Replace Parameter with Method Call](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-method-call.md). 2. Add the parameter with a default equal to today's behavior (`locale = "en-US"`). Run the tests; nothing changes. 3. Update callers one at a time to pass the real value. Run the tests after each batch. 4. When every caller passes the argument, remove the default so new callers must decide. Run the type checker and tests. 5. For overridden methods, add the parameter to every override and interface declaration in the same step. ## Watch out - Do not add a parameter if the method already has many. Group related data with [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md) or pass the source object with [Preserve Whole Object](https://aipatterns.dev/refactoring/techniques/preserve-whole-object.md). - A boolean parameter that switches the method's behavior is a smell. Prefer two methods; see [Replace Parameter with Explicit Methods](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-explicit-methods.md). - In JavaScript and TypeScript, adding a positional parameter before existing optional ones silently shifts arguments. Add new parameters at the end, or switch to an options object. - Leaving a permanent default can hide callers that should have made a choice. - The inverse technique is [Remove Parameter](https://aipatterns.dev/refactoring/techniques/remove-parameter.md). ## Related - Inverse: [Remove Parameter](https://aipatterns.dev/refactoring/techniques/remove-parameter.md). - Alternatives when the list grows: [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md), [Preserve Whole Object](https://aipatterns.dev/refactoring/techniques/preserve-whole-object.md). - Can lead to [Long Parameter List](https://aipatterns.dev/refactoring/smells/long-parameter-list.md) if overused. ## Agent checklist - [ ] The method cannot derive the value from data it already holds. - [ ] The parameter list stays short (about four or fewer), or you used a parameter object. - [ ] Every override and interface declaration got the parameter. - [ ] Temporary defaults were removed once all callers were migrated, unless a default is genuinely correct. --- # Remove Parameter > Delete a parameter the method body no longer uses, updating every caller and override so the signature states only real dependencies. - kind: technique - category: simplifying-method-calls - aliases: Change Function Declaration - canonical: https://aipatterns.dev/refactoring/techniques/remove-parameter - inspired-by: https://refactoring.guru/remove-parameter ## Problem A method takes a parameter it ignores. It was used once, or added "in case", and now every caller has to compute and pass a value that changes nothing. Readers assume it matters. ## Solution **Remove the unused parameter from the method and from every call site.** ## Before ```ts function scheduleReminder( appointment: Appointment, leadMinutes: number, timezone: string, // unused since reminders moved to UTC channel: "sms" | "email", ): Job { const runAt = new Date(appointment.startsAt.getTime() - leadMinutes * 60_000); return queue.enqueue("reminder", { appointmentId: appointment.id, channel }, runAt); } scheduleReminder(appt, 60, clinic.timezone, "sms"); ``` ## After ```ts function scheduleReminder( appointment: Appointment, leadMinutes: number, channel: "sms" | "email", ): Job { const runAt = new Date(appointment.startsAt.getTime() - leadMinutes * 60_000); return queue.enqueue("reminder", { appointmentId: appointment.id, channel }, runAt); } scheduleReminder(appt, 60, "sms"); ``` ## Why - A signature is a contract. Every parameter in it claims the method depends on that value. - Callers stop computing values that are thrown away, which sometimes removes whole lookups. ## Steps 1. Confirm the parameter is unused in the body and in every override and implementation. A parameter unused in the base class may be used in a subclass. 2. If the method is overridden or implements an interface, change all declarations together. 3. If your tooling has a safe "change signature" refactoring, use it. Run the tests. 4. Otherwise create a new method without the parameter, make the old one delegate to it, and move callers over a few at a time. Run the tests after each batch. 5. Delete the old signature when nothing calls it. ## Watch out - Do not remove a parameter required by an interface, callback type or framework hook (Express middleware `next`, event handler signatures). Prefix it with `_` to signal it is intentionally unused. - Positional removal in JavaScript shifts later arguments. Callers passing values by position will send the wrong value into the wrong slot without a type error if the types match. Use the type checker, and update all callers in one change. - Public APIs need a deprecation period. Keep an overload that accepts and ignores the old argument. - Check whether computing the argument had side effects at the call site that callers still rely on. - The inverse technique is [Add Parameter](https://aipatterns.dev/refactoring/techniques/add-parameter.md). ## Related - Inverse: [Add Parameter](https://aipatterns.dev/refactoring/techniques/add-parameter.md). - Fixes [Long Parameter List](https://aipatterns.dev/refactoring/smells/long-parameter-list.md), [Speculative Generality](https://aipatterns.dev/refactoring/smells/speculative-generality.md) and [Dead Code](https://aipatterns.dev/refactoring/smells/dead-code.md). - When the value is still needed but derivable, use [Replace Parameter with Method Call](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-method-call.md). ## Agent checklist - [ ] The parameter is unused in every override and implementation. - [ ] It is not required by an external interface, callback type or framework. - [ ] All positional callers were updated in the same change. - [ ] Argument expressions removed from call sites had no side effects callers depend on. --- # Separate Query from Modifier > Split a method that both returns a value and changes state into a side-effect-free query and a separate command. - kind: technique - category: simplifying-method-calls - aliases: Command-Query Separation - canonical: https://aipatterns.dev/refactoring/techniques/separate-query-from-modifier - inspired-by: https://refactoring.guru/separate-query-from-modifier ## Problem A method returns information and also changes state. Callers who only want the information trigger the side effect anyway. Calling it twice, in a log line, a debugger watch or a test assertion, changes the outcome. ## Solution **Create one method that only answers the question and one that only performs the change. Callers who need both call the modifier, then the query.** ## Before ```ts class RateLimiter { private hits = new Map(); // Returns remaining requests in the window, and records this request remaining(apiKey: string, now: number): number { const windowStart = now - 60_000; const recent = (this.hits.get(apiKey) ?? []).filter((t) => t > windowStart); recent.push(now); this.hits.set(apiKey, recent); return Math.max(0, 100 - recent.length); } } if (limiter.remaining(key, Date.now()) === 0) throw new TooManyRequests(); res.setHeader("X-RateLimit-Remaining", limiter.remaining(key, Date.now())); // counts twice ``` ## After ```ts class RateLimiter { private hits = new Map(); recordHit(apiKey: string, now: number): void { const windowStart = now - 60_000; const recent = (this.hits.get(apiKey) ?? []).filter((t) => t > windowStart); recent.push(now); this.hits.set(apiKey, recent); } remaining(apiKey: string, now: number): number { const windowStart = now - 60_000; const recent = (this.hits.get(apiKey) ?? []).filter((t) => t > windowStart); return Math.max(0, 100 - recent.length); } } limiter.recordHit(key, Date.now()); const left = limiter.remaining(key, Date.now()); if (left === 0) throw new TooManyRequests(); res.setHeader("X-RateLimit-Remaining", left); ``` ## Why - Queries become safe to call any number of times, from anywhere, including tests and logs. - The call site shows where state changes, which makes ordering bugs visible. - The double-counting bug in the "before" code cannot be written by accident. ## Steps 1. Create the query: copy the method, remove every statement that changes state, and give it a name for the value it returns. Run the tests. 2. Make the original method call the query for its return value, so the computation exists once. Run the tests. 3. At each call site, replace `x = original()` with `modifier(); x = query();`. Run the tests after each site. 4. Change the original method to return `void` and rename it for the action. Run the tests. 5. Extract shared logic between the two (here, the window filter) with [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md). ## Watch out - Do not split operations that must be atomic under concurrency. `pop()`, `compareAndSet`, `INCR` in Redis and `nextval` in SQL return and mutate in one step on purpose. Splitting them creates a race between the check and the change. - Do not split when the "side effect" is invisible caching or memoization. That is still a pure query from the caller's view. - Splitting can double the cost of expensive work. Share an intermediate value or cache it if profiling shows a problem. - There is no inverse technique; merging query and modifier is usually a regression. ## Related - Shortens [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md) and clarifies intent. - Lets you use [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md) freely, since queries are safe. - Often followed by [Rename Method](https://aipatterns.dev/refactoring/techniques/rename-method.md) on the modifier. ## Agent checklist - [ ] The new query has no observable side effects. - [ ] Every call site that needed both now calls modifier then query, in that order. - [ ] The operation does not need to be atomic across threads, processes or requests. - [ ] Tests that asserted on the return value of the modifier now use the query. --- # Parameterize Method > Merge several methods that do the same thing with different literal values into one method that takes the value as a parameter. - kind: technique - category: simplifying-method-calls - aliases: Parameterize Function - canonical: https://aipatterns.dev/refactoring/techniques/parameterize-method - inspired-by: https://refactoring.guru/parameterize-method ## Problem You have several methods with almost identical bodies. They differ only in a number, a string or a threshold. A bug fix in one is easily missed in the others, and adding a new variant means copying a method again. ## Solution **Replace the family of methods with one method that takes the differing value as a parameter.** ## Before ```ts class RetentionReport { activeLast7Days(events: UsageEvent[], now: Date): number { const since = now.getTime() - 7 * DAY_MS; return new Set(events.filter((e) => e.at.getTime() >= since).map((e) => e.userId)).size; } activeLast30Days(events: UsageEvent[], now: Date): number { const since = now.getTime() - 30 * DAY_MS; return new Set(events.filter((e) => e.at.getTime() >= since).map((e) => e.userId)).size; } activeLast90Days(events: UsageEvent[], now: Date): number { const since = now.getTime() - 90 * DAY_MS; return new Set(events.filter((e) => e.at.getTime() > since).map((e) => e.userId)).size; } } ``` ## After ```ts class RetentionReport { activeUsers(events: UsageEvent[], now: Date, windowDays: number): number { const since = now.getTime() - windowDays * DAY_MS; const recent = events.filter((e) => e.at.getTime() >= since); return new Set(recent.map((e) => e.userId)).size; } } report.activeUsers(events, now, 30); ``` ## Why - One body to read, test and fix. - New variants (14 days, 365 days) need no new code. - Differences that were accidental become visible. The 90-day version used `>` instead of `>=`. ## Steps 1. Line up the methods and list every difference between their bodies. Each difference should be a value, not a different algorithm. 2. Resolve accidental differences first. Here, decide whether the boundary is inclusive, and fix the odd method out with a test that pins the chosen behavior. 3. Pick one method, add a parameter for the varying value, and replace the literal with it. Run the tests. 4. Change each sibling method to call the parameterized method with its literal. Run the tests. 5. Move callers to the parameterized method and delete the siblings, or keep them as named shortcuts if they read better at call sites. ## Watch out - Do not merge methods whose bodies differ in logic, not only values. The resulting method would need flags and branches. - If callers almost always pass one of a few fixed values, named methods can be clearer than a magic number. Keep thin wrappers like `activeLast30Days()` that delegate. - Validate the new parameter's range if the old literals were implicitly safe (no negative window, no zero divisor). - The inverse technique is [Replace Parameter with Explicit Methods](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-explicit-methods.md). ## Related - Fixes [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md). - Inverse: [Replace Parameter with Explicit Methods](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-explicit-methods.md). - Uses [Add Parameter](https://aipatterns.dev/refactoring/techniques/add-parameter.md). ## Agent checklist - [ ] The merged methods differed only in values, not in control flow. - [ ] Accidental differences between copies were resolved deliberately, with a test. - [ ] The parameter has a clear name and unit (`windowDays`, not `n`). - [ ] Invalid values for the new parameter are rejected or impossible. --- # Replace Parameter with Explicit Methods > Split a method that branches on a flag or enum argument into one clearly named method per value. - kind: technique - category: simplifying-method-calls - aliases: Remove Flag Argument - canonical: https://aipatterns.dev/refactoring/techniques/replace-parameter-with-explicit-methods - inspired-by: https://refactoring.guru/replace-parameter-with-explicit-methods ## Problem A method takes a parameter whose only job is to choose which branch runs. Call sites read like `setStatus(account, true, false)`, and the reader must open the method to learn what `true` means. Each branch has little in common with the others. ## Solution **Create a separate method for each value of the parameter, move the matching branch into it, and have callers call the explicit method.** ## Before ```ts function exportReport(report: Report, format: "csv" | "pdf"): Buffer { if (format === "csv") { const header = report.columns.join(","); const rows = report.rows.map((r) => r.map(csvEscape).join(",")); return Buffer.from([header, ...rows].join("\n")); } else { const doc = new PdfDocument({ size: "A4" }); doc.table(report.columns, report.rows); return doc.render(); } } const file = exportReport(monthly, "pdf"); ``` ## After ```ts function exportCsv(report: Report): Buffer { const header = report.columns.join(","); const rows = report.rows.map((r) => r.map(csvEscape).join(",")); return Buffer.from([header, ...rows].join("\n")); } function exportPdf(report: Report): Buffer { const doc = new PdfDocument({ size: "A4" }); doc.table(report.columns, report.rows); return doc.render(); } const file = exportPdf(monthly); ``` ## Why - Each call states what it does without a flag to decode. - Each method has one job and only the dependencies that job needs. - The type checker catches a missing case at compile time, rather than a runtime `else`. ## Steps 1. Confirm callers pass the parameter as a literal. If the value comes from data at runtime, stop; you would only move the branch to every caller. 2. Create one explicit method per value, with the body of its branch. Run the tests. 3. Make the original method delegate to the explicit methods. Run the tests. 4. Replace each call that passes a literal with a call to the matching explicit method. Run the tests. 5. Delete the original method when no callers remain. ## Watch out - Do not split when the value is decided at runtime (from a user setting, request header or database). Keep the dispatching method, or move the choice to a lookup map. That map is often the boundary where callers need a single entry point. - Do not split when the branches share most of their code and differ by a value. That is the case for [Parameterize Method](https://aipatterns.dev/refactoring/techniques/parameterize-method.md), the inverse technique. - Many values (ten export formats) make many methods. Consider [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md) with one exporter object per format. ## Related - Inverse: [Parameterize Method](https://aipatterns.dev/refactoring/techniques/parameterize-method.md). - Treats [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md) driven by flag arguments and shortens [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md). - Escalate to [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md) as cases grow. ## Agent checklist - [ ] Most callers pass the parameter as a literal. - [ ] The branches do different work, not the same work with different values. - [ ] Any caller with a runtime value still has a single dispatch point. - [ ] Boolean flag arguments were the first candidates. --- # Preserve Whole Object > Pass the object itself instead of several values pulled out of it, so the method signature stays stable as needs change. - kind: technique - category: simplifying-method-calls - canonical: https://aipatterns.dev/refactoring/techniques/preserve-whole-object - inspired-by: https://refactoring.guru/preserve-whole-object ## Problem A caller reads several fields from one object and passes them as separate arguments. When the method needs one more field, every call site changes. The repeated `x.a, x.b, x.c` pattern appears wherever the method is called. ## Solution **Pass the whole object and let the method read the fields it needs.** ## Before ```ts function isWithinBusinessHours( openHour: number, closeHour: number, timezone: string, at: Date, ): boolean { const hour = Number( new Intl.DateTimeFormat("en-US", { hour: "numeric", hourCycle: "h23", timeZone: timezone }).format(at), ); return hour >= openHour && hour < closeHour; } if (isWithinBusinessHours(clinic.openHour, clinic.closeHour, clinic.timezone, slot.start)) { book(slot); } ``` ## After ```ts function isWithinBusinessHours(clinic: Clinic, at: Date): boolean { const hour = Number( new Intl.DateTimeFormat("en-US", { hour: "numeric", hourCycle: "h23", timeZone: clinic.timezone }).format(at), ); return hour >= clinic.openHour && hour < clinic.closeHour; } if (isWithinBusinessHours(clinic, slot.start)) { book(slot); } ``` ## Why - Shorter call sites with no repeated field extraction. - When the rule later needs `clinic.holidays`, only the method changes. - The method now shows it is about a clinic, which often hints it belongs on `Clinic` itself. ## Steps 1. Add a parameter for the whole object, alongside the existing ones. Run the tests. 2. Inside the method, replace one extracted-value parameter with a read from the object. Run the tests. Repeat for each. 3. Remove the now unused value parameters, and the code at call sites that extracted them. Run the tests. 4. If the method only uses the object's data, consider [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to put it on the object's class. ## Watch out - Do not pass the whole object when the method lives in a lower layer that should not depend on the object's type. A pricing library should not import your ORM entity. Pass values, or define a narrow interface (`Pick`). - Passing a mutable object hands the method write access to all of it. Use a readonly type if it must not change anything. - If callers sometimes build the values from different sources (not always from one object), the separate parameters are honest. Consider [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md) instead. - No named inverse exists, but splitting a whole object back into values is the right move when you need to remove a dependency. ## Related - Fixes [Long Parameter List](https://aipatterns.dev/refactoring/smells/long-parameter-list.md) and [Data Clumps](https://aipatterns.dev/refactoring/smells/data-clumps.md). - Alternative: [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md) when no existing object holds the values. - Often followed by [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md). ## Agent checklist - [ ] Every call site took the passed values from the same object. - [ ] The method's module may depend on that object's type, or you used a narrow structural type. - [ ] The method cannot accidentally mutate the object it receives. - [ ] Extraction code at call sites was removed. --- # Replace Parameter with Method Call > Stop passing a value the method can obtain itself; have the method call the query directly and drop the parameter. - kind: technique - category: simplifying-method-calls - aliases: Replace Parameter with Query - canonical: https://aipatterns.dev/refactoring/techniques/replace-parameter-with-method-call - inspired-by: https://refactoring.guru/replace-parameter-with-method-call ## Problem A caller computes a value by calling a method and then passes the result into another method. The receiving method could have called that same query itself. The parameter only lengthens the signature and forces every caller to repeat the computation. ## Solution **Remove the parameter and have the method call the query internally.** ## Before ```ts class Shipment { constructor(private order: Order, private carrier: Carrier) {} dimensionalWeight(): number { return (this.order.lengthCm * this.order.widthCm * this.order.heightCm) / 5000; } label(billableWeight: number): ShippingLabel { return this.carrier.createLabel({ orderId: this.order.id, weightKg: billableWeight }); } } const billable = Math.max(shipment.dimensionalWeight(), order.weightKg); const label = shipment.label(billable); ``` ## After ```ts class Shipment { constructor(private order: Order, private carrier: Carrier) {} dimensionalWeight(): number { return (this.order.lengthCm * this.order.widthCm * this.order.heightCm) / 5000; } billableWeight(): number { return Math.max(this.dimensionalWeight(), this.order.weightKg); } label(): ShippingLabel { return this.carrier.createLabel({ orderId: this.order.id, weightKg: this.billableWeight() }); } } const label = shipment.label(); ``` ## Why - Callers cannot pass a wrong or stale value, because they no longer pass anything. - The rule for billable weight lives in one place, next to the data it reads. - Shorter signatures and simpler call sites. ## Steps 1. Confirm the value passed is always computed the same way, from data the method can reach. 2. If the computation is an expression, extract it into a query method on the receiving class. Run the tests. 3. Inside the method, replace uses of the parameter with a call to the query. Run the tests. 4. Remove the parameter with [Remove Parameter](https://aipatterns.dev/refactoring/techniques/remove-parameter.md), updating callers. Run the tests. ## Watch out - Do not do this when callers pass different values on purpose, such as an override for a manual weight. The parameter is then a real input. - Do not do this if it makes the method depend on something it should not, like a global clock, a database or a service locator. A parameter is easier to test than a hidden call to `Date.now()`. The inverse direction, passing the value in, is how you remove such dependencies. - The query must be cheap or cached if the method calls it repeatedly, and it must return the same value the caller would have passed at that moment. - The inverse is Replace Query with Parameter, performed with [Add Parameter](https://aipatterns.dev/refactoring/techniques/add-parameter.md): move the query out to callers and pass its result in. ## Related - Fixes [Long Parameter List](https://aipatterns.dev/refactoring/smells/long-parameter-list.md). - Uses [Remove Parameter](https://aipatterns.dev/refactoring/techniques/remove-parameter.md) and [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md). - Inverse direction: [Add Parameter](https://aipatterns.dev/refactoring/techniques/add-parameter.md). ## Agent checklist - [ ] All callers computed the argument the same way. - [ ] The method can reach the data without new dependencies on globals, time or I/O. - [ ] The query returns the same value at the new call time as at the old one. - [ ] Tests that passed custom values now set up the underlying data instead. --- # Introduce Parameter Object > Replace a group of parameters that always travel together with one object type, then move behavior on that group into it. - kind: technique - category: simplifying-method-calls - canonical: https://aipatterns.dev/refactoring/techniques/introduce-parameter-object - inspired-by: https://refactoring.guru/introduce-parameter-object ## Problem Several methods take the same cluster of parameters, such as `start` and `end`, or `amount` and `currency`. The values only make sense together. Callers can pass them in the wrong order, and the rules that link them (start before end) are rechecked or forgotten in each method. ## Solution **Create a type that holds the group, replace the parameters with one parameter of that type, and move the logic that belongs to the group into it.** ## Before ```ts function countBookings(roomId: string, from: Date, to: Date): Promise { if (from >= to) throw new RangeError("from must be before to"); return db.bookings.count({ roomId, startsAt: { gte: from, lt: to } }); } function revenue(roomId: string, from: Date, to: Date, currency: string): Promise { if (from >= to) throw new RangeError("from must be before to"); return db.payments.sum({ roomId, currency, paidAt: { gte: from, lt: to } }); } await revenue("r-12", new Date("2026-01-01"), new Date("2026-02-01"), "EUR"); ``` ## After ```ts class DateRange { constructor(readonly from: Date, readonly to: Date) { if (from >= to) throw new RangeError("from must be before to"); } toFilter(): { gte: Date; lt: Date } { return { gte: this.from, lt: this.to }; } } function countBookings(roomId: string, period: DateRange): Promise { return db.bookings.count({ roomId, startsAt: period.toFilter() }); } function revenue(roomId: string, period: DateRange, currency: string): Promise { return db.payments.sum({ roomId, currency, paidAt: period.toFilter() }); } await revenue("r-12", new DateRange(new Date("2026-01-01"), new Date("2026-02-01")), "EUR"); ``` ## Why - Signatures are shorter and say what they work with. - The invariant is enforced once, at construction, so an invalid range cannot exist. - The new type attracts behavior (`contains`, `overlaps`, `lengthInDays`) that was scattered. ## Steps 1. Create an immutable type with the grouped fields. Run the tests. 2. Add a parameter of the new type to one method, keeping the old parameters. Update its callers to pass both. Run the tests. 3. Inside the method, read from the new object instead of each old parameter, then remove the old parameters. Run the tests. 4. Repeat for every method that takes the same group. 5. Move duplicated logic about the group (validation, conversion) into the new type. Run the tests. ## Watch out - Do not group parameters that only coincidentally appear together in one method. The group should show up in several signatures or have rules linking its members. - Make the object immutable. A mutable parameter object shared between callers leads to surprising action at a distance. - If an existing object already holds the values, use [Preserve Whole Object](https://aipatterns.dev/refactoring/techniques/preserve-whole-object.md) instead of creating a new type. - In JavaScript and TypeScript, a plain options object (`{ from, to }`) is a lighter first step when you only need named arguments, not invariants. - Unwrapping a parameter object back into separate parameters has no named technique; do it only when the type stops pulling its weight. ## Related - Fixes [Long Parameter List](https://aipatterns.dev/refactoring/smells/long-parameter-list.md), [Data Clumps](https://aipatterns.dev/refactoring/smells/data-clumps.md) and [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md). - Alternative: [Preserve Whole Object](https://aipatterns.dev/refactoring/techniques/preserve-whole-object.md). - Next step: [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) into the new type, as with [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md). ## Agent checklist - [ ] The same group of parameters appears in more than one signature, or its members share a rule. - [ ] The new type is immutable and validates its invariants on construction. - [ ] Each method was migrated separately with tests green in between. - [ ] Duplicated validation for the group was moved into the type and removed from methods. --- # Remove Setting Method > Delete the setter for a field that should only be set at creation, and assign it in the constructor so it cannot change later. - kind: technique - category: simplifying-method-calls - canonical: https://aipatterns.dev/refactoring/techniques/remove-setting-method - inspired-by: https://refactoring.guru/remove-setting-method ## Problem A field is meant to be fixed once the object exists: an ID, a tenant, the currency of an invoice. A public setter lets any code change it later. Changing it can corrupt related data, and readers cannot tell whether the value is stable. ## Solution **Set the field through the constructor (or a factory), make it read-only, and delete the setter.** ## Before ```ts class Invoice { private currency = "USD"; private lines: InvoiceLine[] = []; setCurrency(currency: string): void { this.currency = currency; } getCurrency(): string { return this.currency; } addLine(line: InvoiceLine): void { this.lines.push(line); } } const invoice = new Invoice(); invoice.setCurrency("EUR"); invoice.addLine({ description: "Consulting", amountCents: 120_000 }); ``` ## After ```ts class Invoice { private lines: InvoiceLine[] = []; constructor(readonly currency: string) {} addLine(line: InvoiceLine): void { this.lines.push(line); } } const invoice = new Invoice("EUR"); invoice.addLine({ description: "Consulting", amountCents: 120_000 }); ``` ## Why - An invoice's currency can no longer change after lines priced in the old currency were added. - Readers and the compiler both know the field is stable, so no defensive re-checks are needed. - Immutable fields are safe to share and to use as map keys. ## Steps 1. Find every caller of the setter. Confirm each call happens right after construction, before the object is used. 2. Add a constructor parameter for the field. Have existing callers pass the value they used to set. Run the tests. 3. Remove the setter calls that follow construction. Run the tests. 4. Delete the setter and mark the field `readonly` (or `final`, `val`). The compiler will flag any remaining writes. Run the tests. 5. If the field is set internally in a later step, such as an ID assigned on save, keep that write private and named for its purpose rather than a general setter. ## Watch out - Do not remove a setter for state that legitimately changes, like an order's status. Replace it with intention-revealing methods (`ship()`, `cancel()`) instead. - Frameworks such as ORMs, serializers and dependency injectors may need setters or a no-argument constructor. Check whether they can use constructor binding or private field access before removing. - If construction now takes many arguments, use a factory or builder instead of a long constructor; see [Replace Constructor with Factory Method](https://aipatterns.dev/refactoring/techniques/replace-constructor-with-factory-method.md). - The inverse direction is adding a setter through [Encapsulate Field](https://aipatterns.dev/refactoring/techniques/encapsulate-field.md); do it only when the value must truly change. ## Related - Treats [Data Class](https://aipatterns.dev/refactoring/smells/data-class.md), where fields are freely writable. - Often paired with [Replace Constructor with Factory Method](https://aipatterns.dev/refactoring/techniques/replace-constructor-with-factory-method.md) and [Hide Method](https://aipatterns.dev/refactoring/techniques/hide-method.md). ## Agent checklist - [ ] All setter calls occurred during initialization only. - [ ] Frameworks that hydrate this class still work without the setter. - [ ] The field is marked `readonly` so the compiler blocks future writes. - [ ] State that does change goes through named domain methods, not a setter. --- # Hide Method > Reduce a method's visibility to private or protected when nothing outside the class needs it, shrinking the public surface. - kind: technique - category: simplifying-method-calls - canonical: https://aipatterns.dev/refactoring/techniques/hide-method - inspired-by: https://refactoring.guru/hide-method ## Problem A method is public, but only the class itself (or its subclasses) calls it. Other code may start depending on it, which ties the class's internals to outside callers and makes later changes risky. ## Solution **Make the method `private`, or `protected` if subclasses need it.** Use the narrowest visibility the language offers that still compiles. ## Before ```ts export class PasswordResetService { constructor(private tokens: TokenStore, private mail: Mailer) {} async requestReset(email: string): Promise { const token = this.generateToken(); await this.tokens.save(email, this.hashToken(token), Date.now() + 15 * 60_000); await this.mail.send(email, "reset", { link: `https://app.example/reset?t=${token}` }); } generateToken(): string { return crypto.randomBytes(32).toString("base64url"); } hashToken(token: string): string { return crypto.createHash("sha256").update(token).digest("hex"); } } ``` ## After ```ts export class PasswordResetService { constructor(private tokens: TokenStore, private mail: Mailer) {} async requestReset(email: string): Promise { const token = this.generateToken(); await this.tokens.save(email, this.hashToken(token), Date.now() + 15 * 60_000); await this.mail.send(email, "reset", { link: `https://app.example/reset?t=${token}` }); } private generateToken(): string { return crypto.randomBytes(32).toString("base64url"); } private hashToken(token: string): string { return crypto.createHash("sha256").update(token).digest("hex"); } } ``` ## Why - A smaller public interface is easier to learn and harder to misuse. - You can change or delete private methods without checking other modules. - For security-related helpers, hiding prevents callers from building tokens outside the reset flow. ## Steps 1. Search for every call to the method outside its class, including tests, other packages and string-based calls. 2. If there are none, change the visibility to `private`. Run the compiler and tests. 3. If only subclasses call it, use `protected`. 4. If tests call it directly, test through the public method instead, or move the logic into its own small class that is public and tested. 5. Repeat periodically. Methods often become hideable after other refactorings remove their external callers. ## Watch out - TypeScript `private` is compile-time only; JavaScript callers and `as any` still reach it. Use `#private` fields when runtime privacy matters. - Do not hide methods that are part of a published library API without a deprecation cycle. - Do not hide a method just to stop a test from calling it, if that leaves important logic untested. Extract it to a separate unit instead. - Interface implementations and framework hooks (lifecycle methods, decorators) must stay public. - The inverse is making the method public, which has no named technique; do it only for a real external caller. ## Related - Treats [Data Class](https://aipatterns.dev/refactoring/smells/data-class.md) and [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md) by narrowing what outsiders can touch. - Pairs with [Remove Setting Method](https://aipatterns.dev/refactoring/techniques/remove-setting-method.md) and [Encapsulate Field](https://aipatterns.dev/refactoring/techniques/encapsulate-field.md). ## Agent checklist - [ ] No caller outside the class (or its subclasses) exists, including in other packages and string lookups. - [ ] The method is not part of an interface, framework hook or published API. - [ ] Tests exercise the logic through public behavior. - [ ] Runtime privacy (`#`) was used where it matters for security. --- # Replace Constructor with Factory Method > Wrap object creation in a named static or module function when a plain constructor cannot express intent, pick a subtype, or reuse instances. - kind: technique - category: simplifying-method-calls - aliases: Replace Constructor with Factory Function - canonical: https://aipatterns.dev/refactoring/techniques/replace-constructor-with-factory-method - inspired-by: https://refactoring.guru/replace-constructor-with-factory-method ## Problem A constructor has to do more than a constructor can express well. Callers pass a flag to get different variants, several constructors differ only by argument order, or creation should sometimes return a cached instance or a subclass. `new` always returns a fresh object of exactly one class, with no name to explain which variant you get. ## Solution **Create a static method (or module-level function) that builds the object, give it a name that describes the variant, make the constructor private, and route callers through the factory.** ## Before ```ts class Discount { constructor( readonly kind: "percent" | "fixed", readonly value: number, readonly currency?: string, ) { if (kind === "percent" && (value <= 0 || value > 100)) throw new RangeError("bad percent"); if (kind === "fixed" && !currency) throw new Error("fixed discount needs a currency"); } } const promo = new Discount("percent", 15); const voucher = new Discount("fixed", 2000, "EUR"); ``` ## After ```ts class Discount { private constructor( readonly kind: "percent" | "fixed", readonly value: number, readonly currency?: string, ) {} static percent(rate: number): Discount { if (rate <= 0 || rate > 100) throw new RangeError("bad percent"); return new Discount("percent", rate); } static fixed(amountCents: number, currency: string): Discount { return new Discount("fixed", amountCents, currency); } } const promo = Discount.percent(15); const voucher = Discount.fixed(2000, "EUR"); ``` ## Why - Each way of creating the object has a name and only the arguments it needs. - Invalid combinations (a percent with a currency, a fixed amount without one) cannot be written. - The factory can later return a subclass, a cached instance or a result type, without changing callers. ## Steps 1. Create a static factory that calls the existing constructor with the same arguments. Run the tests. 2. Replace `new` calls with the factory, one module at a time. Run the tests. 3. Make the constructor `private` (or unexported). The compiler now finds any missed `new` calls. 4. Split the factory by variant, moving variant-specific validation into each one. Run the tests. ## Watch out - Do not add a factory when a plain constructor with clear parameters works. `new Point(x, y)` needs no `Point.of`. - Private constructors block subclassing in some languages and break frameworks that instantiate classes reflectively (ORMs, DI containers, serializers). Check before you hide the constructor. - A factory that grows a `switch` over a type string is just moving the problem. Use [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md) and let each subclass have its own factory, or a map. - The inverse, replacing a factory with a constructor, has no named technique. Do it when the factory adds nothing beyond `new`. ## Related - Treats [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md) and [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md) on type-code constructor arguments. - Supports [Remove Setting Method](https://aipatterns.dev/refactoring/techniques/remove-setting-method.md) by centralizing initialization. - Not the same as the [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) pattern, which uses subclass overrides. See also [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md). ## Agent checklist - [ ] The constructor needed a flag, had ambiguous overloads, or must return something other than a fresh instance. - [ ] Each factory has a name that describes the variant it creates. - [ ] No framework requires the constructor to stay public. - [ ] All `new` calls outside the class were migrated before the constructor went private. --- # Replace Error Code with Exception > Throw a typed exception instead of returning a sentinel such as -1 or false, so failures cannot be silently ignored by callers. - kind: technique - category: simplifying-method-calls - canonical: https://aipatterns.dev/refactoring/techniques/replace-error-code-with-exception - inspired-by: https://refactoring.guru/replace-error-code-with-exception ## Problem A method signals failure with a special return value: `-1`, `null`, `false` or a status string. Every caller must remember to check it. Callers that forget pass the sentinel on as if it were real data, and the error surfaces far away. ## Solution **Throw an exception that names the failure, and remove the error code from the return type.** Callers that can handle the failure catch it; the rest let it propagate. ## Before ```ts // Returns new balance, or -1 if insufficient funds, -2 if the account is frozen function withdraw(account: Account, amountCents: number): number { if (account.frozen) return -2; if (account.balanceCents < amountCents) return -1; account.balanceCents -= amountCents; return account.balanceCents; } const result = withdraw(acct, 5000); if (result === -1) return res.status(402).send("Insufficient funds"); if (result === -2) return res.status(423).send("Account frozen"); res.json({ balance: result }); ``` ## After ```ts class InsufficientFundsError extends Error {} class AccountFrozenError extends Error {} function withdraw(account: Account, amountCents: number): number { if (account.frozen) throw new AccountFrozenError(account.id); if (account.balanceCents < amountCents) throw new InsufficientFundsError(account.id); account.balanceCents -= amountCents; return account.balanceCents; } try { res.json({ balance: withdraw(acct, 5000) }); } catch (err) { if (err instanceof InsufficientFundsError) return res.status(402).send("Insufficient funds"); if (err instanceof AccountFrozenError) return res.status(423).send("Account frozen"); throw err; } ``` ## Why - A forgotten check no longer turns `-1` into a balance shown to the user. - The return type means only success, which simplifies every caller that does not handle the error itself. - Error handling can live at one boundary (an HTTP error middleware) instead of at every call. ## Steps 1. Decide which failures are exceptional. Create one exception class per failure that callers handle differently. 2. Find every caller and check how it handles each code. Add `try/catch` around callers that react to the code, mapping each exception to the old reaction. Keep the code checks too for now. Run the tests. 3. Change the method to throw instead of returning the code. Run the tests. 4. Remove the now unreachable code checks from callers. Run the tests. 5. Update the method's documentation and return type. ## Watch out - Do not use exceptions for expected outcomes that callers check every time, like "user not found" in a lookup. Return `undefined`, an `Option`, or a `Result` type instead. - In Go, Rust and other languages built around error values, returning an error is the idiom. Apply the spirit, typed errors instead of magic numbers, without throwing. - Exceptions are slower than returns in hot loops. Keep error codes or a precondition check there; see [Replace Exception with Test](https://aipatterns.dev/refactoring/techniques/replace-exception-with-test.md). - Async code must `await` the call inside the `try`, or the rejection escapes. - The inverse direction is [Replace Exception with Test](https://aipatterns.dev/refactoring/techniques/replace-exception-with-test.md), for failures callers can predict. ## Related - Inverse direction: [Replace Exception with Test](https://aipatterns.dev/refactoring/techniques/replace-exception-with-test.md). - Removes [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md) over return codes and [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) in repeated checks. - For programmer errors rather than runtime failures, use [Introduce Assertion](https://aipatterns.dev/refactoring/techniques/introduce-assertion.md). ## Agent checklist - [ ] The failure is exceptional for most callers, not a routine branch. - [ ] Each distinct handling path has its own exception type. - [ ] Every caller that reacted to a code now catches the matching exception before the method was changed. - [ ] Catch blocks rethrow unknown errors. --- # Replace Exception with Test > Check a predictable condition before the call instead of catching the exception it would throw, keeping exceptions for real failures. - kind: technique - category: simplifying-method-calls - aliases: Replace Exception with Precheck - canonical: https://aipatterns.dev/refactoring/techniques/replace-exception-with-test - inspired-by: https://refactoring.guru/replace-exception-with-test ## Problem Code calls something, catches the exception, and treats the catch block as a normal branch. The caller could have checked the condition first. Exceptions used as control flow hide the normal logic, are costly in loops, and swallow unrelated errors that share the same type. ## Solution **Test for the condition before the call, and handle the expected case in a plain conditional.** Keep the `catch` only for failures that cannot be predicted. ## Before ```ts function unitPrice(catalog: Map, sku: string, tier: PriceTier): number { try { return catalog.get(sku)!.prices[tier].amountCents; } catch (err) { if (err instanceof TypeError) { // SKU missing or tier not priced: fall back to list price return catalog.get(sku)?.listPriceCents ?? 0; } throw err; } } ``` ## After ```ts function unitPrice(catalog: Map, sku: string, tier: PriceTier): number { const product = catalog.get(sku); if (!product) return 0; const tierPrice = product.prices[tier]; if (!tierPrice) return product.listPriceCents; return tierPrice.amountCents; } ``` ## Why - The expected cases read as ordinary logic, not as error recovery. - A real `TypeError` from a bug elsewhere is no longer mistaken for a missing price. - No exception objects or stack traces are built on a common path. ## Steps 1. Identify the condition that causes the exception in the expected case. 2. Add a check for that condition before the risky call, returning what the `catch` block returned. Leave the `try/catch` in place. Run the tests. 3. Add a temporary log or assertion in the `catch` block for the expected case. If tests show it is never reached, the check is complete. 4. Remove the `catch` branch that handled the expected case. If nothing else is caught, remove the `try`. Run the tests. ## Watch out - Do not replace a `catch` with a check when the condition can change between check and use. File existence, record locks, network availability and unique-key inserts are race-prone. Try the operation and handle the error ("look before you leap" fails here). - Do not do this when checking is as expensive as doing, such as parsing a string to see whether it parses. Use a non-throwing variant (`safeParse`, `tryParse`) instead. - Python's idiom favors trying first and catching (`EAFP`) for many cases. Follow the language's convention where it differs. - The inverse technique is [Replace Error Code with Exception](https://aipatterns.dev/refactoring/techniques/replace-error-code-with-exception.md). ## Related - Inverse: [Replace Error Code with Exception](https://aipatterns.dev/refactoring/techniques/replace-error-code-with-exception.md). - Shortens [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md) by removing exception-handling noise. - For repeated "missing" checks, consider [Introduce Null Object](https://aipatterns.dev/refactoring/techniques/introduce-null-object.md). ## Agent checklist - [ ] The caught exception covered a predictable, routine condition. - [ ] The condition cannot change between the check and the call (no race). - [ ] The check is cheaper than the operation it guards. - [ ] Unexpected errors of the same type now propagate instead of being swallowed. --- # Consolidate Tools > Merge thin, overlapping tools into one task-shaped tool that returns only the fields the task reads, and describe when to use it - so consumers pick right the first time and stop chaining calls. - kind: technique - category: simplifying-method-calls - aliases: Merge Tools, Task-Shaped Tools - canonical: https://aipatterns.dev/refactoring/techniques/consolidate-tools - inspired-by: https://www.anthropic.com/engineering/writing-tools-for-agents ## Problem An agent-facing surface grew one tool per upstream endpoint. Answering one real question now takes a chain of calls - user, then transactions, then notes - each returning provider-shaped payloads with fields nobody reads. Consumers guess between overlapping tools, misfire, and burn tokens filtering noise down to the three values they wanted. ## Solution Group the tools by the tasks consumers actually perform, and replace each group with one task-shaped tool: it calls the upstream endpoints internally, joins what the task needs, and returns only the high-signal fields - names and values, not cursors and metadata. Write its description as a trigger rule: when to use it, and when not. ## Before ```ts // One tool per endpoint; the caller chains and filters. const tools = { getUser: (userId: string) => fetch(`${crm}/users/${userId}`).then((r) => r.json()), listTransactions: (userId: string) => fetch(`${crm}/users/${userId}/transactions`).then((r) => r.json()), listNotes: (userId: string) => fetch(`${crm}/users/${userId}/notes`).then((r) => r.json()), }; // Three calls, three payloads, a manual join: const user = await tools.getUser("u-9"); const txns = await tools.listTransactions("u-9"); const lastPurchase = (await tools.listNotes("u-9")).items[0]?.body; ``` ## After ```ts // One tool per task: it chains internally, the caller gets the answer. const tools = { // USE for "who is this customer / what did they last buy" questions. // NOT for editing customer records - use update_customer for that. getCustomerContext: async (userId: string) => { const [user, txns, notes] = await Promise.all([ fetch(`${crm}/users/${userId}`).then((r) => r.json()), fetch(`${crm}/users/${userId}/transactions`).then((r) => r.json()), fetch(`${crm}/users/${userId}/notes`).then((r) => r.json()), ]); return { name: user.display_name, email: user.primary_email, lastPurchaseCents: txns.items[0]?.amount_cents ?? 0, latestNote: notes.items[0]?.body ?? "", }; }, }; // One call, the answer, nothing to filter: const context = await tools.getCustomerContext("u-9"); ``` ## Why A consumer that dispatches on descriptions and pays per token needs the surface shaped for its tasks, not for the upstream's org chart. Task-shaped tools cut calls-per-task, cut response tokens to what is read, and make the right choice guessable from the description alone - which is what turns tool errors into rare events instead of a retry genre. The same instinct as [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md), applied across a network instead of a field access. ## Steps 1. Inventory the tools and the tasks: for each real consumer task, list which tools it chains today. Groups of two or more tools serving one task are your candidates. 2. Design the task-shaped tool first as its return type: the fields the task reads, named for the domain, defaults and pagination decided up front. If two candidate tasks need the same shape, they are one task. 3. Implement the consolidation so it calls the same upstream endpoints - no behavior change beyond the join, and errors from upstream surface with their origin intact. 4. Write the description as a trigger rule naming when to use and when not to, plus the one-line shape of the return. A description that says what it calls is a smell ([Thin Tool Wrapper](https://aipatterns.dev/refactoring/smells/thin-tool-wrapper.md)). 5. Switch every caller in the same change and delete the old tools. Overlapping tools left "for compatibility" re-create the guess the consolidation exists to remove. 6. Re-run the consumer's evaluation set: calls-per-task and output tokens should drop, task success should hold. Numbers or it did not happen. ## Watch out - Do not merge tools with different permission or billing characteristics - a read-everything tool can become a privilege escalation, and a "one big tool" that does everything fails the distinct-purpose rule from the other side. - Keep the upstream call semantics inside: consolidation must not turn three independent failures into one retried mega-call with surprise side effects. - If a raw variant genuinely serves its own distinct task (debugging, export), leave it alone and say so in its description - the goal is one tool per task, not fewest tools. - Consumers outside your control may still call removed tools; deprecate with an execution error naming the replacement before you delete. ## Related - [Thin Tool Wrapper](https://aipatterns.dev/refactoring/smells/thin-tool-wrapper.md): the smell this technique treats. - [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md): the same move one layer down - hiding `a.getB().getC()` behind the object that owns the chain. - [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md): the counterweight - consolidate when the consumer needs the task, expose the delegate when the consumer needs the control. ## Agent checklist - [ ] Groups were derived from real consumer tasks, not from endpoint adjacency. - [ ] The return type contains only fields at least one task reads. - [ ] The description is a trigger rule: when to use, when not, one-line return shape. - [ ] Every caller moved and the old tools deleted in the same change. - [ ] Calls-per-task and output tokens were measured before and after. --- # Long Method > A function that does several jobs in a row and has grown too long to read, name or test as one unit. - kind: smell - category: bloaters - aliases: God Function - canonical: https://aipatterns.dev/refactoring/smells/long-method - inspired-by: https://refactoring.guru/smells/long-method ## Signs - You have to scroll to see the whole function, or you need to keep more than a handful of local variables in your head at once. - Blank lines and comments split the body into "paragraphs" such as `// validate`, `// compute totals`, `// send email`. - The name is vague (`process`, `handle`, `run`) because no specific name covers everything it does. - Tests for it need large fixtures, and each test asserts on many unrelated outputs. - Diffs touch the same function for unrelated reasons: a tax change and a logging change land in the same 150 lines. ## Why it happens Adding a line to an existing function always feels cheaper than creating a new one. Each change is small and reasonable, so nobody notices the function crossing the line from "a step" to "a workflow". Local variables make it worse: once five temporaries are shared across the body, pulling out a piece means deciding what to pass in and out, so people keep appending instead. ## Example An order-placement handler that validates, prices, persists and notifies in one block. ```ts title="placeOrder.ts" async function placeOrder(req: OrderRequest, db: Db, mailer: Mailer): Promise { // validate if (!req.items.length) throw new Error("empty order"); for (const item of req.items) { if (item.qty <= 0) throw new Error(`bad qty for ${item.sku}`); } // price let subtotal = 0; for (const item of req.items) { const product = await db.products.get(item.sku); subtotal += product.priceCents * item.qty; } let discount = 0; if (req.coupon === "SPRING10") discount = Math.round(subtotal * 0.1); const tax = Math.round((subtotal - discount) * 0.08); const total = subtotal - discount + tax; // persist const id = await db.orders.insert({ ...req, subtotal, discount, tax, total }); // notify await mailer.send(req.email, "Order received", `Order ${id}: $${(total / 100).toFixed(2)}`); return id; } ``` After extracting each paragraph into a named function, the top-level function reads as a table of contents. ```ts title="placeOrder.ts" async function placeOrder(req: OrderRequest, db: Db, mailer: Mailer): Promise { assertValid(req); const price = await priceOrder(req, db.products); const id = await db.orders.insert({ ...req, ...price }); await sendConfirmation(mailer, req.email, id, price.total); return id; } function assertValid(req: OrderRequest): void { if (!req.items.length) throw new Error("empty order"); const bad = req.items.find((i) => i.qty <= 0); if (bad) throw new Error(`bad qty for ${bad.sku}`); } async function priceOrder(req: OrderRequest, products: ProductRepo): Promise { const subtotal = await subtotalOf(req.items, products); const discount = couponDiscount(req.coupon, subtotal); const tax = Math.round((subtotal - discount) * TAX_RATE); return { subtotal, discount, tax, total: subtotal - discount + tax }; } ``` ## Treatment - If the body has comment-headed paragraphs, use [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) on each one and name it after the comment. - If temporaries block extraction, first apply [Replace Temp with Query](https://aipatterns.dev/refactoring/techniques/replace-temp-with-query.md) so the extracted code can recompute the value. - If many parameters must flow into the extracted pieces, group them with [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md) or [Preserve Whole Object](https://aipatterns.dev/refactoring/techniques/preserve-whole-object.md). - If locals are so entangled that no clean cut exists, use [Replace Method with Method Object](https://aipatterns.dev/refactoring/techniques/replace-method-with-method-object.md): the locals become fields and every step becomes a method. - If conditionals dominate, apply [Decompose Conditional](https://aipatterns.dev/refactoring/techniques/decompose-conditional.md) to give each condition and branch a name. - If a loop does one self-contained job, extract the loop into its own function. ## Payoff - Each function has one name that tells the truth, so callers and reviewers read intent, not mechanics. - Small functions are testable in isolation and reusable elsewhere. - Duplicate fragments become visible once pieces have names. - Changes land in one small function, so diffs and merge conflicts shrink. ## When to ignore - The function is long but flat and linear, such as a configuration table, a route list or a generated mapping. Splitting it adds jumps without adding meaning. - A hot loop where a profiler shows that call overhead matters and the compiler does not inline. This is rare; measure first. - Test functions that set up one scenario end to end. Arrange-act-assert in one place can be clearer than helper soup. ## Agent checklist - [ ] Tests cover the function before you split it; add characterization tests if they don't. - [ ] Each extracted function has a name specific enough that the comment above the old paragraph can be deleted. - [ ] Extracted functions take the data they need as arguments and do not reach back into the caller's locals through closures or shared mutable state. - [ ] The original function now reads as a short sequence of steps at one level of abstraction. - [ ] Behavior is unchanged: the same tests pass with no edits to their assertions. --- # Large Class > A class with too many fields, methods or responsibilities, so every change risks breaking something unrelated inside it. - kind: smell - category: bloaters - aliases: God Class, God Object, Blob - canonical: https://aipatterns.dev/refactoring/smells/large-class - inspired-by: https://refactoring.guru/smells/large-class ## Signs - The class has dozens of fields, and groups of them share a prefix: `billingStreet`, `billingCity`, `shippingStreet`, `shippingCity`. - Some methods use one cluster of fields and never touch the others. - The file is the most frequently edited one in the repository, by people working on unrelated features. - Its name is generic (`Manager`, `Service`, `Helper`, `Context`) because nothing narrower fits. - Constructing it in a test needs a long list of collaborators, most of which the test never exercises. ## Why it happens A class starts small with a clear job. Each new feature needs "somewhere" to live, and the class that already has the relevant data is the obvious place. Nobody creates a new class for one method, so methods accumulate. After a while the class owns several responsibilities that only coincidentally share an instance. ## Example A user account class that also handles billing, notifications and audit logging. ```ts title="account.ts" class Account { id: string; email: string; displayName: string; cardLast4?: string; cardExpiry?: string; stripeCustomerId?: string; emailOptIn = true; smsOptIn = false; phone?: string; auditLog: string[] = []; rename(name: string) { this.displayName = name; this.audit(`rename ${name}`); } changeEmail(email: string) { this.email = email; this.audit(`email ${email}`); } attachCard(token: string, last4: string, expiry: string) { /* calls payment API */ } chargeMonthly(amountCents: number) { /* uses stripeCustomerId */ } isCardExpired(today: Date): boolean { /* parses cardExpiry */ return false; } notify(subject: string, body: string) { /* checks emailOptIn, smsOptIn, phone */ } audit(entry: string) { this.auditLog.push(`${new Date().toISOString()} ${entry}`); } } ``` The card fields and methods form one cluster, the notification preferences another. Each becomes its own class, and `Account` holds them. ```ts title="account.ts" class PaymentProfile { constructor(private customerId: string, private card?: Card) {} attachCard(token: string, card: Card) { /* ... */ } charge(amountCents: number) { /* ... */ } isExpired(today: Date): boolean { return this.card?.expiresBefore(today) ?? true; } } class ContactPreferences { constructor(public email: string, public phone?: string, public emailOptIn = true, public smsOptIn = false) {} channels(): Channel[] { /* ... */ return []; } } class Account { constructor( readonly id: string, public displayName: string, readonly contact: ContactPreferences, readonly payment: PaymentProfile, private readonly audit: AuditTrail, ) {} rename(name: string) { this.displayName = name; this.audit.record(this.id, `rename ${name}`); } } ``` ## Treatment - If a subset of fields and the methods that use them belong together, use [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md). - If some behavior applies only to certain instances (trial accounts, enterprise accounts), use [Extract Subclass](https://aipatterns.dev/refactoring/techniques/extract-subclass.md), or prefer composition if the variation is likely to grow. - If clients each use a different slice of the class, use [Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md) so each client depends on only what it calls. This is a first step that reduces coupling before you split the implementation. - If the large class is a UI component that also holds domain data, use [Duplicate Observed Data](https://aipatterns.dev/refactoring/techniques/duplicate-observed-data.md) to move the data into a domain object the view observes. - Split long methods inside the class first; the clusters become easier to see. ## Payoff - Each class can be understood, tested and changed without loading the others into your head. - Fewer merge conflicts, because unrelated features no longer edit the same file. - Constructors shrink and test setup becomes proportional to what is being tested. - Duplicated logic between clusters becomes visible and removable. ## When to ignore - A facade or composition root whose job is to wire many collaborators together. Its size reflects the number of collaborators, not tangled logic. - Generated code (ORM models, protocol stubs) that you do not edit by hand. - A data-heavy record that mirrors an external schema one to one. Splitting it would diverge from the schema you must match. ## Agent checklist - [ ] You identified field clusters by checking which methods read which fields, not by guessing from names. - [ ] Each extracted class has a name that describes one responsibility. - [ ] Callers that reached into the moved fields now go through the new class, and no field is left duplicated in both places. - [ ] Public API changes are either avoided (delegate from the old class) or every caller is updated in the same change. - [ ] Existing tests pass, and the new classes have their own focused tests. --- # Primitive Obsession > Domain concepts like money, emails, IDs or statuses are passed around as raw strings and numbers, so their rules get re-checked everywhere. - kind: smell - category: bloaters - aliases: Stringly Typed Code - canonical: https://aipatterns.dev/refactoring/smells/primitive-obsession - inspired-by: https://refactoring.guru/smells/primitive-obsession ## Signs - The same validation (`/^[^@]+@[^@]+$/`, `amount >= 0`, `code.length === 3`) appears in many places. - Function signatures read `(string, string, number, string)` and callers swap arguments without the compiler noticing. - Constants like `STATUS_ACTIVE = 2` or `"pending"` drive `if` and `switch` statements. - Money is a `number` of unknown unit (dollars or cents) and currency lives in a separate variable, or nowhere. - Arrays or tuples carry fixed positions with meaning: `row[0]` is the name, `row[3]` is the price. ## Why it happens Primitives are already there and cost nothing to type. Creating a class for "only an email" feels like overkill when the first use appears. Later the email gains normalization, the amount gains a currency, and the status gains transitions, but by then the primitive is threaded through dozens of signatures and nobody wants to touch them all. ## Example A transfer function where every argument is a primitive. ```ts title="transfer.ts" function transfer(fromIban: string, toIban: string, amount: number, currency: string): void { if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(fromIban)) throw new Error("bad from IBAN"); if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(toIban)) throw new Error("bad to IBAN"); if (amount <= 0 || !Number.isInteger(amount)) throw new Error("amount must be positive cents"); if (!["EUR", "USD", "GBP"].includes(currency)) throw new Error("unsupported currency"); ledger.post(fromIban, -amount, currency); ledger.post(toIban, amount, currency); } transfer("DE89370400440532013000", "FR1420041010050500013M02606", 2500, "EUR"); ``` With value objects, each rule lives in one constructor, and invalid values cannot reach `transfer` at all. ```ts title="transfer.ts" class Iban { private constructor(readonly value: string) {} static parse(raw: string): Iban { const v = raw.replace(/\s+/g, "").toUpperCase(); if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(v)) throw new Error(`bad IBAN: ${raw}`); return new Iban(v); } } type Currency = "EUR" | "USD" | "GBP"; class Money { constructor(readonly cents: number, readonly currency: Currency) { if (!Number.isInteger(cents)) throw new Error("cents must be an integer"); } negate(): Money { return new Money(-this.cents, this.currency); } } function transfer(from: Iban, to: Iban, amount: Money): void { if (amount.cents <= 0) throw new Error("transfer amount must be positive"); ledger.post(from, amount.negate()); ledger.post(to, amount); } ``` ## Treatment - If a single primitive carries rules or formatting (email, IBAN, SKU, percentage), use [Replace Data Value with Object](https://aipatterns.dev/refactoring/techniques/replace-data-value-with-object.md). - If several primitives always travel together (amount and currency, lat and lng), use [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md), or [Preserve Whole Object](https://aipatterns.dev/refactoring/techniques/preserve-whole-object.md) when the caller already has the owning object. - If a coded value only identifies a kind and has no behavior, use [Replace Type Code with Class](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-class.md), or a string literal union or enum in TypeScript. - If the code value changes behavior, use [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md), or [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md) when the type can change at runtime. - If an array is used as a record with positional meaning, use [Replace Array with Object](https://aipatterns.dev/refactoring/techniques/replace-array-with-object.md). ## Payoff - Validation and formatting live in one place, so they cannot drift. - The type system catches swapped arguments and unit mix-ups. - New behavior (currency conversion, IBAN masking) has an obvious home. - Signatures document themselves: `transfer(from: Iban, to: Iban, amount: Money)`. ## When to ignore - A value that has no rules, no formatting and no unit, such as a free-text comment. - Throwaway scripts and one-off migrations. - Hot paths where allocating an object per value matters. In TypeScript, a branded type (`string & { __brand: "Iban" }`) gives compile-time safety with no runtime cost; in Go and Rust, a named type over the primitive is free. ## Agent checklist - [ ] The value object validates in its constructor or factory, and there is no public way to build an invalid one. - [ ] Value objects are immutable and compare by value, not identity. - [ ] Parsing happens at the system boundary (HTTP handler, file reader), and inner code receives the typed value. - [ ] Duplicate validation scattered through callers is deleted, not left alongside the new type. - [ ] Serialization (JSON, database columns) still produces the same wire format, verified by tests. --- # Long Parameter List > A function takes so many parameters that callers mix up their order and every new option changes every call site. - kind: smell - category: bloaters - aliases: Too Many Parameters - canonical: https://aipatterns.dev/refactoring/smells/long-parameter-list - inspired-by: https://refactoring.guru/smells/long-parameter-list ## Signs - The signature has more than four or five parameters, especially several of the same type in a row. - Call sites pass `null`, `undefined` or `false` for most arguments to reach the one they care about. - Boolean flags appear in the middle of the list: `render(doc, true, false, true)`. - The caller pulls several fields off one object only to pass them in separately. - Adding an option means touching every caller, even those that never use it. ## Why it happens Parameters are the honest alternative to global state, so developers add them. Then each new requirement adds one more. Sometimes several algorithms were merged into one function and the flags pick between them. Sometimes the function was decoupled from an object by unpacking that object's fields at every call site. ## Example A report export function that grew a flag per feature request. ```ts title="exportReport.ts" function exportReport( reportId: string, from: Date, to: Date, format: "csv" | "pdf", includeCharts: boolean, timezone: string, locale: string, recipientEmail: string | null, compress: boolean, ): Promise { /* ... */ } exportReport(r.id, r.from, r.to, "pdf", true, user.timezone, user.locale, null, false); ``` Grouping the date range, taking the user as a whole, and splitting the "deliver" concern out leaves a short signature. ```ts title="exportReport.ts" interface DateRange { from: Date; to: Date } interface ExportOptions { format: "csv" | "pdf"; includeCharts?: boolean; compress?: boolean; } function exportReport(reportId: string, range: DateRange, viewer: User, options: ExportOptions): Promise { const { timezone, locale } = viewer; /* ... */ } async function emailReport(blob: Blob, to: string): Promise { /* ... */ } const pdf = await exportReport(r.id, r.range, user, { format: "pdf", includeCharts: true }); ``` ## Treatment - If the function can get a value itself from an object it already has access to, use [Replace Parameter with Method Call](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-method-call.md). - If the caller unpacks several fields from one object, use [Preserve Whole Object](https://aipatterns.dev/refactoring/techniques/preserve-whole-object.md). - If several parameters always appear together and form a concept (range, address, coordinates), use [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md). - If a boolean or enum parameter switches between distinct behaviors, use [Replace Parameter with Explicit Methods](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-explicit-methods.md): `exportCsv()` and `exportPdf()`. - If the remaining parameters are optional tuning knobs, collect them in an options object with defaults. In languages with named arguments (Python, Kotlin, C#), keyword arguments solve the readability half of the problem. ## Payoff - Call sites become readable without an IDE showing parameter names. - Swapped-argument bugs disappear when distinct concepts have distinct types. - New optional settings no longer ripple through every caller. - Hidden concepts (`DateRange`) get a name and can grow behavior, such as `contains(date)`. ## When to ignore - Passing a whole object would create a dependency you deliberately avoided, for example passing a full `User` into a low-level formatting module. A few primitives keep the module decoupled. - Pure math or geometry functions where the parameters are the natural inputs: `lerp(a, b, t)`, `clamp(value, min, max)`. - Constructors used with dependency injection, where each parameter is a distinct collaborator. A long list there points to [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md), not to the list itself. ## Agent checklist - [ ] Each grouped parameter object represents a real concept with a name a domain expert would recognize, not a bag called `Params`. - [ ] Boolean flags that select behavior are replaced by separate functions or an enum, not hidden inside an options object. - [ ] Every call site is updated in the same change, or an overload or adapter keeps old callers working with a deprecation note. - [ ] Defaults for optional settings are defined once, at the function, not repeated at call sites. - [ ] Tests pass with no change to their expected outputs. --- # Data Clumps > The same group of variables travels together through fields and parameters, signaling a missing object that should own them. - kind: smell - category: bloaters - canonical: https://aipatterns.dev/refactoring/smells/data-clumps - inspired-by: https://refactoring.guru/smells/data-clumps ## Signs - Three or more parameters appear side by side in several signatures: `(street, city, postalCode, country)`. - Several classes have the same set of fields with the same names, often copy-pasted. - Validation or formatting for the group is repeated: building a one-line address string happens in four places. - Deleting one member of the group would make the others meaningless. That is the test: if `lat` without `lng` is nonsense, they are a clump. ## Why it happens The first time two values travel together, adding a type for them seems unnecessary. The second and third times, copying the existing signature is faster than designing one. Database tables and form layouts also push the group apart into separate columns and inputs, and code that mirrors them inherits the split. ## Example A delivery scheduling module where a geographic point and a time window are always passed as loose values. ```ts title="dispatch.ts" function distanceKm(lat1: number, lng1: number, lat2: number, lng2: number): number { /* haversine */ return 0; } function canDeliver( depotLat: number, depotLng: number, stopLat: number, stopLng: number, windowStart: Date, windowEnd: Date, now: Date, ): boolean { if (now < windowStart || now > windowEnd) return false; return distanceKm(depotLat, depotLng, stopLat, stopLng) < 40; } class Stop { lat = 0; lng = 0; windowStart = new Date(); windowEnd = new Date(); } ``` Extracting `GeoPoint` and `TimeWindow` shortens every signature and gives the behavior a home. ```ts title="dispatch.ts" class GeoPoint { constructor(readonly lat: number, readonly lng: number) {} distanceKmTo(other: GeoPoint): number { /* haversine */ return 0; } } class TimeWindow { constructor(readonly start: Date, readonly end: Date) { if (end < start) throw new Error("window ends before it starts"); } contains(t: Date): boolean { return t >= this.start && t <= this.end; } } class Stop { constructor(readonly location: GeoPoint, readonly window: TimeWindow) {} } function canDeliver(depot: GeoPoint, stop: Stop, now: Date): boolean { return stop.window.contains(now) && depot.distanceKmTo(stop.location) < 40; } ``` ## Treatment - If the clump appears as fields in a class, use [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) to move them into their own type. - If the clump appears in parameter lists, use [Introduce Parameter Object](https://aipatterns.dev/refactoring/techniques/introduce-parameter-object.md). - If the caller already holds an object containing the clump, use [Preserve Whole Object](https://aipatterns.dev/refactoring/techniques/preserve-whole-object.md) and pass that object. - After the type exists, look for functions that operate only on the clump (formatting, distance, overlap checks) and move them onto it with [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md). ## Payoff - Signatures shrink and the concept gets a name the team can use. - Invariants (end after start, latitude in range) are enforced once. - Duplicate helper code collapses into methods on the new type. - Future changes, such as adding altitude or a time zone, touch one type instead of every signature. ## When to ignore - The values coincide but do not form a concept: `width` and `retryCount` passed together to one function by accident. - Across a module boundary where you intentionally keep the API to primitives, such as a public HTTP or FFI interface. Introduce the type on each side of the boundary instead. - Performance-critical numeric code that keeps parallel arrays for cache locality. ## Agent checklist - [ ] You confirmed the members belong together by applying the "delete one, do the others still make sense" test. - [ ] The new type is immutable unless the domain requires mutation. - [ ] Every occurrence of the clump is replaced, not only the one you happened to notice; search for the field names across the codebase. - [ ] Behavior that operated only on the clump moved onto the new type. - [ ] Persistence mapping (ORM columns, JSON keys) is unchanged or migrated explicitly, and tests confirm the stored shape. --- # Divergent Change > One class gets edited for many unrelated reasons, so every kind of change lands in the same file and collides with every other kind. - kind: smell - category: change-preventers - canonical: https://aipatterns.dev/refactoring/smells/divergent-change - inspired-by: https://refactoring.guru/smells/divergent-change ## Signs - The commit history of one file mixes unrelated topics: a tax rule change, a new export format, a database column rename. - You can describe the class only with "and": it "validates invoices *and* renders PDFs *and* writes to Postgres". - Different teams or people edit the same class for different reasons, and their changes conflict in review. - Methods form clusters that never call each other and touch disjoint fields. - Adding a new database requires changing the same methods you'd change to add a new currency. ## Why it happens A class starts with one job. Each later feature needs "somewhere to go", and the class that already holds the data is the closest home. Nobody decides to merge persistence, formatting and business rules; it accretes one reasonable-looking pull request at a time. Copy-paste of an existing method inside the class reinforces the cluster. Divergent Change is the mirror of [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md): here one class absorbs many kinds of change; there one kind of change is spread across many classes. ## Example An `Invoice` class changes whenever finance updates tax rules, whenever the PDF layout changes, and whenever storage moves. ```ts title="invoice.ts" class Invoice { constructor( public id: string, public lines: { sku: string; qty: number; unitCents: number }[], public region: "EU" | "US", ) {} // Reason to change #1: tax policy taxCents(): number { const net = this.netCents(); return this.region === "EU" ? Math.round(net * 0.21) : Math.round(net * 0.07); } netCents(): number { return this.lines.reduce((sum, l) => sum + l.qty * l.unitCents, 0); } // Reason to change #2: document layout toPdfLines(): string[] { return [ `Invoice ${this.id}`, ...this.lines.map((l) => `${l.sku} x${l.qty} ${(l.unitCents / 100).toFixed(2)}`), `Tax: ${(this.taxCents() / 100).toFixed(2)}`, ]; } // Reason to change #3: storage async save(db: { query(sql: string, args: unknown[]): Promise }): Promise { await db.query("INSERT INTO invoices (id, region, net) VALUES ($1, $2, $3)", [ this.id, this.region, this.netCents(), ]); } } ``` After [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md), each reason to change has its own home: ```ts title="invoice.ts" class Invoice { constructor( readonly id: string, readonly lines: { sku: string; qty: number; unitCents: number }[], readonly region: "EU" | "US", ) {} netCents(): number { return this.lines.reduce((sum, l) => sum + l.qty * l.unitCents, 0); } } class TaxPolicy { taxCents(inv: Invoice): number { const rate = inv.region === "EU" ? 0.21 : 0.07; return Math.round(inv.netCents() * rate); } } class InvoicePdf { constructor(private readonly tax: TaxPolicy) {} lines(inv: Invoice): string[] { /* layout only */ return [`Invoice ${inv.id}`]; } } class InvoiceRepository { async save(inv: Invoice): Promise { /* storage only */ } } ``` ## Treatment - If the class has clusters of methods and fields that change together, use [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) to move each cluster out. - If one method sits in the wrong cluster, use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to put it next to the data it uses. - If several classes share the same divergent behavior, use [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md) to give the shared part one home. - If the divergence is a variant of the object (a special kind of invoice), use [Extract Subclass](https://aipatterns.dev/refactoring/techniques/extract-subclass.md). - Split along the axes that actually change in your history, not along every axis you can imagine. ## Payoff - Each change touches one small class, so diffs are focused and reviews are fast. - Merge conflicts between unrelated features drop. - Tests for tax rules no longer need a database or a PDF renderer. ## When to ignore - The class is small and the "reasons to change" are each one short method. Splitting a 60-line class into four files adds navigation cost for no gain. - The axes have not actually changed in practice. Wait for the second real change along an axis before extracting it. - The code is a throwaway script or a prototype that will be rewritten. ## Agent checklist - [ ] You named each distinct reason the class changes, backed by commit history or the current task, not speculation. - [ ] Each extracted class owns one reason to change and the data that reason needs. - [ ] Callers were updated, and no method is left delegating back and forth between the old and new classes. - [ ] Existing tests pass unchanged before you add new focused tests per class. - [ ] You did not split along axes that have never changed. --- # Shotgun Surgery > One logical change forces small edits in many classes, so one missed edit ships an inconsistent system. - kind: smell - category: change-preventers - canonical: https://aipatterns.dev/refactoring/smells/shotgun-surgery - inspired-by: https://refactoring.guru/smells/shotgun-surgery ## Signs - A single requirement ("add a `CAD` currency", "rename the `trial` plan") produces a diff that touches eight files with two lines each. - You find the places to change with a global text search, not by following the code. - Bugs appear because one of the many copies was missed. - The same small decision (a threshold, a format, a list of allowed values) is encoded in several classes. - Several classes each hold a fragment of one concept, and none of them owns it. ## Why it happens Responsibility for one concept got split across layers, often by a rule like "controllers format, services validate, repositories map" applied mechanically. Each layer then encodes its own copy of the same knowledge. Copy-paste of a small snippet into several places has the same effect. Over-eager splitting to cure [Divergent Change](https://aipatterns.dev/refactoring/smells/divergent-change.md) can also cause it. ## Example A SaaS app supports subscription plans. Adding a plan means editing the validator, the pricing code, the feature gate and the email template. ```ts title="plans.ts" // validation.ts export function isValidPlan(plan: string): boolean { return plan === "free" || plan === "pro" || plan === "team"; } // pricing.ts export function monthlyPriceCents(plan: string): number { if (plan === "pro") return 1200; if (plan === "team") return 4900; return 0; } // features.ts export function maxProjects(plan: string): number { return plan === "team" ? 100 : plan === "pro" ? 20 : 3; } // emails.ts export function planLabel(plan: string): string { return plan === "team" ? "Team" : plan === "pro" ? "Pro" : "Free"; } ``` Adding `"enterprise"` needs four edits in four files. After moving the knowledge into one place: ```ts title="plans.ts" export const PLANS = { free: { label: "Free", priceCents: 0, maxProjects: 3 }, pro: { label: "Pro", priceCents: 1200, maxProjects: 20 }, team: { label: "Team", priceCents: 4900, maxProjects: 100 }, } as const; export type PlanId = keyof typeof PLANS; export function isValidPlan(plan: string): plan is PlanId { return plan in PLANS; } // Callers read PLANS[plan].priceCents, PLANS[plan].maxProjects, PLANS[plan].label. ``` A new plan is now one entry, and the type checker flags any caller that cannot handle it. ## Treatment - If behavior about one concept is spread across classes, use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to gather it in the class that owns the concept's data. - If the data itself is scattered, use [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) first, then move the methods that use it. - If gathering leaves behind classes that now do almost nothing, use [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md) to fold them away. - If the scattered pieces are copies of the same logic, treat it as [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) as well. - Where the scattered code is a type switch, consider [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md) or a lookup table so a new case is one addition. ## Payoff - One change, one place. Reviews show the whole change at once. - Missed-copy bugs disappear. - The concept gets a name and a home that new contributors can find. ## When to ignore - The edits are spread across genuinely separate deployables (a client and a server, two services) where sharing code would couple release cycles. Use a shared schema or contract test instead. - The repeated knowledge is trivial and stable, such as an HTTP status code. - Generated code (protobuf, ORM models) produces the spread. Fix the generator input, not the output. ## Agent checklist - [ ] You searched for every place that encodes the concept, including tests, templates and config. - [ ] The concept now has one owning module, and other code reads from it. - [ ] Adding a new case requires one edit, and you verified that with a dry run or the type checker. - [ ] You did not create a new cross-service dependency to centralize code. - [ ] All tests pass after each move, not only at the end. --- # Parallel Inheritance Hierarchies > Every new subclass in one hierarchy forces a matching subclass in another, so two trees must always grow in lockstep. - kind: smell - category: change-preventers - canonical: https://aipatterns.dev/refactoring/smells/parallel-inheritance-hierarchies - inspired-by: https://refactoring.guru/smells/parallel-inheritance-hierarchies ## Signs - Two class trees share name prefixes: `EmailNotification` / `EmailNotificationRenderer`, `SmsNotification` / `SmsNotificationRenderer`. - Adding a subclass to one tree fails at runtime or compile time until you add its twin to the other. - A factory or map pairs each class in tree A with exactly one class in tree B. - Code downcasts or checks `instanceof` to find the matching partner. ## Why it happens It starts as sensible separation: data in one class, the code that acts on it in another. With two or three variants it looks clean. Each new variant then adds a pair, and the pairing rule lives only in developers' heads or in a registry. It is a special case of [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md), where the scattered edit is a whole new class. ## Example A notification system has one tree for messages and another for senders. ```ts title="notifications.ts" abstract class Notification { constructor(readonly userId: string, readonly text: string) {} } class EmailNotification extends Notification { constructor(userId: string, text: string, readonly subject: string) { super(userId, text); } } class SmsNotification extends Notification {} abstract class Sender { abstract send(n: N): Promise; } class EmailSender extends Sender { async send(n: EmailNotification) { /* SMTP with n.subject */ } } class SmsSender extends Sender { async send(n: SmsNotification) { /* SMS gateway, 160 chars */ } } function senderFor(n: Notification): Sender { if (n instanceof EmailNotification) return new EmailSender(); if (n instanceof SmsNotification) return new SmsSender(); throw new Error("no sender"); // adding PushNotification breaks here } ``` Collapse the pairing by moving the sending behavior onto the notification and passing the transport in: ```ts title="notifications.ts" interface Transports { smtp(to: string, subject: string, body: string): Promise; sms(to: string, body: string): Promise; } abstract class Notification { constructor(readonly userId: string, readonly text: string) {} abstract send(t: Transports): Promise; } class EmailNotification extends Notification { constructor(userId: string, text: string, readonly subject: string) { super(userId, text); } send(t: Transports) { return t.smtp(this.userId, this.subject, this.text); } } class SmsNotification extends Notification { send(t: Transports) { return t.sms(this.userId, this.text.slice(0, 160)); } } ``` A push notification is now one class, not two plus a registry edit. ## Treatment - If the second hierarchy's classes hold behavior for exactly one class in the first, use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to put that behavior on its partner, then delete the second tree. - If the partner classes also carry data, use [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) before moving methods. - If the two trees must stay separate (for example, domain objects that must not depend on infrastructure), keep one hierarchy and make the other a set of instances or functions keyed by type, so a new variant needs one registration, not a new subclass. - If both trees vary independently (N shapes times M renderers), the pairing is real; use [Bridge](https://aipatterns.dev/design-patterns/bridge.md) so each tree grows on its own. - If you need many operations over a stable set of types, [Visitor](https://aipatterns.dev/design-patterns/visitor.md) makes the pairing explicit and type-checked. ## Payoff - A new variant is one class in one place. - No registry, no `instanceof` dispatch and no runtime "missing partner" errors. - The pairing rule is enforced by the type system rather than by memory. ## When to ignore - The separation is a deliberate layering boundary, such as domain types that must not import email or SMS libraries. Accept the pairs, and add a test that fails when a partner is missing. - The trees vary independently and the cross product is real. That is a Bridge, not a smell. - There are two or three stable variants and no new ones in sight. ## Agent checklist - [ ] You confirmed the trees are paired one-to-one, not independently varying. - [ ] Moving behavior across does not make a domain class depend on infrastructure it should not know. - [ ] Every `instanceof` dispatch or partner registry tied to the pairing was removed or reduced. - [ ] A test covers each variant's behavior after the merge. - [ ] If you kept both trees on purpose, a test fails when a new variant lacks its partner. --- # Feature Envy > A method reads and computes on another object's data more than its own. Move the logic to where the data lives. - kind: smell - category: couplers - canonical: https://aipatterns.dev/refactoring/smells/feature-envy - inspired-by: https://refactoring.guru/smells/feature-envy ## Signs - A method calls several getters on one other object and few or none on `this`. - The method's result depends almost entirely on another type's fields. - You see the same chain of reads on that other object in several places. - The method's name mentions the other type: `computeInvoiceTotal` living in `ReportService`. - Changing the other type's fields forces edits in this method, never the reverse. ## Why it happens - Data and behavior started in different places. Someone made a plain record type and wrote the logic in a service next to the code that first needed it. - Moving a field to a new class left behind the methods that used it. - A layered architecture rule ("models hold data, services hold logic") was applied mechanically, so every calculation sits away from its inputs. - Each new caller copied the calculation instead of asking the data owner for it. ## Example A shipping service computes everything about a parcel from the parcel's own fields. ```ts title="before.ts" class Parcel { constructor( public lengthCm: number, public widthCm: number, public heightCm: number, public weightKg: number, ) {} } class ShippingService { billableWeight(p: Parcel): number { const volumetric = (p.lengthCm * p.widthCm * p.heightCm) / 5000; return Math.max(p.weightKg, volumetric); } isOversized(p: Parcel): boolean { return Math.max(p.lengthCm, p.widthCm, p.heightCm) > 120; } quote(p: Parcel, ratePerKg: number): number { return Math.ceil(this.billableWeight(p)) * ratePerKg; } } ``` `billableWeight` and `isOversized` touch only `Parcel`. They belong there. ```ts title="after.ts" class Parcel { constructor( readonly lengthCm: number, readonly widthCm: number, readonly heightCm: number, readonly weightKg: number, ) {} billableWeight(): number { const volumetric = (this.lengthCm * this.widthCm * this.heightCm) / 5000; return Math.max(this.weightKg, volumetric); } isOversized(): boolean { return Math.max(this.lengthCm, this.widthCm, this.heightCm) > 120; } } class ShippingService { quote(p: Parcel, ratePerKg: number): number { return Math.ceil(p.billableWeight()) * ratePerKg; } } ``` `quote` stays in the service because the rate belongs to the service, not the parcel. ## Treatment - If the whole method works on the other object's data, use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md). - If only part of the method envies the other object, first use [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) on that part, then move the extracted piece. - If the method uses data from several objects, move it to the object whose data it uses most. Pass the rest in as arguments. - If a field is read mostly by another class, consider [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) instead of moving the methods. ## Payoff - Behavior sits next to the data it depends on, so a change to that data touches one class. - Fewer getters leak internal fields; some can become private. - Duplicate calculations across callers collapse into one method. ## When to ignore - The method deliberately sits apart from the data: [Strategy](https://aipatterns.dev/design-patterns/strategy.md) and [Visitor](https://aipatterns.dev/design-patterns/visitor.md) put behavior outside the data so it can vary independently. - The data type is a DTO or wire format shared across services, and adding logic would couple consumers to it. - The other type is generated code or a third-party class you cannot edit. See [Incomplete Library Class](https://aipatterns.dev/refactoring/smells/incomplete-library-class.md). - In a functional codebase, plain data plus free functions is the convention. Keep the functions in the module that owns the type rather than moving them into the type. ## Agent checklist - [ ] Count reads on `this` versus reads on the other object before moving anything. - [ ] Confirm the target class is editable and is not a shared DTO or generated type. - [ ] Extract the envious fragment first if only part of the method qualifies. - [ ] Leave a delegating method in the old place if public callers depend on it, and mark it for removal. - [ ] Run the tests after the move and check no new circular import appeared. --- # Inappropriate Intimacy > Two classes reach into each other's private fields and internals. Cut the back-and-forth by moving members or narrowing what each exposes. - kind: smell - category: couplers - aliases: Inappropriate Closeness - canonical: https://aipatterns.dev/refactoring/smells/inappropriate-intimacy - inspired-by: https://refactoring.guru/smells/inappropriate-intimacy ## Signs - Class `A` reads or writes fields of `B` that exist only for `A` to use, and `B` does the same to `A`. - Fields are made `public`, `internal` or package-private "so the other class can get at them". - Two classes hold references to each other and call back and forth during one operation. - A subclass depends on protected fields of its parent in ways the parent never documented. - You cannot change one class's internals without reading the other class first. ## Why it happens - Two concepts started as one class and were split without deciding who owns what. - A quick fix needed one value, so a field became visible instead of a method being added. - A bidirectional association was added for convenience and both sides started using it. - Inheritance was used to share fields, so the child grew dependent on the parent's private layout. ## Example An `Order` and its `Invoice` edit each other's state directly. ```ts title="before.ts" class Order { items: { sku: string; price: number; qty: number }[] = []; invoice?: Invoice; _discountApplied = false; // only Invoice uses this } class Invoice { total = 0; constructor(public order: Order) { order.invoice = this; } compute(): void { let sum = 0; for (const i of this.order.items) sum += i.price * i.qty; if (sum > 10000 && !this.order._discountApplied) { sum *= 0.95; this.order._discountApplied = true; } this.total = sum; } } ``` `Invoice` walks `Order`'s item list and flips a flag that only exists for it. `Order` keeps a back-reference it never uses. ```ts title="after.ts" type LineItem = { sku: string; price: number; qty: number }; class Order { private readonly items: LineItem[] = []; add(item: LineItem): void { this.items.push(item); } subtotal(): number { return this.items.reduce((s, i) => s + i.price * i.qty, 0); } } class Invoice { readonly total: number; constructor(order: Order) { const subtotal = order.subtotal(); this.total = subtotal > 10000 ? subtotal * 0.95 : subtotal; } } ``` The discount flag disappears because the invoice computes the total once. The association now runs one way. ## Treatment - If one class uses another's fields to do work, use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) and [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) until each field has one owner. - If both classes share a cluster of state, use [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) to give that state its own home. - If the link runs both ways but only one side needs it, use [Change Bidirectional Association to Unidirectional](https://aipatterns.dev/refactoring/techniques/change-bidirectional-association-to-unidirectional.md). - If a client reaches through one class into another, use [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md). - If a subclass is too intimate with its parent, use [Replace Inheritance with Delegation](https://aipatterns.dev/refactoring/techniques/replace-inheritance-with-delegation.md). ## Payoff - Each class can change its internals without breaking the other. - Invariants are enforced in one place, so state stays consistent. - The classes become testable on their own. ## When to ignore - The two types are one concept split across files for size reasons, such as a builder and the object it builds, or a class and its private nested helper. Close access is intended there. - Languages offer scoped access for this case: Java package-private, C# `internal`, Kotlin `internal`, Rust `pub(crate)`. Tight coupling inside one module is acceptable when the module boundary is the real API. - Performance-critical code where an accessor layer measurably costs too much. Measure first. ## Agent checklist - [ ] List every member each class touches on the other and decide one owner per member. - [ ] Check for a cyclic import between the two modules before and after the change. - [ ] Reduce visibility of each field once no outside class reads it. - [ ] Keep public behavior identical and run the tests after each move. - [ ] Do not break up intentionally co-located types such as builders or nested helpers. --- # Message Chains > Client code walks a chain like a.getB().getC().getD() to reach what it needs, coupling itself to every link in the path. - kind: smell - category: couplers - aliases: Train Wreck, Law of Demeter violation - canonical: https://aipatterns.dev/refactoring/smells/message-chains - inspired-by: https://refactoring.guru/smells/message-chains ## Signs - Expressions such as `order.customer.account.billingAddress.country` appear in client code. - The same navigation path is repeated in several files. - Null checks guard each step: `a?.b?.c?.d`. - A change to how `Account` stores addresses breaks code that only wanted a country code. - Tests must build a deep object graph to exercise one line. ## Why it happens - The object model mirrors the database schema, and callers navigate it the way they would join tables. - Getters exist for every field, so following them is the path of least resistance. - Nobody asked the first object for what the client actually needed, so no method for it exists. ## Example A tax calculator needs the buyer's country and digs for it. ```ts title="before.ts" class Address { constructor(readonly country: string) {} } class Account { constructor(readonly billingAddress: Address) {} } class Customer { constructor(readonly account: Account) {} } class Order { constructor(readonly customer: Customer, readonly subtotal: number) {} } function vatFor(order: Order): number { const country = order.customer.account.billingAddress.country; return country === "DE" ? order.subtotal * 0.19 : 0; } function shippingZone(order: Order): string { return order.customer.account.billingAddress.country === "US" ? "domestic" : "intl"; } ``` Both functions know the full path from `Order` to `Address`. ```ts title="after.ts" class Customer { constructor(private readonly account: Account) {} billingCountry(): string { return this.account.billingAddress.country; } } class Order { constructor(private readonly customer: Customer, readonly subtotal: number) {} billingCountry(): string { return this.customer.billingCountry(); } } function vatFor(order: Order): number { return order.billingCountry() === "DE" ? order.subtotal * 0.19 : 0; } function shippingZone(order: Order): string { return order.billingCountry() === "US" ? "domestic" : "intl"; } ``` Clients now depend on `Order` alone. Only `Customer` knows about `Account`. ## Treatment - If clients repeat a chain to reach one value, use [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md) on the first link so it answers directly. - If the client does work with the value at the end of the chain, use [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) on that work, then [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) it to an object near the start of the chain. - If you find yourself hiding delegates on every class, stop. That produces [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md). Hide only the paths clients actually use. ## Payoff - Clients depend on one neighbor instead of the whole object graph. - Restructuring intermediate objects stops breaking distant code. - Tests need shallower fixtures. ## When to ignore - Fluent and builder APIs return `this` or a value of the same kind: `query.where(...).orderBy(...).limit(10)`. That is one object, not a chain of strangers. - Pipelines over plain values, such as `items.filter(...).map(...)`, are transformations, not navigation. - Immutable records and read models you only read, such as parsed JSON or configuration, where the shape is the contract. Wrapping every path adds methods without reducing coupling. - Hiding every link would turn each class into a forwarding layer. Prefer the occasional chain. ## Agent checklist - [ ] Confirm the chain crosses several distinct types, not a fluent API on one type. - [ ] Count how many call sites use the same path before adding a delegating method. - [ ] Name the new method for what the client wants (`billingCountry`), not for the path. - [ ] Reduce visibility of the intermediate getters once no client uses them. - [ ] Avoid creating a [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md) with many pure forwarders. --- # Middle Man > A class mostly forwards calls to another object and adds nothing. Let clients talk to the real object or fold the wrapper away. - kind: smell - category: couplers - canonical: https://aipatterns.dev/refactoring/smells/middle-man - inspired-by: https://refactoring.guru/smells/middle-man ## Signs - Most public methods of a class are one line that calls the same field: `return this.repo.find(id)`. - Adding a method to the delegate forces a matching forwarding method on the wrapper. - The wrapper has no state of its own beyond the delegate reference. - Reviewers ask what the class is for, and the answer is "it wraps X". ## Why it happens - [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md) was applied to cure [Message Chains](https://aipatterns.dev/refactoring/smells/message-chains.md), then kept being applied until the class became pure forwarding. - Logic that once lived in the wrapper moved elsewhere, leaving the shell behind. - An abstraction layer was added "in case we switch implementations", and the switch never came. ## Example A `CustomerService` exists only to pass calls to the repository. ```ts title="before.ts" interface Customer { id: string; email: string; } class CustomerRepository { private rows = new Map(); find(id: string): Customer | undefined { return this.rows.get(id); } save(c: Customer): void { this.rows.set(c.id, c); } delete(id: string): void { this.rows.delete(id); } } class CustomerService { constructor(private readonly repo: CustomerRepository) {} find(id: string) { return this.repo.find(id); } save(c: Customer) { this.repo.save(c); } delete(id: string) { this.repo.delete(id); } } // controller const service = new CustomerService(new CustomerRepository()); service.save({ id: "c1", email: "a@example.com" }); ``` Every method forwards. Callers gain nothing by going through the service. ```ts title="after.ts" const customers = new CustomerRepository(); customers.save({ id: "c1", email: "a@example.com" }); ``` If one method in the service had real logic, such as normalizing the email before saving, keep that method and remove the rest. Callers then use the repository for plain reads and writes. ## Treatment - If most methods forward, use [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md) and let clients call the delegate. - If only a few forwarding methods exist on an otherwise useful class, use [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md) on those forwarders at their call sites. - If the wrapper adds a little behavior and forwards the entire delegate interface, consider [Replace Delegation with Inheritance](https://aipatterns.dev/refactoring/techniques/replace-delegation-with-inheritance.md), but only when the wrapper truly is a kind of the delegate. ## Payoff - Fewer types and fewer hops when reading the code. - New delegate methods are usable at once, without wrapper edits. - The remaining classes each have a real reason to exist. ## When to ignore - The wrapper is a deliberate seam: [Proxy](https://aipatterns.dev/design-patterns/proxy.md), [Decorator](https://aipatterns.dev/design-patterns/decorator.md), [Adapter](https://aipatterns.dev/design-patterns/adapter.md) or [Facade](https://aipatterns.dev/design-patterns/facade.md). These forward on purpose to add access control, caching, translation or simplification. - The wrapper isolates a third-party library so the rest of the code does not import it. That boundary has value even when thin. - The wrapper narrows a wide interface to the few methods clients may use. - Removing it would reintroduce long [Message Chains](https://aipatterns.dev/refactoring/smells/message-chains.md) in client code. ## Agent checklist - [ ] Count forwarding methods versus methods with real logic before removing the class. - [ ] Confirm the wrapper is not a named pattern or a dependency boundary that the team relies on. - [ ] Check dependency injection wiring and mocks in tests that reference the wrapper type. - [ ] Update every caller to the delegate, then delete the wrapper in a separate step. - [ ] Run the tests and confirm no layering or lint rule forbids the new direct dependency. --- # Comments > Comments that explain what unclear code does are a sign the code should be renamed or restructured so it explains itself. - kind: smell - category: dispensables - aliases: Deodorant comments - canonical: https://aipatterns.dev/refactoring/smells/comments - inspired-by: https://refactoring.guru/smells/comments ## Signs - A comment restates the next line: `// increment retry count` above `retries++`. - A block of code is introduced by a comment that names it: `// compute shipping`, followed by 20 lines. - A comment explains what a cryptic variable or method name means. - A comment states a precondition ("`items` must not be empty here") that nothing enforces. - Comments and code disagree, because the code changed and the comment did not. Not every comment is a smell. Comments that explain *why* (a regulatory rule, a workaround for a vendor bug, a performance trade-off) carry information the code cannot. Public API docs are not this smell either. ## Why it happens The author knew the code was hard to read and patched the reader's experience instead of the code. Comments are cheaper to write than a good name or an extracted function, and they feel responsible. They then rot, because nothing tests them. ## Example A loyalty program computes a discount. ```ts title="loyalty.ts" function discount(c: { y: number; o: number; t: string }, total: number): number { // c.y = years as member, c.o = orders this year, c.t = tier let d = 0; // long-time members get 5% if (c.y >= 3) d += 0.05; // frequent buyers get another 5% if (c.o > 12) d += 0.05; // gold tier doubles it if (c.t === "gold") d *= 2; // never more than 25% if (d > 0.25) d = 0.25; // total must be positive return total * d; } ``` After renaming, extracting and asserting, the comments have nothing left to say: ```ts title="loyalty.ts" interface Member { yearsAsMember: number; ordersThisYear: number; tier: "standard" | "gold"; } const MAX_DISCOUNT_RATE = 0.25; function loyaltyDiscount(member: Member, orderTotal: number): number { if (orderTotal <= 0) throw new RangeError("orderTotal must be positive"); return orderTotal * discountRate(member); } function discountRate(member: Member): number { const isLongTime = member.yearsAsMember >= 3; const isFrequent = member.ordersThisYear > 12; let rate = (isLongTime ? 0.05 : 0) + (isFrequent ? 0.05 : 0); if (member.tier === "gold") rate *= 2; return Math.min(rate, MAX_DISCOUNT_RATE); } ``` ## Treatment - If a comment explains a complex expression, use [Extract Variable](https://aipatterns.dev/refactoring/techniques/extract-variable.md) and give the variable the comment's meaning. - If a comment introduces a block of code, use [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) and name the method after the comment. - If a comment explains what a method does because its name does not, use [Rename Method](https://aipatterns.dev/refactoring/techniques/rename-method.md). - If a comment states an assumption about state, use [Introduce Assertion](https://aipatterns.dev/refactoring/techniques/introduce-assertion.md) so the assumption is checked. - If a comment explains a magic value, use [Replace Magic Number with Symbolic Constant](https://aipatterns.dev/refactoring/techniques/replace-magic-number-with-symbolic-constant.md). - Keep, or add, comments that explain *why*. Delete comments that only explain *what*. ## Payoff - Names and structure stay in sync with behavior because the compiler and tests check them. - Less text to read and nothing to go stale. - Assertions turn silent assumptions into failures you can see. ## When to ignore - The comment explains intent, a non-obvious constraint, a link to a bug tracker or spec, or why a simpler approach was rejected. - The comment is API documentation (`/** ... */`, docstrings) consumed by tooling or library users. - The code is irreducibly complex (a numerical algorithm, a bit-twiddling hack), and the comment cites the source or derivation. - License headers and generated-code markers. ## Agent checklist - [ ] For each comment you removed, its meaning now lives in a name, a type, a constant or an assertion. - [ ] You kept every comment that explains why, cites a ticket, or documents a public API. - [ ] You did not add new comments that restate the refactored code. - [ ] Assertions you introduced do not change behavior on valid inputs, and tests pass. - [ ] Any comment that disagreed with the code was resolved by checking which one is correct, not by deleting it blindly. --- # Duplicate Code > The same logic appears in two or more places, so every fix and change must be made several times and the copies drift apart. - kind: smell - category: dispensables - aliases: Clones, DRY violation - canonical: https://aipatterns.dev/refactoring/smells/duplicate-code - inspired-by: https://refactoring.guru/smells/duplicate-code ## Signs - Two functions are identical except for a name, a constant or a field. - A bug fix in one place needs to be repeated in another, and sometimes was not. - Sibling subclasses contain the same method body. - Two methods compute the same result by different algorithms. - Both branches of an `if` end with the same lines. - Two components render near-identical markup, and only the shared data was deduplicated. ## Why it happens Copying working code is the fastest way to ship a similar feature. Different people solve the same problem without knowing the other solution exists. Deadlines discourage the extra hour it takes to find a shared shape. And sometimes code looks the same by coincidence today and is supposed to diverge tomorrow; that is not duplication. ## Example Two report exporters in an analytics service share the same date filtering and totaling logic. ```ts title="exports.ts" interface Sale { at: Date; region: string; cents: number } function exportCsv(sales: Sale[], from: Date, to: Date): string { const inRange = sales.filter((s) => s.at >= from && s.at < to); const total = inRange.reduce((sum, s) => sum + s.cents, 0); const rows = inRange.map((s) => `${s.at.toISOString()},${s.region},${s.cents}`); return ["at,region,cents", ...rows, `total,,${total}`].join("\n"); } function exportJson(sales: Sale[], from: Date, to: Date): string { const inRange = sales.filter((s) => s.at >= from && s.at <= to); // drifted: <= vs < const total = inRange.reduce((sum, s) => sum + s.cents, 0); return JSON.stringify({ rows: inRange, total }); } ``` The copies already disagree on the end boundary. After [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md): ```ts title="exports.ts" interface Sale { at: Date; region: string; cents: number } function salesInRange(sales: Sale[], from: Date, to: Date) { const rows = sales.filter((s) => s.at >= from && s.at < to); // half-open, one rule const total = rows.reduce((sum, s) => sum + s.cents, 0); return { rows, total }; } function exportCsv(sales: Sale[], from: Date, to: Date): string { const { rows, total } = salesInRange(sales, from, to); const lines = rows.map((s) => `${s.at.toISOString()},${s.region},${s.cents}`); return ["at,region,cents", ...lines, `total,,${total}`].join("\n"); } function exportJson(sales: Sale[], from: Date, to: Date): string { return JSON.stringify(salesInRange(sales, from, to)); } ``` Decide which boundary is correct before merging. Merging silently changes behavior for one caller. ## Treatment - If the copies are in the same class, use [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) and call it from both places. - If the copies are in sibling subclasses, extract, then use [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md). - If the siblings share the same steps but differ in details, use [Form Template Method](https://aipatterns.dev/refactoring/techniques/form-template-method.md). - If two methods do the same thing with different algorithms, pick the clearer one with [Substitute Algorithm](https://aipatterns.dev/refactoring/techniques/substitute-algorithm.md). - If the copies live in unrelated classes, use [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md) when they share an "is a" relationship, or [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) when they do not. - If several conditions lead to the same action, use [Consolidate Conditional Expression](https://aipatterns.dev/refactoring/techniques/consolidate-conditional-expression.md). If every branch ends with the same code, use [Consolidate Duplicate Conditional Fragments](https://aipatterns.dev/refactoring/techniques/consolidate-duplicate-conditional-fragments.md). - If the copies are UI markup, extract one shared component. Sharing the data (an options list, a constant) is not enough: the copy-pasted tags and handlers stay until one component owns them. ## Payoff - One place to fix bugs and change rules. - Less code to read, test and maintain. - Hidden drift between copies surfaces and gets decided once. ## When to ignore - The code looks alike but represents different concepts that will change for different reasons. Merging couples them; wait for a third copy to confirm the pattern. - The copies are in separate services or packages, and sharing would add a dependency that costs more than the duplication. - Test code, where explicit repetition often reads better than a clever shared helper. - Two or three trivial lines, where the helper would be harder to read than the copy. ## Agent checklist - [ ] You compared every copy line by line and listed the differences before merging. - [ ] Each difference was either confirmed as a bug and fixed, or kept as a parameter. - [ ] The copies change for the same reason; otherwise you left them separate. - [ ] You checked for markup duplication, not only logic duplication. - [ ] You did not add a cross-package dependency to remove a few lines. - [ ] Tests for every former caller pass, including a case on each boundary that differed. --- # Lazy Class > A class does too little to justify the file, name and indirection it costs; fold it into its caller or delete it. - kind: smell - category: dispensables - aliases: Freeloader - canonical: https://aipatterns.dev/refactoring/smells/lazy-class - inspired-by: https://refactoring.guru/smells/lazy-class ## Signs - A class has one short method, or only forwards calls to another object. - A subclass adds nothing over its parent, or overrides one trivial value. - A class was planned for features that never arrived. - After a refactoring, a class is left holding one field and a getter. - Readers have to open the file to learn that it does almost nothing. ## Why it happens Classes shrink over time as responsibilities move elsewhere, and nobody deletes the shell. Others are created up front for a design that was never built. Some come from a rule like "every concept gets its own class", applied to concepts that are a single value or a single line. ## Example A billing module wraps a currency formatter in its own class and a subclass. ```ts title="billing.ts" class MoneyFormatter { format(cents: number): string { return (cents / 100).toFixed(2); } } class UsdMoneyFormatter extends MoneyFormatter { format(cents: number): string { return "$" + super.format(cents); } } class InvoiceView { private formatter = new UsdMoneyFormatter(); totalLine(totalCents: number): string { return `Total: ${this.formatter.format(totalCents)}`; } } ``` Two classes and an inheritance link wrap one expression. Inline them: ```ts title="billing.ts" const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }); class InvoiceView { totalLine(totalCents: number): string { return `Total: ${usd.format(totalCents / 100)}`; } } ``` The platform formatter also handles thousands separators and negative values, which the hand-written class did not. ## Treatment - If a class does little and has one main user, use [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md) to move its members into that user. - If a subclass or superclass adds almost nothing, use [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md) to merge it into its neighbor. - If the class only forwards calls, treat it as [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md) and use [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md). - If a standard library or platform feature already does the job, delete the class and call the platform directly. ## Payoff - Fewer files and fewer jumps to follow the code. - The logic sits next to its only caller, where readers expect it. - Less surface to keep in sync during later refactorings. ## When to ignore - The class marks a real boundary: an interface you mock in tests, a seam for a third-party dependency, or a public API type. - It is a value type that gives a primitive a name and validation (for example `EmailAddress`). That cures [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md), even if it is small. - The class is a deliberate placeholder for work scheduled in the current iteration, not "someday". - Framework conventions require it (a controller, a migration, a DI module). ## Agent checklist - [ ] You listed every user of the class and confirmed none needs it as a seam or mock point. - [ ] The class is not a value object that protects an invariant. - [ ] After inlining, no caller lost behavior, including edge cases the old class handled. - [ ] Imports, DI registrations and exports referencing the class were removed. - [ ] Tests pass and test doubles for the removed class were deleted or replaced. --- # Data Class > A class holds fields and accessors but no behavior, while other classes reach in to do the work that belongs to it. - kind: smell - category: dispensables - aliases: Anemic model - canonical: https://aipatterns.dev/refactoring/smells/data-class - inspired-by: https://refactoring.guru/smells/data-class ## Signs - A class has only fields, getters and setters. - Public mutable fields or a setter for every field, including ones that should never change after creation. - Collections are exposed directly, so callers push and splice them. - Other classes contain methods that read three or four fields of the data class and compute something from them. - The same calculation over the class's fields appears in several services. ## Why it happens The class started as a record for transfer or persistence, and behavior grew in the services that use it. Some architectures encourage this by putting all logic in "service" classes. ORMs and serializers that need public setters push in the same direction. A Data Class is not always wrong. The smell is the combination: passive data here, and code elsewhere that envies it. ## Example A warehouse `Shipment` exposes everything, and the services do the thinking. ```ts title="shipment.ts" class Shipment { id = ""; status: "packing" | "shipped" | "delivered" = "packing"; parcels: { weightKg: number }[] = []; shippedAt?: Date; } class ShippingService { ship(s: Shipment): void { if (s.status !== "packing" || s.parcels.length === 0) throw new Error("cannot ship"); s.status = "shipped"; s.shippedAt = new Date(); } totalWeight(s: Shipment): number { return s.parcels.reduce((sum, p) => sum + p.weightKg, 0); } } // elsewhere: s.parcels.push(...) and s.status = "delivered" with no checks ``` Move the rules into the class and close the fields: ```ts title="shipment.ts" type Status = "packing" | "shipped" | "delivered"; class Shipment { private _status: Status = "packing"; private _shippedAt?: Date; private readonly _parcels: { weightKg: number }[] = []; constructor(readonly id: string) {} get status(): Status { return this._status; } get parcels(): readonly { weightKg: number }[] { return this._parcels; } addParcel(weightKg: number): void { if (this._status !== "packing") throw new Error("shipment is closed"); this._parcels.push({ weightKg }); } ship(now = new Date()): void { if (this._status !== "packing" || this._parcels.length === 0) throw new Error("cannot ship"); this._status = "shipped"; this._shippedAt = now; } totalWeight(): number { return this._parcels.reduce((sum, p) => sum + p.weightKg, 0); } } ``` ## Treatment - If fields are public, use [Encapsulate Field](https://aipatterns.dev/refactoring/techniques/encapsulate-field.md). - If a collection is exposed, use [Encapsulate Collection](https://aipatterns.dev/refactoring/techniques/encapsulate-collection.md) and add `add`/`remove` methods. - If client code computes from the class's fields, use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to put that code on the class. Use [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) first if only part of a client method belongs there. - If a field should not change after construction, use [Remove Setting Method](https://aipatterns.dev/refactoring/techniques/remove-setting-method.md). - If an accessor is no longer needed outside, use [Hide Method](https://aipatterns.dev/refactoring/techniques/hide-method.md). ## Payoff - Invariants are enforced in one place instead of trusted to every caller. - Duplicate calculations in services collapse into one method. - The class's API shows what you can do with it, not only what it stores. ## When to ignore - It is a data transfer object, API payload, event or database row whose job is to carry data across a boundary. Keep it plain, and put behavior in the domain type on the other side. - It is an immutable record (`readonly` fields, TypeScript type, Java record, Python frozen dataclass) with no invariants to protect. - In functional style, data and functions are separate by design. Group the functions in one module next to the type. ## Agent checklist - [ ] You confirmed the class is a domain object, not a DTO, event or persistence row that must stay plain. - [ ] Each moved method uses mostly the data class's fields, not the service's. - [ ] Every caller that mutated fields directly now goes through a method, and the type checker confirms no direct writes remain. - [ ] Serialization, ORM mapping and test fixtures still work with the new private fields. - [ ] Tests cover the invariants the new methods enforce. --- # Dead Code > Variables, parameters, branches, methods or classes that nothing uses still sit in the codebase, costing reading time and misleading readers. - kind: smell - category: dispensables - aliases: Unused code, Unreachable code - canonical: https://aipatterns.dev/refactoring/smells/dead-code - inspired-by: https://refactoring.guru/smells/dead-code ## Signs - The editor or linter greys out a variable, import or private method. - A function has no callers in the repository. - A branch can never run: a condition on a feature flag removed a year ago, or a `case` for an enum value that no longer exists. - A parameter is passed by every caller and ignored by the function. - Large blocks are commented out "in case we need it". ## Why it happens Requirements change and the old path is left in place because removing it feels risky. Feature flags are rolled out to 100% and never cleaned up. Refactorings leave orphaned helpers. Commented-out code is kept because people do not trust version control to remember it. ## Example A checkout service still carries a legacy payment path and an unused parameter. ```ts title="checkout.ts" import { legacyGateway } from "./legacy-gateway"; // only used below import { stripe } from "./stripe"; const USE_LEGACY_GATEWAY = false; // rolled out in 2023 export async function charge(orderId: string, cents: number, currency: string, retries: number) { // const fraud = await fraudCheck(orderId); // if (fraud.score > 0.8) throw new Error("fraud"); if (USE_LEGACY_GATEWAY) { return legacyGateway.charge(orderId, cents); } return stripe.charge({ orderId, amount: cents, currency }); } function formatLegacyReceipt(orderId: string): string { return `LEGACY-${orderId}`; } ``` `retries` is ignored, the flag is constant, the receipt helper has no callers, and the fraud check is a comment. After cleanup: ```ts title="checkout.ts" import { stripe } from "./stripe"; export async function charge(orderId: string, cents: number, currency: string) { return stripe.charge({ orderId, amount: cents, currency }); } ``` Git history keeps the old code if anyone needs it. ## Treatment - Delete it. Version control is the archive. - If a parameter is unused, use [Remove Parameter](https://aipatterns.dev/refactoring/techniques/remove-parameter.md) and update every caller. - If a class became unused or nearly empty, delete it, or use [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md) if a fragment is still needed. - If a subclass is no longer instantiated, use [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md). - For flag-guarded code, remove the flag and the losing branch together. - Use the compiler, linter (`noUnusedLocals`, `noUnusedParameters`), coverage reports and "find references" to locate candidates; confirm before deleting. ## Payoff - Less code to read, search, compile and keep secure. - Readers stop wondering whether a path is live. - Upgrades and refactorings no longer have to carry dead dependencies along. ## When to ignore - The symbol is part of a published library API. Removing it is a breaking change; deprecate it first and follow your versioning policy. - It is called by reflection, dependency injection, a framework by name, a scheduled job, a CLI, or another repository. "No references" in one repo does not prove "no callers". - The code is behind a flag that is still live in some environment or for some tenant. ## Agent checklist - [ ] You searched for callers by name across the repo, including string references, config, routes, DI registrations and templates. - [ ] The symbol is not exported from a published package, or you deprecated it instead of deleting it. - [ ] Flag-guarded code was removed only after confirming the flag's value in every environment. - [ ] Unused imports and dependencies freed by the deletion were removed too. - [ ] The build, type check and full test suite pass after deletion. --- # Speculative Generality > Abstractions, hooks and parameters built for future needs that never arrived make code harder to read without serving any current use. - kind: smell - category: dispensables - aliases: YAGNI violation, Over-engineering - canonical: https://aipatterns.dev/refactoring/smells/speculative-generality - inspired-by: https://refactoring.guru/smells/speculative-generality ## Signs - An interface has exactly one implementation, and no test double uses it either. - An abstract base class has one subclass. - A function takes options or parameters that every caller passes with the same value, or never passes. - A plugin system, registry or strategy hook exists with one plugin. - Names are generic (`DataProcessor`, `handleEntity`, `GenericRepository`) for code that handles one concrete thing. - The only callers of some methods are their own tests. ## Why it happens Developers anticipate change and build the flexibility up front, often after being burned by a rigid design. The anticipated change rarely arrives in the shape that was predicted, so the abstraction ends up both unused and wrong. AI agents are prone to this smell too, because generating extra layers costs them nothing. ## Example A notification module was built for "any channel" but only ever sends email. ```ts title="notify.ts" interface Channel { send(payload: TPayload, options?: { priority?: number; retry?: boolean }): Promise; } abstract class BaseChannel implements Channel { abstract send(payload: T, options?: { priority?: number; retry?: boolean }): Promise; } class EmailChannel extends BaseChannel<{ to: string; body: string }> { async send(p: { to: string; body: string }) { await smtp.send(p.to, p.body); } } class ChannelRegistry { private channels = new Map>(); register(name: string, c: Channel) { this.channels.set(name, c); } get(name: string) { return this.channels.get(name)!; } } const registry = new ChannelRegistry(); registry.register("email", new EmailChannel()); await registry.get("email").send({ to: "a@example.com", body: "Your order shipped" }); declare const smtp: { send(to: string, body: string): Promise }; ``` The `priority` and `retry` options are never read. Collapse to what is used: ```ts title="notify.ts" export async function sendEmail(to: string, body: string): Promise { await smtp.send(to, body); } await sendEmail("a@example.com", "Your order shipped"); declare const smtp: { send(to: string, body: string): Promise }; ``` When SMS arrives, you will know its real shape and can introduce an abstraction that fits both. ## Treatment - If an abstract class has one subclass, use [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md). - If a class only delegates or wraps without adding value, use [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md). - If a method only forwards to another, use [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md). - If a parameter is unused or always the same value, use [Remove Parameter](https://aipatterns.dev/refactoring/techniques/remove-parameter.md). - If a method has an abstract, future-proof name, use [Rename Method](https://aipatterns.dev/refactoring/techniques/rename-method.md) so it says what it does today. - Delete tests that only exist to exercise the unused generality. ## Payoff - Code says what the system does, not what it might do. - Fewer types and indirections to follow. - When the real second case arrives, you design the abstraction from two concrete examples instead of guessing. ## When to ignore - You are building a framework or library whose users supply the other implementations. - The interface is a test seam: tests substitute a fake, and that fake counts as a second implementation. - The second implementation is committed work in the current iteration, not a "someday". - The abstraction isolates a third-party dependency you are contractually or operationally likely to swap. ## Agent checklist - [ ] For each abstraction you removed, you confirmed there is no second implementation in tests, other packages or plugins. - [ ] Parameters you removed were unused or constant at every call site. - [ ] You did not delete an extension point that is part of a public API. - [ ] The simplified code passes the same tests, minus tests that only covered the removed generality. - [ ] You did not add new "for later" flexibility while simplifying. --- # Switch Statements > The same switch or if-chain over a type code is repeated across the codebase, so adding one case means editing every copy. - kind: smell - category: oo-abusers - aliases: Repeated Switch, Type Switching - canonical: https://aipatterns.dev/refactoring/smells/switch-statements - inspired-by: https://refactoring.guru/smells/switch-statements ## Signs - Two or more functions switch on the same discriminator (`plan.tier`, `shape.kind`, `event.type`) with the same set of cases. - Adding a new kind requires a search for every `case "pro":` in the codebase. - A `default:` branch throws "unknown type", which is the only thing protecting you from a forgotten case. - The switch lives in a class that does not own the data it switches on. A single switch in one place, such as a factory or a parser, is not the smell. Duplication of the switch is. ## Why it happens A switch is the most direct way to express "behave differently for each kind", and the first one is fine. Then a second feature needs per-kind behavior, and copying the switch is faster than reorganizing the code. Each copy is reasonable on its own; together they scatter the definition of each kind across the codebase. ## Example Subscription tiers handled by switches in pricing, limits and display code. ```ts title="plans.ts" type Tier = "free" | "pro" | "team"; function monthlyPriceCents(tier: Tier, seats: number): number { switch (tier) { case "free": return 0; case "pro": return 1200; case "team": return 900 * Math.max(seats, 3); } } function storageLimitGb(tier: Tier): number { switch (tier) { case "free": return 2; case "pro": return 100; case "team": return 1000; } } function badge(tier: Tier): string { switch (tier) { case "free": return ""; case "pro": return "PRO"; case "team": return "TEAM"; } } ``` Each tier becomes one object that owns all of its behavior. Adding an "enterprise" tier is one new entry, and the compiler flags any missing member. ```ts title="plans.ts" interface Plan { monthlyPriceCents(seats: number): number; storageLimitGb: number; badge: string; } const plans: Record = { free: { monthlyPriceCents: () => 0, storageLimitGb: 2, badge: "" }, pro: { monthlyPriceCents: () => 1200, storageLimitGb: 100, badge: "PRO" }, team: { monthlyPriceCents: (seats) => 900 * Math.max(seats, 3), storageLimitGb: 1000, badge: "TEAM" }, }; const price = plans[account.tier].monthlyPriceCents(account.seats); ``` ## Treatment - First isolate each switch with [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md), then use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to put it on the class that owns the discriminator. - If the discriminator is a type code fixed at creation, use [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md), then [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md). - If the type can change during the object's life (an account upgrades), use [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md). - If each case calls the same method with a different argument, use [Replace Parameter with Explicit Methods](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-explicit-methods.md). - If one of the cases is "null" or "none", use [Introduce Null Object](https://aipatterns.dev/refactoring/techniques/introduce-null-object.md). - In TypeScript, Rust, Kotlin, Swift and Scala, a discriminated union plus an exhaustiveness check (`never` in the default branch, or a `match`) makes a missing case a compile error. That can be enough on its own when the set of kinds is closed. ## Payoff - Each kind is defined in one place, so adding a kind is one change. - Forgotten cases become compile errors instead of runtime exceptions. - Per-kind behavior is testable without constructing the other kinds. ## When to ignore - There is exactly one switch over the type, typically in a factory that picks which object to create. That is where the decision belongs. - The set of cases is closed and small, and the language checks exhaustiveness. A `match` over a sealed union is idiomatic in functional code and often clearer than a class hierarchy. - You add new *operations* far more often than new *kinds*. Polymorphism makes kinds cheap and operations expensive; the switch makes the opposite trade. For a closed set of kinds with many operations, see [Visitor](https://aipatterns.dev/design-patterns/visitor.md) or keep the switches with exhaustiveness checks. ## Agent checklist - [ ] You counted the switches on the same discriminator; one switch alone is not a reason to refactor. - [ ] You decided whether kinds or operations change more often before choosing polymorphism over exhaustive matching. - [ ] The remaining single decision point (factory or lookup) is the only place that maps the raw code to an object. - [ ] A test exists per kind for each behavior that moved. - [ ] A missing case is now caught by the compiler or by an interface the new kind must implement. --- # Temporary Field > A field that is set and meaningful only during one operation or in one special case, and is empty or stale the rest of the time. - kind: smell - category: oo-abusers - canonical: https://aipatterns.dev/refactoring/smells/temporary-field - inspired-by: https://refactoring.guru/smells/temporary-field ## Signs - A field is `undefined` or `null` for most of the object's life, and readers must guess when it holds a real value. - Methods begin with guards like `if (!this.currentBatch) throw ...`. - A field is assigned at the top of one method and cleared at the bottom, so it is really a local variable stored on the instance. - Field names carry hints like `tmp`, `current`, `working` or `last`. - Running two operations concurrently on the same instance corrupts results because they share the field. ## Why it happens A long algorithm needs many intermediate values. Passing them between helper methods means long parameter lists, so the author stores them as fields instead. The helpers stay short, but the class now has state that is only valid while that one algorithm is running. A similar pattern arises when an optional feature adds fields that only some instances use. ## Example A payroll calculator that stores intermediate values on the service instance. ```ts title="payroll.ts" class PayrollService { private grossCents?: number; private taxableCents?: number; private employee?: Employee; computeNet(employee: Employee, hours: number): number { this.employee = employee; this.grossCents = Math.round(hours * employee.hourlyRateCents); this.taxableCents = this.grossCents - this.pretaxDeductions(); const net = this.taxableCents - this.incomeTax(); this.employee = this.grossCents = this.taxableCents = undefined; return net; } private pretaxDeductions(): number { return Math.round(this.grossCents! * this.employee!.retirementRate); } private incomeTax(): number { return Math.round(this.taxableCents! * 0.22); } } ``` Moving the calculation into its own short-lived object makes the fields always valid, and the service stays stateless. ```ts title="payroll.ts" class PayCalculation { readonly gross: number; readonly taxable: number; constructor(private readonly employee: Employee, hours: number) { this.gross = Math.round(hours * employee.hourlyRateCents); this.taxable = this.gross - this.pretaxDeductions(); } net(): number { return this.taxable - this.incomeTax(); } private pretaxDeductions(): number { return Math.round(this.gross * this.employee.retirementRate); } private incomeTax(): number { return Math.round(this.taxable * 0.22); } } class PayrollService { computeNet(employee: Employee, hours: number): number { return new PayCalculation(employee, hours).net(); } } ``` ## Treatment - If the fields support one algorithm, use [Replace Method with Method Object](https://aipatterns.dev/refactoring/techniques/replace-method-with-method-object.md): the algorithm becomes a class whose fields are set in its constructor and always valid. - If the fields and the code that uses them form a separate concept (an optional feature, a special mode), use [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) and hold an instance only when it applies. - If code checks the field for emptiness before every use, use [Introduce Null Object](https://aipatterns.dev/refactoring/techniques/introduce-null-object.md) to provide a harmless default. - If the intermediate values are cheap, pass them as parameters or return them from helpers instead of storing them anywhere. ## Payoff - Every field on every object holds a meaningful value whenever the object is reachable. - Non-null assertions and defensive guards disappear. - Long-lived services become stateless, so they are safe to share across requests and threads. - The algorithm becomes a named, testable unit. ## When to ignore - Lazily computed caches that are valid once filled and invalidated deliberately. A memoized field is not a temporary field if its lifecycle is clear. - Framework lifecycle hooks that require fields to be set in one callback and read in another, such as test fixtures assigned in `beforeEach`. ## Agent checklist - [ ] You traced each field's writes and confirmed it is valid only during part of the object's life. - [ ] After the change, no field on the original class is optional solely because of the algorithm you moved. - [ ] The new method object or extracted class is constructed fully valid; no setter is required before use. - [ ] Shared services you touched are now free of per-call state, and concurrent calls cannot interfere. - [ ] Existing tests pass; add a test that runs two calculations back to back to catch stale state. --- # Refused Bequest > A subclass inherits methods or data it does not want, overriding them to throw or do nothing, so the hierarchy lies about what it can do. - kind: smell - category: oo-abusers - canonical: https://aipatterns.dev/refactoring/smells/refused-bequest - inspired-by: https://refactoring.guru/smells/refused-bequest ## Signs - A subclass overrides inherited methods to throw `NotSupportedError` or return without doing anything. - Callers check `instanceof` before calling a base-class method, because some subclasses can't honor it. - The subclass ignores most inherited fields; they sit unused on every instance. - The inheritance was chosen to reuse a few helper methods, not because the subclass "is a" kind of the parent. ## Why it happens Inheritance is the quickest way to reuse code: extend a class and its methods are yours. When the new class needs only part of the parent, the rest gets stubbed out. The hierarchy then claims a substitutability that does not hold, which breaks callers that trust the base type. ## Example A read-only archive storage that extends a general-purpose file store. ```ts title="storage.ts" class FileStore { constructor(protected bucket: string) {} async read(key: string): Promise { /* ... */ return Buffer.alloc(0); } async list(prefix: string): Promise { /* ... */ return []; } async write(key: string, data: Buffer): Promise { /* ... */ } async delete(key: string): Promise { /* ... */ } } class ArchiveStore extends FileStore { async write(): Promise { throw new Error("archive is read-only"); } async delete(): Promise { throw new Error("archive is read-only"); } } async function cleanup(store: FileStore) { for (const key of await store.list("tmp/")) await store.delete(key); // explodes for ArchiveStore } ``` Splitting the capability into a narrower interface and using delegation makes the type honest. ```ts title="storage.ts" interface ReadableStore { read(key: string): Promise; list(prefix: string): Promise; } interface WritableStore extends ReadableStore { write(key: string, data: Buffer): Promise; delete(key: string): Promise; } class FileStore implements WritableStore { /* unchanged */ } class ArchiveStore implements ReadableStore { constructor(private readonly inner: FileStore) {} read(key: string) { return this.inner.read(key); } list(prefix: string) { return this.inner.list(prefix); } } async function cleanup(store: WritableStore) { /* ArchiveStore no longer type-checks here */ } ``` ## Treatment - If the subclass is not truly a kind of the parent and only wants some of its behavior, use [Replace Inheritance with Delegation](https://aipatterns.dev/refactoring/techniques/replace-inheritance-with-delegation.md). Hold the former parent as a field and forward only what you need. - If inheritance is appropriate but the parent has members only some children use, create a sibling class and use [Push Down Method](https://aipatterns.dev/refactoring/techniques/push-down-method.md) and [Push Down Field](https://aipatterns.dev/refactoring/techniques/push-down-field.md) to move those members into the children that want them. - If two classes share a genuine core and differ elsewhere, use [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md) for the shared core and keep the rest separate. - Split wide interfaces by capability (read, write, admin) so each implementation declares only what it supports. ## Payoff - Any object passed as the base type actually supports the base type's operations. - `instanceof` checks and runtime "not supported" errors disappear. - The compiler rejects misuse that previously failed in production. - The hierarchy documents real relationships instead of code-sharing shortcuts. ## When to ignore - The refused method is rarely called and the parent is a framework class you cannot change; a documented no-op may be the least bad option. - The subclass refuses an *implementation* but still honors the *interface*, for example by overriding a method with a different but valid behavior. That is ordinary specialization. - The standard library does it deliberately, such as read-only collection views that throw on mutation, and callers are used to it. Match the convention of your platform. ## Agent checklist - [ ] You listed every inherited member the subclass overrides to throw, ignore or leave unused. - [ ] You checked callers that accept the base type and confirmed which ones would break with the subclass. - [ ] After the change, no method in the hierarchy exists only to throw "not supported". - [ ] Callers that need the narrower capability accept the narrower type. - [ ] Tests pass, and a type-level or unit test shows the invalid call is now rejected. --- # Alternative Classes with Different Interfaces > Two classes do the same job but expose different method names or signatures, so callers cannot swap one for the other. - kind: smell - category: oo-abusers - canonical: https://aipatterns.dev/refactoring/smells/alternative-classes-with-different-interfaces - inspired-by: https://refactoring.guru/smells/alternative-classes-with-different-interfaces ## Signs - Two classes have methods that do the same thing under different names: `sendSms(to, text)` and `deliver(message)`. - Callers contain `if (provider === "a") a.foo() else b.bar()` to bridge the difference. - The implementations duplicate logic such as retries, rate limiting or formatting. - One class was written later by someone who did not know the other existed. ## Why it happens Two teams or two people solve the same problem at different times. A vendor switch or a second payment provider is added next to the first rather than behind a shared contract. Each class gets names that fit the vendor's own API, so the interfaces diverge even though the intent is identical. ## Example Two SMS providers integrated separately. ```ts title="sms.ts" class TwilioClient { async sendSms(toNumber: string, body: string): Promise { /* returns message SID */ return ""; } } class VonageClient { async deliver(msg: { recipient: string; text: string; from?: string }): Promise<{ id: string }> { return { id: "" }; } } async function sendVerificationCode(phone: string, code: string, region: string) { const text = `Your code is ${code}`; if (region === "EU") { const res = await new VonageClient().deliver({ recipient: phone, text }); return res.id; } return new TwilioClient().sendSms(phone, text); } ``` One interface with matching signatures removes the branch from callers. ```ts title="sms.ts" interface SmsSender { send(to: string, text: string): Promise; } class TwilioSender implements SmsSender { async send(to: string, text: string): Promise { /* ... */ return ""; } } class VonageSender implements SmsSender { async send(to: string, text: string): Promise { const res = await vonage.deliver({ recipient: to, text }); return res.id; } } async function sendVerificationCode(sender: SmsSender, phone: string, code: string) { return sender.send(phone, `Your code is ${code}`); } ``` ## Treatment - Start with [Rename Method](https://aipatterns.dev/refactoring/techniques/rename-method.md) so methods that mean the same thing share a name. - Align signatures with [Add Parameter](https://aipatterns.dev/refactoring/techniques/add-parameter.md) and [Parameterize Method](https://aipatterns.dev/refactoring/techniques/parameterize-method.md) until the methods take and return the same shapes. - If only part of a class matches the other, use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to relocate behavior so the matching parts line up. - Once the methods match, use [Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md) so callers depend on the shared contract. - If the implementations also share real code, use [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md) to hold it. Prefer an interface plus a shared helper when the classes wrap different third-party SDKs. - If one class is a strict subset of the other, delete it and redirect its callers. ## Payoff - Callers can swap implementations without branching, which makes tests with fakes straightforward. - Duplicate logic around the call (retry, logging) collapses into one place. - Adding a third provider means one new class, not another branch in each caller. ## When to ignore - The classes look alike but serve different domains that will diverge, such as transactional email and marketing email with different compliance rules. - The classes live in separate libraries you do not control. Wrap them with an [Adapter](https://aipatterns.dev/design-patterns/adapter.md) in your code instead of trying to change them. - Only one caller uses both, and it is about to be deleted. ## Agent checklist - [ ] You confirmed the two classes have the same intent and the same failure semantics, not only similar names. - [ ] Renames are applied across every caller, including dynamic lookups and string references. - [ ] The extracted interface contains only methods every implementation supports fully. - [ ] Callers receive the interface through a parameter or constructor rather than constructing a concrete class. - [ ] Each implementation passes the same contract test suite. --- # Incomplete Library Class > A library type lacks a method you need and you cannot edit it, so helper logic for it is scattered across your code. - kind: smell - category: other - canonical: https://aipatterns.dev/refactoring/smells/incomplete-library-class - inspired-by: https://refactoring.guru/smells/incomplete-library-class ## Signs - The same few lines operating on a library type appear in several modules: computing the start of a week from a `Date`, or splitting a `URL` query. - A `utils` file grows functions whose first parameter is always one library type. - Code works around a missing library method with manual field access. - Upgrading the library would mean finding every copy of that workaround. ## Why it happens - Library authors design for the common case and cannot predict every caller's need. - The library is third-party, generated or shared across teams, so you cannot add the method upstream. - Each developer who hit the gap solved it inline, without checking whether someone already had. ## Example Several reports need "next business day" from the built-in `Date`. ```ts title="before.ts" // billing.ts function dueDate(issued: Date): Date { const d = new Date(issued); d.setDate(d.getDate() + 1); while (d.getDay() === 0 || d.getDay() === 6) d.setDate(d.getDate() + 1); return d; } // shipping.ts function dispatchDate(ordered: Date): Date { const d = new Date(ordered.getTime()); do { d.setDate(d.getDate() + 1); } while ([0, 6].includes(d.getDay())); return d; } ``` Two copies of the same logic, written slightly differently. A foreign method puts the logic in one function that takes the library type as its first argument: ```ts title="dates.ts" export function nextBusinessDay(from: Date): Date { const d = new Date(from.getTime()); do { d.setDate(d.getDate() + 1); } while (d.getDay() === 0 || d.getDay() === 6); return d; } ``` When many such helpers accumulate, a local extension groups them in one type: ```ts title="business-date.ts" export class BusinessDate { constructor(private readonly value: Date) {} next(): BusinessDate { return new BusinessDate(nextBusinessDay(this.value)); } isWeekend(): boolean { const day = this.value.getDay(); return day === 0 || day === 6; } toDate(): Date { return new Date(this.value.getTime()); } } ``` ## Treatment - If you need one or two extra operations, use [Introduce Foreign Method](https://aipatterns.dev/refactoring/techniques/introduce-foreign-method.md): a function in your code that takes the library object as its first argument. - If you need many operations, use [Introduce Local Extension](https://aipatterns.dev/refactoring/techniques/introduce-local-extension.md): a wrapper class or subclass that adds them. - Prefer a wrapper over a subclass when the library type is final, sealed, or created by the library itself. - If the gap is a bug or an obvious omission, also open an issue or a pull request upstream, and keep your local fix until it lands. ## Payoff - One implementation of each missing operation, with one set of tests. - Library upgrades touch one file. - Call sites read as domain intent, such as `nextBusinessDay(d)`, instead of loops over date fields. ## When to ignore - The missing operation is a one-off call on the library's own type, where an extension method reads naturally. In languages that have them - C# extension methods, Kotlin extension functions, Swift extensions, Rust traits - write it there; it is the idiomatic form of a foreign method. - Monkey-patching the library prototype (JavaScript, Ruby) solves the same problem but leaks to every consumer and breaks on upgrade. Do not use it for this smell. - The workaround appears once. A single inline helper is fine until a second caller needs it. - A maintained library already provides the operation, such as a date library. Check the dependencies you already have before writing your own. ## Agent checklist - [ ] Search the codebase for existing helpers on the same library type before adding one. - [ ] Check whether an installed dependency already offers the operation. - [ ] Choose a foreign function for one or two operations and a wrapper for many. - [ ] Never modify the library's prototype or vendored source. - [ ] Replace every duplicated inline workaround with the new helper and run the tests. --- # Thin Tool Wrapper > A tool for an agent or API consumer that passes an upstream operation through raw - one endpoint, one tool, provider-shaped payloads - so every real task becomes a chain of calls over low-signal data. - kind: smell - category: other - aliases: Pass-Through Tool, Raw Endpoint Exposure - canonical: https://aipatterns.dev/refactoring/smells/thin-tool-wrapper - inspired-by: https://www.anthropic.com/engineering/writing-tools-for-agents ## Signs - A tool's description says what it calls ("wraps the Users API") instead of when to use it - and consumers guess. - One task takes a chain of calls: `get_user`, then `list_transactions`, then `list_notes`, each returning a page the caller must join themselves. - Responses carry provider-shaped payloads - uuids, mime types, pagination cursors, twelve metadata columns - when the caller needs a name, a total, and a link. - Two or more tools overlap: either could answer the call, and the consumer must guess which is meant. - The tool count grows with the upstream API's surface - a new upstream endpoint produces a new tool, not a decision. ## Why it happens Wrapping each upstream endpoint one-to-one feels complete and is easy to review - the mapping is mechanical, so nobody has to think. The provider's schema is documented and at hand, while the consumer's actual tasks are nobody's assigned job. And every downstream consumer quietly adapts: they write the joins, filter the noise, and learn the unwritten rules, which hides the smell from the team that shipped it. ## Example Three thin tools over one CRM. Answering "who is this customer and what did they last buy?" costs three calls and returns fields nobody reads: ```ts // Tools as endpoint mirrors - the consumer does the work. const crmTools = { getUser: (userId: string) => fetch(`${crm}/users/${userId}`).then((r) => r.json()), // 14 metadata columns listTransactions: (userId: string) => fetch(`${crm}/users/${userId}/transactions`).then((r) => r.json()), listNotes: (userId: string) => fetch(`${crm}/users/${userId}/notes`).then((r) => r.json()), }; // Every caller chains the trio and joins by hand: const user = await crmTools.getUser("u-9"); const txns = await crmTools.listTransactions("u-9"); const notes = await crmTools.listNotes("u-9"); const lastPurchase = txns.items[0]?.amount_cents; // buried in the raw row ``` The tools are correct and useless: each one is [Middle Man](https://aipatterns.dev/refactoring/smells/middle-man.md) with an HTTP client. The chain is [Message Chains](https://aipatterns.dev/refactoring/smells/message-chains.md) across a network. ## Treatment Consolidate the endpoints into task-shaped tools: one tool per task the consumer actually performs, returning only the fields that task reads, with a description that says when to use it and when not to. See [Consolidate Tools](https://aipatterns.dev/refactoring/techniques/consolidate-tools.md) for the step-by-step. If the consumer is a human browsing a dashboard, the diagnosis changes: people can chain calls and skim columns, and the raw surface may be the product. The smell is specific to consumers that dispatch on descriptions and pay per token - agents and automated callers. ## Payoff - Tasks stop being call chains: one call, one answer, fewer round trips and failures. - Responses shrink to what is read, which for machine consumers is a direct token saving on every call. - Descriptions that say when to use a tool let consumers pick right the first time; overlapping guesses disappear. - The upstream API can change behind the task-shaped tool without breaking every caller. ## When to ignore - The consumer is a human with a screen and time - they can chain and skim; optimize their UX, not their call count. - The wrapper's job is fidelity: a debugging tool that mirrors the upstream API one-to-one is the product, not a smell. - There is exactly one task per endpoint and no chain forms - a thin wrapper serving one real task is just an API. ## Agent checklist - [ ] Each tool's description states when to use it, not what it wraps. - [ ] No consumer task requires chaining two or more of these tools to get its answer. - [ ] Responses contain only fields at least one caller reads. - [ ] No two tools on the surface could both plausibly serve the same call. - [ ] Tool count is derived from the task list, not from the upstream API's endpoint list.