The 4 Rules of Simple Design: A Practical Guide with TypeScript — DeepSeek Blog | Neura Market
    Neura MarketNeura Market/DeepSeek
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsBlogVideosGuidesCoursesCommunityTrendingGenerate
    DeepSeekBlogThe 4 Rules of Simple Design: A Practical Guide with TypeScript
    Back to Blog
    The 4 Rules of Simple Design: A Practical Guide with TypeScript
    programming

    The 4 Rules of Simple Design: A Practical Guide with TypeScript

    Maxime Sahroui February 13, 2026
    0 views

    Kent Beck introduced the 4 Rules of Simple Design as part of Extreme Programming in the late 1990s....

    Kent Beck introduced the **4 Rules of Simple Design** as part of Extreme Programming in the late 1990s. Decades later, they remain one of the most elegant and practical frameworks for writing maintainable software. These rules define what "simple" truly means in code — not naive or dumbed-down, but *intentionally minimal*. The rules are ordered by priority. You never sacrifice a higher-priority rule to satisfy a lower one. ## Rule 1: Passes the Tests The code must work. It must do what it's supposed to do, and you must be able to prove it. A beautiful design that produces wrong results is worthless. This rule also implies that tests *exist*. Untested code is not "simple" — it's unknown. ```typescript // ❌ No tests, no confidence function calculateDiscount(price: number, tier: string): number { if (tier === "gold") return price * 0.8; if (tier === "silver") return price * 0.9; return price; } // ✅ Tested behavior — we know exactly what this does describe("calculateDiscount", () => { it("applies 20% discount for gold tier", () => { expect(calculateDiscount(100, "gold")).toBe(80); }); it("applies 10% discount for silver tier", () => { expect(calculateDiscount(100, "silver")).toBe(90); }); it("applies no discount for unknown tiers", () => { expect(calculateDiscount(100, "bronze")).toBe(100); }); }); ``` The key insight: tests are not overhead. They are the *first* rule. Everything else builds on the confidence they provide. ## Rule 2: Reveals Intention Code should clearly communicate *what* it does and *why*. A reader — including your future self — should understand the purpose without digging through implementation details. ```typescript // ❌ What does this do? You have to execute it mentally. function process(items: { a: number; s: string; d: Date }[]) { return items.filter((i) => { const n = Date.now(); const t = i.d.getTime(); return n - t < 86400000 && i.a > 0 && i.s === "A"; }); } // ✅ The code reads like a specification type OrderItem = { amount: number; status: "active" | "cancelled" | "pending"; createdAt: Date; }; const ONE_DAY_MS = 24 * 60 * 60 * 1000; function isRecentActiveOrder(order: OrderItem): boolean { const age = Date.now() - order.createdAt.getTime(); return age < ONE_DAY_MS && order.amount > 0 && order.status === "active"; } function getRecentActiveOrders(orders: OrderItem[]): OrderItem[] { return orders.filter(isRecentActiveOrder); } ``` Notice what changed: meaningful types, descriptive names, extracted constants, and a named predicate function. The behaviour is identical — but the *intention* is now unmistakable. ### Naming: clarity is not verbosity Revealing intention doesn't mean cramming every detail into a name. Over-descriptive names add cognitive load without adding clarity. If the name repeats context already obvious from the module, the type signature, or the surrounding code, it's noise — not intention. ```typescript // ❌ Too verbose — the names repeat context that's already clear // file: userService.ts function getUserByIdFromDatabaseAndReturnUserObject(userId: string): Promise<User> { ... } function validateUserEmailAddressFormat(emailAddress: string): boolean { ... } function checkIfUserAccountIsCurrentlyActive(user: User): boolean { ... } function sendPasswordResetEmailNotificationToUser(user: User): Promise<void> { ... } // ❌ Too vague — the names tell you nothing function get(id: string): Promise<unknown> { ... } function check(s: string): boolean { ... } function do(u: unknown): Promise<void> { ... } // ✅ Concise — context is already provided by the module and types // file: users.ts function findById(userId: string): Promise<User> { ... } function isValidEmail(email: string): boolean { ... } function isActive(user: User): boolean { ... } function sendPasswordReset(user: User): Promise<void> { ... } // The caller reads naturally: import * as users from "./users"; const user = await users.findById(id); if (users.isActive(user)) { ... } ``` The module name (`users`) already provides the domain context. The type signature (`User`, `string`) already tells you what goes in and what comes out. The function name only needs to express the *action* — everything else is redundant. Good naming lives in the sweet spot between cryptic and encyclopedic. ## Rule 3: No Duplication (DRY) Every piece of knowledge should live in exactly one place. Duplication isn't just repeated code — it's repeated *concepts*. When the same business rule appears in two places, you'll eventually change one and forget the other. ```typescript // ❌ The pricing logic is duplicated across two functions // invoice.ts type LineItem = { price: number; quantity: number; }; function calculateSubtotal(items: LineItem[]): number { let total = 0; for (const item of items) { total += item.price * item.quantity; if (item.quantity >= 10) { total -= item.price * item.quantity * 0.1; // bulk discount } } return total; } function generatePreview(items: LineItem[]): string { let total = 0; for (const item of items) { total += item.price * item.quantity; if (item.quantity >= 10) { total -= item.price * item.quantity * 0.1; // bulk discount } } return `Estimated total: $${total.toFixed(2)}`; } // ✅ Single source of truth — each concept lives in one place // pricing.ts const BULK_THRESHOLD = 10; const BULK_DISCOUNT_RATE = 0.1; type LineItem = { price: number; quantity: number; }; function lineItemSubtotal(item: LineItem): number { const gross = item.price * item.quantity; const discount = item.quantity >= BULK_THRESHOLD ? gross * BULK_DISCOUNT_RATE : 0; return gross - discount; } function calculateSubtotal(items: LineItem[]): number { return items.reduce((sum, item) => sum + lineItemSubtotal(item), 0); } // invoice.ts function generatePreview(items: LineItem[]): string { return `Estimated total: $${calculateSubtotal(items).toFixed(2)}`; } ``` The pricing logic now lives in `lineItemSubtotal`. Change the discount threshold once, and every consumer reflects it. No classes needed — just composable functions. ## Rule 4: Fewest Elements Once rules 1–3 are satisfied, use the minimum number of classes, functions, modules, and abstractions needed. Don't build for hypothetical futures. Don't add layers "just in case." ```typescript // ❌ Over-engineered for a simple need type NotificationStrategy = { send(message: string, recipient: string): Promise<void>; }; function createEmailStrategy(): NotificationStrategy { return { async send(message, recipient) { await sendEmail(recipient, message); }, }; } function createNotificationDispatcher(strategies: Record<string, NotificationStrategy>) { return async (type: string, message: string, recipient: string) => { const strategy = strategies[type]; if (!strategy) throw new Error(`Unknown type: ${type}`); await strategy.send(message, recipient); }; } const dispatch = createNotificationDispatcher({ email: createEmailStrategy(), }); // ✅ Just send the email. Add abstractions when you actually need them. async function sendNotification(message: string, recipient: string): Promise<void> { await sendEmail(recipient, message); } ``` The dispatcher, the strategy interface, and the factory function — none of them are needed when you only have one notification channel. If you add SMS later, *then* introduce the abstraction. Not before. ## The Priority Matters The ordering is everything. Here's how conflicts are resolved: - **Don't remove duplication (Rule 3) if it makes the code harder to understand (Rule 2).** Sometimes a little repetition is clearer than a clever abstraction. - **Don't simplify elements (Rule 4) if it reintroduces duplication (Rule 3).** An extracted helper function is worth having even if it adds a function to the module. - **Never sacrifice correctness (Rule 1) for anything.** A clean, readable, DRY codebase that produces wrong results is just a well-organised bug. ```typescript // Sometimes duplication is clearer than a forced abstraction // This is OK — Rule 2 (clarity) beats Rule 3 (DRY) function formatUserGreeting(user: User): string { return `Hello, ${user.firstName} ${user.lastName}!`; } function formatAdminGreeting(admin: Admin): string { return `Welcome back, ${admin.firstName} ${admin.lastName}. You have ${admin.pendingReviews} pending reviews.`; } // ❌ Don't force this into a shared function to eliminate duplication // The "shared" part (name formatting) is trivial, and the contexts are different ``` ## Summary | Priority | Rule | Question to Ask | |----------|------|-----------------| | 1 | Passes the Tests | Does it work? Can I prove it? | | 2 | Reveals Intention | Can someone else understand this without asking me? | | 3 | No Duplication | Is every concept expressed in exactly one place? | | 4 | Fewest Elements | Can I remove anything without breaking rules 1–3? | These four rules are not a checklist you apply once. They form a continuous loop—the heartbeat of refactoring. After every change: green tests → clarify intent → remove duplication → simplify. --- ## Appendix: AI Coding Rules The following rule set can be used as instructions for AI coding assistants (LLMs, copilots, agents) to enforce Kent Beck's 4 Rules of Simple Design. Copy and adapt them to your system prompt, project rules, or `.cursorrules` / `.claude` configuration. ```markdown # AI Coding Rules — 4 Rules of Simple Design You follow Kent Beck's 4 Rules of Simple Design in the order of their priority. When rules conflict, higher-priority rules always win. ## Rule 1: Passes the Tests (Highest Priority) - Every function you write or modify MUST have corresponding tests. - If modifying existing code, run existing tests first. Never break them. - Write the test BEFORE or alongside the implementation, never as an afterthought. - Tests must cover: expected behaviour, edge cases, and error paths. - If you are unsure whether behaviour is correct, ask — do not guess. - Never mark a task as complete if tests are failing. ## Rule 2: Reveals Intention - Use descriptive, specific names for variables, functions, types, and modules. Bad: `data`, `process`, `handle`, `item`, `tmp`, `val` Good: `unpaidInvoices`, `calculateShippingCost`, `OrderStatus` - But avoid over-descriptive names that repeat context already clear from the module, the type signature, or the surrounding code. Bad: `getUserByIdFromDatabaseAndReturnUserObject`, `checkIfUserIsActive` Good: `findById` (in a users module), `isActive` (takes a User) - Extract magic numbers and strings into named constants. - Use TypeScript types to document the shape of data. Prefer `type` over `interface` — use `interface` only when declaration merging is needed. - Prefer named functions over inline lambdas for non-trivial logic. - Each function should do one thing. If you need "and" to describe it, split it. - Comments explain WHY, never WHAT. The code itself explains what. ## Rule 3: No Duplication - Never copy-paste logic. Extract shared behaviour into a function, type, or module. - Duplication includes: repeated business rules, repeated conditionals, repeated data transformations, and repeated structural patterns. - When you spot duplication, refactor it — even if you didn't introduce it. - BUT: do not force an abstraction when two pieces of code only look similar on the surface but represent different concepts (Rule 2 takes priority). ## Rule 4: Fewest Elements (Lowest Priority) - Do not create abstractions for hypothetical future requirements. - Do not introduce unnecessary abstractions, factories, strategies, or wrappers unless there is a concrete, current need. - Prefer functions over classes when no state management is required. - Prefer module-level functions with namespace imports (`import * as users`) over classes with methods. Use types for data shapes, functions for behaviour. - Remove dead code, unused imports, and unnecessary parameters. - If an abstraction makes the code harder to follow without reducing real duplication, remove it. - One file with 3 clear functions is better than 3 files with 1 function each, unless the functions serve genuinely different domains. ## Conflict Resolution When rules conflict, apply this priority: 1. Working, tested code (Rule 1) > everything. 2. Clarity (Rule 2) > DRY (Rule 3). A little repetition is OK if the alternative is an unclear abstraction. 3. DRY (Rule 3) > Minimalism (Rule 4). An extra function to eliminate duplication is justified. 4. Never add complexity to satisfy Rule 4. Minimalism means removing unnecessary things, not avoiding necessary ones. ## Refactoring Loop After every change, mentally run this loop: 1. Are all tests green? → If not, fix. 2. Does the code clearly express intent? → If not, rename/restructure. 3. Is there duplication? → If yes, extract. 4. Can anything be removed without breaking rules 1-3? → If yes, remove. ```

    Tags

    programmingtypescriptcleancodebeginners

    Comments

    More Blog

    View all
    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠ai

    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠

    Hi everyone! 👋 I’m Tara, a Senior Software Engineer and Consultant. Over the years, I've jumped...

    T
    tworrell
    Local AI Will Save Us All (The Math Says So, Trust Me)ai

    Local AI Will Save Us All (The Math Says So, Trust Me)

    Every few weeks a take goes viral in tech circles making the case for ditching cloud AI and running...

    S
    Sebastian Schürmann
    Lost in the AI Hype, I Started Smallai

    Lost in the AI Hype, I Started Small

    And it helped me get back into tech without drowning TL;DR at the end Coming back to...

    R
    Rohini Gaonkar
    Building a Replay-Tested Interactive Brokers Client in Gogo

    Building a Replay-Tested Interactive Brokers Client in Go

    I wanted an IBKR library that felt like Go and had testing I could trust. So I wrote one.

    T
    Thomas Marcelis
    Playwright in Pictures: Fully Parallel Modeplaywright

    Playwright in Pictures: Fully Parallel Mode

    Playwright’s fullyParallel mode is often treated as a simple performance switch. In practice, it...

    V
    Vitaliy Potapov
    Designing a CLI for Both Humans and Agentscli

    Designing a CLI for Both Humans and Agents

    Learn how Alpic designed its CLI for both human developers and AI agents — covering tradeoffs like polling, context windows, interactivity, and statelessness.

    J
    Julien Vallini

    Stay up to date

    Get the latest DeepSeek prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for DeepSeek and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    © 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.