The 4 Rules of Simple Design: A Practical Guide with…
    Neura MarketNeura Market/DeepSeek
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsGamesBlogVideosGuidesCoursesCommunityTrending
    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.

    // ❌ 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.

    // ❌ 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.

    // ❌ 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.

    // ❌ 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."

    // ❌ 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.
    // 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

    PriorityRuleQuestion to Ask
    1Passes the TestsDoes it work? Can I prove it?
    2Reveals IntentionCan someone else understand this without asking me?
    3No DuplicationIs every concept expressed in exactly one place?
    4Fewest ElementsCan 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.

    # 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
    Five Gemma-4 models, one accelerator: what porting E2B 31B to AWS Inferentia2 taught megemma

    Five Gemma-4 models, one accelerator: what porting E2B 31B to AWS Inferentia2 taught me

    I ported the whole Gemma-4 family — E2B, E4B, 12B, 31B, and the 26B-A4B MoE — to run on...

    X
    xbill
    Hey DEV, I'm Tobore. Let's actually connect.community

    Hey DEV, I'm Tobore. Let's actually connect.

    Hey DEV, I'm Tobore. Let's actually connect. I've been on here for a while now, mostly writing and...

    L
    Laurina Ayarah
    I burned through thousands of AI tokens. Then a friend did it for freeai

    I burned through thousands of AI tokens. Then a friend did it for free

    (yep, kinda clickbait, just for the funsies 😊) At the beginning of the year, I relaunched my...

    P
    Paulo Henrique
    Claude might be saturating your machineai

    Claude might be saturating your machine

    My laptop was sitting idle with the fan at full tilt. Nothing was running that I knew of. The culprit...

    S
    Sidhant Panda
    Automated GitHub Code Reviews Using Google Geminigithubactions

    Automated GitHub Code Reviews Using Google Gemini

    I Built a Thing! TL;DR — Google Gemini-based Pull Request reviews and Issue Triaging for...

    D
    Darren "Dazbo" Lester
    What is an "agentic harness," actually?ai

    What is an "agentic harness," actually?

    I've been hearing the word "harness" thrown around a lot lately. I assumed it just meant "the IDE" or...

    T
    Tilde A. Thurium

    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.

    Neura Market

    Custom AI Systems & Services

    Our team of experienced AI builders will help build custom AI systems, workflows, and solutions for your business.

    Request custom work

    Ready-made automations for this

    Workflows from the Neura Market marketplace related to this DeepSeek resource

    • Add TypeScript IntelliSense Support to Code Nodes with JSDocn8n · $4.99 · Related topic
    • Create a REST API for PDF Digital Signatures with Webhooksn8n · $24.99 · Related topic
    • Build AI Agents with Think-Plan-Act Architecture Using Llama-4 Reasoningn8n · $24.99 · Related topic
    • Auto-Publish Content to 9 Social Platforms with Blotato & Airtablen8n · $24.99 · Related topic
    Browse all workflows