| name | kent-implementation-patterns |
| description | Use when designing a new feature/class/module or reviewing an implementation, diff, or PR — applies Kent Beck's Implementation Patterns (values, principles, and patterns) as a systematic lens for code that communicates intent and is cheap to change. Triggers on "design this", "review this code", "is this good code", code quality, readability, naming, structure. |
Implementation Patterns (Kent Beck)
Overview
Code is communication. Most of a program's cost comes after first deploy, and
most of that is spent understanding existing code. So the goal is code that
communicates intent to the next reader and is cheap to change.
Don't review or design by ad-hoc instinct. Run a fixed lens so coverage is
repeatable and you catch the structural issues that aren't staring back at you as
bugs. The lens has three layers — apply them top-down:
Values (why) → Principles (how to decide) → Patterns (concrete moves).
Values
- Communication — write for the next reader, not just the compiler.
- Simplicity — remove excess complexity so essential complexity shows.
- Flexibility — keep options open, but get it from simplicity + tests, not
speculative config/abstraction. (Flexibility is the most-abused excuse for bad design.)
Conflict? Prefer communication > simplicity > speculative flexibility.
Principles (the six checks)
| Principle | The question to ask |
|---|
| Local Consequences | Does a change here stay local, or ripple there? |
| Minimize Repetition | Is this idea expressed in more than one place? |
| Logic and Data Together | Does this logic live next to the data it uses? |
| Symmetry | Is the same idea expressed the same way everywhere? |
| Declarative Expression | Could this control flow be stated as a fact/table? |
| Rate of Change | Are things that change together grouped, and things that change on different clocks separated? |
patterns.md holds the full 77-pattern catalog, grouped by the principle each serves.
When designing a feature
Work top-down through the lens — decide placement and names before writing bodies.
- Find the metaphor and name it. Name the central class/concept first (one
strong word beats a descriptive phrase). Good names cascade into everything else.
- Put logic with its data. For each behavior, ask which object owns the data
it needs — put the behavior there. Reach for a Value Object (immutable,
value-equality) for value-like concepts (Money, Range, Coordinate); keep
state-changing objects at the edges. Resist a "Manager/Processor/Util" that
reaches into other objects' data.
- Pick the simplest structure that communicates. One clear class beats a
speculative hierarchy. Express number with a collection; express a choice
with a Conditional first — promote to polymorphism (Choosing Message /
Delegation) only when the same axis of variation repeats.
- Defer flexibility. Don't add config/abstraction/extension points for a future
that may not arrive. Add them when the need is real.
- Check Symmetry and Rate of Change. Parallel operations should have parallel
shapes. Fields/concepts that change on different clocks belong in different
objects (the classic Rate-of-Change move: split them out).
Then state, briefly, which patterns you applied and why — naming them makes the
design reviewable.
When reviewing an implementation
Read the code as a reader first (does it tell a story?), then run the six
principle-checks above as a pass. For every finding:
- Name the principle violated and the pattern that fixes it — not just "this
is messy." This makes the review teachable and the fix obvious.
- Prefer placement over patching. When you spot duplicated calculation, ask
whose data is this? and move the logic onto that object (Logic-and-Data-Together
/ Value Object) — don't just hoist a private helper that keeps logic away from
data. This is the single most common miss.
- Order findings by cost of change, not by cosmetics: non-local consequences and
duplication first, naming/readability next, style last.
Smell → pattern quick reference
| Smell in the code | Reach for |
|---|
Nested if pyramid, let result then assign in branches | Guard Clause (early returns) |
| A method mixing high-level steps with low-level twiddling | Composed Method (one level of abstraction) |
Name describes how (linearSearch) | Intention-Revealing Name (name the what) |
| Same calc/loop copy-pasted across methods | Minimize Repetition → extract onto the owning object |
Primitive trio passed together (amount, currency, …) | Parameter Object → often a missing Value Object |
switch/if on a type tag, repeated on the same axis | Choosing Message / Delegation |
Magic number/string (0.2, 0x0004) | Constant with an intent-revealing name |
| Getter/setter pulling data out to compute elsewhere | move the computation in (Logic and Data Together) |
process() validates but refund() doesn't; mismatched return shapes | Symmetry |
Long if/elseif mapping input→output with no real sequence | Declarative Expression (table/map) |
Worked example
Mixed-abstraction, nested, duplicated:
process(o: any, db: any) {
if (o != null) {
if (o.items != null && o.items.length > 0) {
if (o.customer != null && o.customer.active) {
let t = 0;
for (let i = 0; i < o.items.length; i++) t += o.items[i].price * o.items[i].qty;
if (o.customer.tier == "gold") t -= t * 0.1;
else if (o.customer.tier == "silver") t -= t * 0.05;
o.total = t; o.tax = t * 0.2; o.grandTotal = o.total + o.tax;
db.flags |= 0x0004;
db.save(o); return { ok: true, total: o.grandTotal };
} else return { ok: false, error: "inactive customer" };
} else return { ok: false, error: "no items" };
} else return { ok: false, error: "null order" };
}
Through the lens:
- Logic and Data Together / Value Object — subtotal+discount+tax is value-like and
duplicated in
refund(). It belongs on an immutable Money/Order value, not
copy-pasted in a Processor. (This is the finding a generic review misses — it
extracts a private helper instead of moving logic to the data.)
- Guard Clause — replace the four-deep pyramid with early returns; the
let result accumulator disappears.
- Composed Method — every line of
process should sit at one abstraction level;
db.flags |= 0x0004 (a Constant named FLAG_ORDER_PROCESSED) doesn't belong
beside business steps.
- Symmetry —
refund() skips the validation process() does and returns a bare
number while process() returns a result object. Make their shapes parallel.
- Declarative Expression — tiers→rates is a fact:
{ gold: 0.1, silver: 0.05 }.
class Money { }
process(order: Order, repo: OrderRepo): Result {
if (order == null) return fail("null order");
if (!order.items.length) return fail("no items");
if (!order.customer.active) return fail("inactive customer");
const total = order.discountedSubtotal().withTax(TAX_RATE);
repo.save(order.markProcessed(total));
return ok(total);
}
Common mistakes
- Reviewing/designing ad-hoc. If you didn't run the six principle-checks, you
reviewed by vibe. Run them; name what you find.
- Extracting helpers instead of relocating logic. Hoisting a private method
removes duplication but leaves logic away from its data. Ask "whose data is this?"
- Justifying complexity with "flexibility." Speculative config/abstraction/
extension points are a cost now for a benefit that may never come. Defer them.
- Applying patterns mechanically. The catalog is not a checklist to satisfy.
Pull a pattern just before you need it; the values and principles decide whether.
- Naming the smell but not the fix. "This is messy" isn't actionable. Name the
principle and the pattern.
- Optimizing style before consequences. Fix non-local change and duplication
before bikeshedding
== vs ===.
Note on frameworks / libraries
When clients can't change with you (public API, library, plugin host), the
assumption "change is cheap" breaks — there, add complexity to protect clients:
hide fields, reveal only stable surface, prefer a Library-Class API, and stage
breaking changes (deprecate, parallel architectures, migration tools). See the
Evolving Frameworks section in patterns.md.