Claude Cursor GitHub Copilot Skill

pragmatic-programmer

Apply meta-principles of software craftsmanship: DRY, orthogonality, tracer bullets, and design by contract. Use when the user mentions "best practices", "pragmatic approach", "broken windows", "tracer bullet", "software craftsmanship", "avoid technical debt", "code ownership", o

LLM Mart · 0 points · 7 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download wondelai-skills-pragmatic-programmer-eade5d1.zip · 38 KB
Part of wondelai/skills — 183 skills

Install

skills CLI npx skills add https://github.com/wondelai/skills/tree/main/pragmatic-programmer
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wondelai-skills@llmmart
Git git clone https://github.com/wondelai/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wondelai/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

The Pragmatic Programmer Framework

A systems-level approach to software craftsmanship from Hunt & Thomas' "The Pragmatic Programmer" (20th Anniversary Edition). Apply these meta-principles when designing systems, reviewing architecture, writing code, or advising on engineering culture -- how to think about software, not just how to write it.

Core Principle

Care about your craft. Software development demands continuous learning, disciplined practice, and personal responsibility -- pragmatic programmers think beyond the immediate problem to context, trade-offs, and long-term consequences. Great software comes from great habits: avoid duplication ruthlessly, keep components orthogonal, and treat every line of code as a living asset that must earn its place. The goal is not perfection -- it is systems that are easy to change, easy to understand, and easy to trust.

Scoring

Goal: 10/10. Score against the seven Quick Diagnostic rows: award ~1.4 points per row answered "yes" (7 yes = 10). Then band the result:

  • 9-10: every principle holds -- DRY knowledge, orthogonal layers, a working tracer slice, contracts at boundaries, no broken windows, reversible vendor/DB choices, ranged estimates.
  • 5-6: 1-2 violations that cost real change-effort (e.g. business logic coupled to the DB, single-point estimates).
  • <=3: pervasive duplication, global state, or accumulated broken windows -- entropy is winning.

Always state the score, name the failing diagnostic rows, and give the specific fix from the Action column to reach 10/10.

The Seven Meta-Principles

Seven principles for building software that lasts:

1. DRY (Don't Repeat Yourself)

Core concept: Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. DRY is about knowledge, not code -- duplicated logic, business rules, or configuration are far more dangerous than duplicated syntax.

Why it works: Duplicated knowledge must be changed in multiple places; eventually one gets missed, introducing inconsistency. DRY reduces the surface area for bugs and makes systems easier to change.

Key insights:

  • DRY applies to knowledge and intent, not textual similarity -- two identical code blocks serving different business rules are NOT duplication
  • Four types of duplication: imposed (environment forces it), inadvertent (developers don't realize), impatient (too lazy to abstract), inter-developer (multiple people duplicate)
  • Comments that restate the code violate DRY -- explain why, not what
  • Database schemas, API specs, and documentation duplicate knowledge unless generated from a single source
  • The opposite of DRY is WET: "Write Everything Twice" or "We Enjoy Typing"

Code applications:

Context Pattern Example
Config values Single source of truth DB connection in one env file, referenced everywhere
Validation rules Shared schema One JSON Schema or Zod schema for client and server
API contracts Generate from spec OpenAPI spec generates types, docs, and client code

See: references/dry-orthogonality.md when classifying a specific duplication or deciding whether two code blocks are truly the same knowledge -- per-type examples and mitigations for the four duplication types.

2. Orthogonality

Core concept: Two components are orthogonal if changes in one do not affect the other. Design systems where components are self-contained, independent, and have a single, well-defined purpose.

Why it works: Decoupling localizes change -- a fix in one module can't ripple into unrelated ones, so blast radius stays bounded. Change the database layer and the UI should not break; change the auth provider and business logic should not care.

Key insights:

  • Ask: "If I dramatically change the requirements behind a function, how many modules are affected?" The answer should be one
  • Eliminate effects between unrelated things -- a logging change should never break billing
  • Layered architectures promote orthogonality: presentation, domain logic, data access
  • Avoid global data -- every consumer of global state is coupled to it
  • Frameworks that force you to inherit from their classes reduce orthogonality

Code applications:

Context Pattern Example
Architecture Layered separation Controller -> Service -> Repository, each replaceable
Dependencies Dependency injection Pass a Notifier interface, not a SlackClient concrete class
Testing Isolated unit tests Test business logic without database, network, or filesystem

See: references/dry-orthogonality.md when measuring coupling or refactoring toward decoupled layers -- the change-impact and stranger tests, layered-architecture diagram, and the helicopter analogy.

3. Tracer Bullets and Prototypes

Core concept: Tracer bullets are end-to-end implementations connecting all layers of the system with minimal functionality. Unlike prototypes (which are throwaway), tracer bullet code is production code -- thin but real.

Why it works: Tracer bullets give immediate end-to-end feedback before you invest in filling out every feature. Users see something real, developers have a framework to build on, and integration issues surface early.

Key insights:

  • Tracer bullet: thin but complete path through the system (UI -> API -> DB) -- you keep it
  • Prototype: focused exploration of a single risky aspect -- you throw it away
  • Use tracer bullets when "shooting in the dark" -- vague requirements, unproven architecture
  • If a tracer misses, adjust and fire again -- the cost of iteration is low
  • Label prototypes clearly as throwaway -- never let one become production code

Code applications:

Context Pattern Example
New project Vertical slice One feature end-to-end: button -> API -> DB -> response
Uncertain tech Spike prototype Test WebSocket performance before committing
Microservice Walking skeleton Hello-world service through the full CI/CD pipeline

See: references/tracer-bullets.md when deciding tracer vs. prototype on a new project or building a walking skeleton -- the shooting-in-the-dark decision, iteration loop, and common pitfalls.

4. Design by Contract and Assertive Programming

Core concept: Define and enforce the rights and responsibilities of software modules through preconditions (what must be true before), postconditions (what is guaranteed after), and invariants (what is always true). When a contract is violated, fail immediately and loudly.

Why it works: Contracts make assumptions explicit. Instead of silently corrupting data or limping along in an invalid state, the system crashes at the point of the problem -- dead programs tell no lies.

Key insights:

  • Preconditions: caller's responsibility -- "I accept only positive integers"
  • Postconditions: routine's guarantee -- "I will return a sorted list"
  • Invariants: always true -- "Account balance never goes negative"
  • Crash early: a dead program does far less damage than a crippled one
  • Use assertions for things that should never happen; error handling for things that might
  • In dynamic languages, implement contracts through runtime checks and guard clauses

Code applications:

Context Pattern Example
Function entry Precondition guard assert age >= 0, "Age cannot be negative" at function start
Class state Invariant validation validate! called after every state mutation
API boundary Schema validation Validate request body against schema before processing

See: references/contracts-assertions.md when adding contracts to a routine or deciding assertion vs. error handling -- worked pre/post/invariant patterns, dynamic-language guard clauses, and the assertions-vs-error-handling boundary.

5. The Broken Window Theory

Core concept: One broken window -- a badly designed piece of code, a poor management decision, a hack that "we'll fix later" -- starts the rot. Once a system shows neglect, entropy accelerates and discipline collapses.

Why it works: Psychology. When code is clean, developers feel social pressure to keep it that way; when code is already messy, the threshold for adding more mess drops to zero. Quality is a team habit, not an individual heroic effort.

Key insights:

  • Don't leave broken windows (bad designs, wrong decisions, poor code) unrepaired
  • If you can't fix it now, board it up: a TODO with a ticket, a disabled feature, a stub
  • Be a catalyst for change: show people a working glimpse of the future (stone soup)
  • Watch for slow degradation (boiled frog) -- monitor tech debt metrics over time
  • The first hack is the most expensive because it gives permission for all subsequent hacks

Code applications:

Context Pattern Example
Legacy code Board up windows Wrap bad code in a clean interface before adding features
Code review Zero-tolerance for new debt Reject PRs adding // TODO: fix later without a ticket
Tech debt Debt budget Allocate 20% of each sprint to fixing broken windows

See: references/broken-windows.md when a team is normalizing neglect or you need to drive a turnaround -- repair strategies, the stone-soup catalyst play, and building a culture of quality.

6. Reversibility and Flexibility

Core concept: There are no final decisions. Build systems that make it easy to change your mind about databases, frameworks, vendors, architecture, and deployment targets -- the cost of change should be proportional to the scope of change.

Why it works: Requirements change, vendors get acquired, technologies fall out of favor. If your architecture hard-codes assumptions about any of these, every change becomes a rewrite; flexible architecture treats decisions as configuration, not structure.

Key insights:

  • Abstract third-party dependencies behind your own interfaces -- never let vendor APIs leak into business logic
  • The "forking road" test: could you switch from Postgres to DynamoDB in a week? If not, you're coupled
  • Metadata-driven systems (config files, feature flags) are more flexible than hard-coded logic
  • YAGNI applies to premature abstraction too -- don't build flexibility you don't need yet
  • Reversibility is not predicting the future; it's not painting yourself into a corner

Code applications:

Context Pattern Example
Database Repository pattern Business logic calls repo.save(user), not pg.query(...)
External API Adapter/wrapper PaymentGateway interface wraps Stripe; swap to Braintree later
Feature flags Runtime toggles New checkout flow behind a flag, rollback in seconds

See: references/reversibility.md when committing to a vendor or framework, or weighing how reversible a decision must be -- per-layer reversibility patterns, the forking-road test, and when NOT to optimize for reversibility.

7. Estimation and Knowledge Portfolio

Core concept: Learn to estimate reliably by understanding scope, building models, decomposing into components, and assigning ranges. Manage your learning like a financial portfolio: invest regularly, diversify, and rebalance.

Why it works: Honest estimation builds trust with stakeholders ("1-3 weeks" beats a confidently wrong "2 weeks"). A knowledge portfolio keeps you relevant as technologies shift -- the programmer who stops learning stops being effective.

Key insights:

  • Ask "what is this estimate for?" -- context determines precision (budget planning vs. sprint planning)
  • Use PERT: (Optimistic + 4x Most Likely + Pessimistic) / 6
  • Decompose into components and estimate each; the sum is more accurate than a single guess
  • Keep an estimation log: compare estimates to actuals and calibrate
  • Portfolio rules: invest regularly (learn weekly), diversify beyond your stack, mix safe and speculative bets, learn emerging tech early (buy low)

Code applications:

Context Pattern Example
Sprint planning Range estimates "3-5 days" with confidence level, not a single number
New technology Time-boxed spike "2 days evaluating; then I can estimate properly"
Learning Weekly investment 1 hour/week on a new language, tool, or domain

See: references/estimation-portfolio.md when producing an estimate you'll be held to or calibrating past misses -- the PERT and decomposition procedures, an estimation-log calibration loop, and portfolio rebalancing.

Common Mistakes

Mistake Why It Fails Fix
DRY-ing similar-looking code that serves different purposes Couples unrelated concepts; changes to one break the other Only DRY knowledge, not coincidental code similarity
Skipping tracer bullets, building layer-by-layer Integration issues surface late; no end-to-end feedback Build one thin vertical slice first
Ignoring broken windows "because we'll refactor later" Entropy accelerates; later never comes; morale drops Fix immediately or board up with a tracked ticket
Estimates as single-point commitments False precision erodes trust when missed Always give ranges with confidence levels
Making everything "flexible" upfront Over-engineering; abstraction without evidence of need Add flexibility when you have concrete evidence you'll need it
Removing production assertions "for performance" Bugs assertions would catch now silently corrupt data Keep critical assertions; benchmark before removing any
Global state "for convenience" Destroys orthogonality; everything coupled to everything Use dependency injection and explicit parameters

Quick Diagnostic

Question If No Action
Can I change the database without touching business logic? Orthogonality violation Introduce repository/adapter pattern
Do I have an end-to-end slice working? Missing tracer bullet Build one vertical slice before expanding
Is every business rule defined in exactly one place? DRY violation Identify the authoritative source; remove duplicates
Would a new developer call this codebase "clean"? Broken windows present Schedule a dedicated cleanup sprint
Do my estimates include ranges and confidence levels? Estimation problem Switch to PERT or range-based estimates
Can I roll back this deployment in under 5 minutes? Reversibility gap Add feature flags and blue-green deploys
Am I learning something new every week? Knowledge portfolio stagnant Schedule weekly learning time and track it

Further Reading

About the Authors

Andrew Hunt and David Thomas co-founded the Pragmatic Bookshelf and were among the 17 original authors of the Agile Manifesto. Thomas coined "DRY" and "Code Kata" and co-authored Programming Ruby (the Pickaxe book); Hunt focuses on how teams learn, communicate, and maintain quality. Together they wrote The Pragmatic Programmer, one of the most influential software books ever published.

Files (skills)
  • references
    • broken-windows.md 11.5 KB
      # The Broken Window Theory in Software
      
      Deep reference for understanding and combating software entropy. Load when guidance is needed on technical debt, code quality culture, and strategies for maintaining clean codebases.
      
      ## Table of Contents
      1. [The Original Theory](#the-original-theory)
      2. [Software Entropy](#software-entropy)
      3. [Don't Live with Broken Windows](#dont-live-with-broken-windows)
      4. [Stone Soup and Boiled Frogs](#stone-soup-and-boiled-frogs)
      5. [Being a Catalyst for Change](#being-a-catalyst-for-change)
      6. [Identifying Broken Windows](#identifying-broken-windows)
      7. [Repair Strategies](#repair-strategies)
      8. [Building a Culture of Quality](#building-a-culture-of-quality)
      
      ---
      
      ## The Original Theory
      
      In 1982, criminologists James Q. Wilson and George L. Kelling published "Broken Windows," arguing that visible signs of disorder (a broken window left unrepaired) signal that nobody cares, which invites further disorder and eventually serious crime. The key insight: **neglect accelerates decay.**
      
      A building with one broken window will soon have all its windows broken. Not because criminals target buildings with broken windows, but because the broken window sends a signal: "Nobody cares about this building."
      
      ---
      
      ## Software Entropy
      
      Entropy is the tendency of systems toward disorder. In physics, it's a law. In software, it's a choice -- but it takes constant effort to resist.
      
      ### How Entropy Manifests in Code
      
      | Stage | Signs | Severity |
      |-------|-------|----------|
      | **Early decay** | A few TODO comments, one skipped test, a "temporary" workaround | Low -- easy to fix |
      | **Spreading neglect** | Growing list of known bugs, inconsistent naming, unused imports everywhere | Medium -- needs dedicated effort |
      | **Normalized deviance** | "That's just how this codebase is," copy-paste as standard practice, no code review standards | High -- requires culture change |
      | **Terminal entropy** | Nobody dares touch core modules, every change causes regressions, new features take 10x longer than expected | Critical -- rewrite may be cheaper |
      
      ### The Entropy Acceleration Curve
      
      Entropy doesn't increase linearly -- it accelerates:
      
      ```
      Quality
        ^
        |*
        | *
        |  *
        |   **
        |     ***
        |        ****
        |            ********
        |                    ****************
        +----------------------------------------> Time
      
        First broken window
            ↓
        Each subsequent one is easier to create
      ```
      
      The first broken window is the hardest to create because the codebase is clean. Every subsequent one is easier because the bar has already been lowered. This is why the first hack matters disproportionately.
      
      ---
      
      ## Don't Live with Broken Windows
      
      The pragmatic programmer's prime directive for code quality: **don't leave broken windows unrepaired.** Fix each one as soon as you discover it.
      
      ### What Counts as a Broken Window?
      
      - **Bad designs or architecture:** A module that grew beyond its original purpose and now has 15 responsibilities
      - **Wrong decisions left in place:** Using a SQL database for a graph problem because "we already have Postgres"
      - **Poor code:** Functions that are 200+ lines, nested ternaries, magic numbers, misleading variable names
      - **Disabled or ignored tests:** `@skip("fails sometimes")` or `// TODO: fix this test`
      - **Dead code:** Functions nobody calls, imports nobody uses, feature flags for features launched two years ago
      - **Missing error handling:** Bare except clauses, swallowed errors, TODO error handling
      - **Workarounds:** Code that exists solely to compensate for a bug elsewhere
      
      ### If You Can't Fix It Now: Board It Up
      
      Sometimes you genuinely can't fix a broken window immediately. In that case, **board it up** -- take some visible action to show it's being managed:
      
      | Action | How |
      |--------|-----|
      | **Create a ticket** | File a tracked issue with clear description and severity |
      | **Add a clear comment** | `# TECH-DEBT(JIRA-123): This needs refactoring because...` |
      | **Wrap it in a clean interface** | Put a well-designed adapter around the messy code |
      | **Disable the feature** | If it's broken, turn it off rather than shipping broken functionality |
      | **Add a failing test** | Document the expected behavior even if the implementation is wrong |
      
      The critical difference between a broken window and a boarded-up window: **visibility and intent.** A boarded-up window says "we know this is broken and we have a plan."
      
      ---
      
      ## Stone Soup and Boiled Frogs
      
      Two related parables from the pragmatic programmer:
      
      ### Stone Soup: Be a Catalyst
      
      In the folk tale, soldiers convince villagers to contribute ingredients to a pot of "stone soup." Each villager adds a little, and the result is better than anyone expected.
      
      **In software:** When you want to improve the codebase but face resistance ("we don't have time for refactoring"), use the stone soup strategy:
      
      1. Start small -- fix one broken window yourself
      2. Show the result -- "look, this module is now 50% simpler and fully tested"
      3. People join in -- others see the improvement and want to contribute
      4. The codebase improves incrementally -- without anyone approving a "big refactoring project"
      
      **Key insight:** It's easier to ask forgiveness than permission. Don't ask for a refactoring sprint -- just start improving code in every PR you touch.
      
      ### Boiled Frog: Watch for Gradual Decay
      
      A frog placed in boiling water jumps out immediately. A frog placed in slowly heating water doesn't notice the danger until it's too late.
      
      **In software:** Codebases rarely go from good to bad overnight. The decay is gradual:
      
      - Sprint 1: "Let's skip tests for this one ticket -- we're behind schedule"
      - Sprint 3: "Tests are too hard to write for this module -- just do manual QA"
      - Sprint 10: "We don't really write tests for this service"
      - Sprint 20: "Testing? We do production monitoring instead"
      
      **Prevention:** Track quality metrics over time and set alerts for negative trends:
      
      | Metric | Healthy Trend | Alarm |
      |--------|---------------|-------|
      | Test coverage | Stable or increasing | Dropped 5%+ in a quarter |
      | Build time | Stable or decreasing | Increased 50%+ in 6 months |
      | Linting violations | Decreasing | Increasing quarter over quarter |
      | Cyclomatic complexity | Stable per module | New modules starting above threshold |
      | Deployment frequency | Stable or increasing | Decreasing (fear of deploying) |
      | Time to resolve incidents | Stable or decreasing | Increasing (system is harder to debug) |
      
      ---
      
      ## Being a Catalyst for Change
      
      You don't need permission to improve code quality. Strategies for pragmatic programmers who want to raise the bar:
      
      ### The Boy Scout Rule
      
      "Leave the campground cleaner than you found it." Every time you touch a file:
      
      - Fix one naming issue
      - Extract one magic number into a named constant
      - Add one missing type annotation
      - Remove one unused import
      - Improve one error message
      
      These micro-improvements compound. In a team of 5 developers each making 3 PRs per week, that's 15 micro-improvements per week, 780 per year.
      
      ### The Strangler Pattern for Legacy Code
      
      Don't rewrite the old system. Strangle it gradually:
      
      1. Put a clean facade in front of the legacy module
      2. Route new features through the facade to new, clean implementations
      3. Gradually migrate old functionality behind the facade
      4. Eventually, the legacy module has no direct consumers and can be removed
      
      ### Leading by Example
      
      | Action | Impact |
      |--------|--------|
      | Write thorough tests in your PRs | Others see the standard and follow |
      | Add clear commit messages | Raises the bar for the whole team |
      | Document your architectural decisions | Creates a culture of documentation |
      | Refactor one thing in every PR | Normalizes continuous improvement |
      | Respond to broken windows in code review | Makes quality everyone's job |
      
      ---
      
      ## Identifying Broken Windows
      
      ### Code Review Checklist
      
      When reviewing code, look for these broken windows:
      
      | Category | Broken Window Signs |
      |----------|-------------------|
      | **Naming** | Variables named `x`, `temp`, `data`, `stuff`; misleading names; inconsistent conventions |
      | **Structure** | Functions > 50 lines; deeply nested logic; God classes with 20+ methods |
      | **Error handling** | Empty catch blocks; generic exception handling; errors swallowed silently |
      | **Tests** | No tests for new code; skipped tests; tests that test nothing meaningful |
      | **Dependencies** | Unused imports; outdated dependencies with known vulnerabilities |
      | **Duplication** | Copy-pasted blocks; same logic in multiple places |
      | **Comments** | Commented-out code; comments that contradict the code; "temporary" hacks from 2019 |
      
      ### Automated Detection
      
      Use tools to find broken windows automatically:
      
      | Tool Category | What It Catches |
      |---------------|----------------|
      | **Linters** (ESLint, Pylint, Clippy) | Style violations, unused variables, complexity |
      | **Static analysis** (SonarQube, CodeClimate) | Duplication, cognitive complexity, security issues |
      | **Dependency scanners** (Dependabot, Snyk) | Outdated or vulnerable dependencies |
      | **Test coverage** (Istanbul, Coverage.py) | Untested code paths |
      | **Dead code detectors** (knip, vulture) | Unused exports, unreachable code |
      
      ---
      
      ## Repair Strategies
      
      ### Triage: Which Windows to Fix First
      
      Not all broken windows are equally damaging. Prioritize:
      
      | Priority | Category | Rationale |
      |----------|----------|-----------|
      | **P0** | Security vulnerabilities | Active risk of exploitation |
      | **P1** | Data integrity issues | Silent corruption is worse than crashes |
      | **P2** | High-traffic code with poor error handling | Most likely to cause incidents |
      | **P3** | Core domain logic that's hard to understand | Slows down every feature |
      | **P4** | Cosmetic issues in rarely-touched code | Low impact, fix opportunistically |
      
      ### The 20% Rule
      
      Allocate approximately 20% of engineering capacity to fixing broken windows. This isn't a luxury -- it's maintenance:
      
      - 2 engineers out of 10 work on tech debt each sprint
      - Or: every engineer spends 1 day per week on cleanup
      - Or: one "cleanup sprint" every 5 sprints
      
      The exact model doesn't matter as much as the commitment. Teams that spend 0% on maintenance accumulate debt exponentially. Teams that spend 20% maintain a sustainable velocity.
      
      ---
      
      ## Building a Culture of Quality
      
      ### Team Practices
      
      | Practice | How It Helps |
      |----------|-------------|
      | **"No broken windows" as a team value** | Gives everyone permission and responsibility to maintain quality |
      | **Tech debt tracking** | Makes broken windows visible to management and the team |
      | **Quality gates in CI** | Prevents new broken windows from merging |
      | **Blameless postmortems** | Focus on the system (the broken window), not the person |
      | **Celebrate cleanup** | Recognize engineers who fix broken windows, not just those who ship features |
      
      ### Definition of Done
      
      A feature is not "done" when the code works. It's done when:
      
      - Code is clean and follows conventions
      - Tests are written and passing
      - Error handling is appropriate
      - Documentation is updated (if applicable)
      - No new broken windows were introduced
      - At least one existing broken window in the area was fixed
      
      ### The Social Contract
      
      Quality is a team decision. One person maintaining high standards while the team ignores broken windows is a losing battle. The conversation must happen:
      
      > "As a team, we agree: no new broken windows. If we find one, we fix it or board it up immediately. We allocate 20% of our capacity to this. This is not optional -- it's how we maintain our ability to ship quickly."
      
      When the whole team commits, the social pressure shifts from "don't slow us down with your cleanup" to "don't leave broken windows in your PRs." That shift is the turning point.
      
    • contracts-assertions.md 13.3 KB
      # Design by Contract and Assertive Programming
      
      Deep reference for making assumptions explicit through contracts and assertions. Load when guidance is needed on defensive programming, crash-early strategies, or formal precondition/postcondition patterns.
      
      ## Table of Contents
      1. [Design by Contract (DBC)](#design-by-contract-dbc)
      2. [Preconditions](#preconditions)
      3. [Postconditions](#postconditions)
      4. [Class Invariants](#class-invariants)
      5. [DBC in Dynamic Languages](#dbc-in-dynamic-languages)
      6. [Assertive Programming](#assertive-programming)
      7. [Dead Programs Don't Lie](#dead-programs-dont-lie)
      8. [Assertions vs. Error Handling](#assertions-vs-error-handling)
      
      ---
      
      ## Design by Contract (DBC)
      
      Design by Contract was formalized by Bertrand Meyer for the Eiffel programming language, but the principle applies universally. Every function or method has a contract:
      
      - **Preconditions:** What must be true before the routine is called (caller's responsibility)
      - **Postconditions:** What the routine guarantees will be true when it finishes (routine's responsibility)
      - **Class invariants:** What is always true about the object's state between method calls
      
      ### The Contract Metaphor
      
      Think of a function like a business contract:
      
      > "If you provide me with valid input (precondition), I guarantee I'll produce correct output (postcondition) and leave everything in a consistent state (invariant)."
      
      If the caller violates the precondition, the contract is void -- the routine owes nothing. If the routine violates the postcondition, it's a bug in the routine. If an invariant is violated, the system is in an invalid state and should halt.
      
      ### Why Contracts Matter
      
      | Without Contracts | With Contracts |
      |------------------|---------------|
      | Functions silently accept bad input | Bad input is caught immediately at the boundary |
      | Bugs propagate far from their source | Bugs are detected at the point of violation |
      | Debugging requires tracing through layers | Stack trace points directly to the violated contract |
      | Assumptions are implicit and undocumented | Assumptions are explicit and enforced |
      | Tests must guess at valid input ranges | Contracts document valid input ranges |
      
      ---
      
      ## Preconditions
      
      A precondition defines what must be true when a function is called. It is the **caller's responsibility** to satisfy the precondition.
      
      ### Examples Across Languages
      
      **Python:**
      ```python
      def transfer_funds(from_account, to_account, amount):
          # Preconditions
          assert amount > 0, f"Transfer amount must be positive, got {amount}"
          assert from_account.balance >= amount, (
              f"Insufficient funds: balance={from_account.balance}, amount={amount}"
          )
          assert from_account.id != to_account.id, "Cannot transfer to same account"
      
          # Implementation
          from_account.balance -= amount
          to_account.balance += amount
      ```
      
      **TypeScript:**
      ```typescript
      function transferFunds(from: Account, to: Account, amount: number): void {
        // Preconditions
        if (amount <= 0) throw new PreconditionError(`Amount must be positive: ${amount}`);
        if (from.balance < amount) throw new PreconditionError(`Insufficient funds`);
        if (from.id === to.id) throw new PreconditionError(`Cannot self-transfer`);
      
        from.balance -= amount;
        to.balance += amount;
      }
      ```
      
      ### Precondition Guidelines
      
      | Guideline | Rationale |
      |-----------|-----------|
      | Check preconditions at the start of the function | Fail fast before any side effects |
      | Use descriptive error messages | Include actual values so debugging is immediate |
      | Don't correct bad input silently | If amount is negative, don't negate it -- crash |
      | Document preconditions in the function's docstring | Callers need to know what's expected |
      | Preconditions should be cheap to check | If validation is expensive, it's a design smell |
      
      ### What Makes a Good Precondition?
      
      A precondition should be:
      - **Verifiable:** Can be checked programmatically
      - **Documented:** Callers can read and understand it
      - **Minimal:** Only what's truly necessary, not overly restrictive
      - **Stable:** Doesn't change between versions (it's part of the contract)
      
      ---
      
      ## Postconditions
      
      A postcondition defines what the function guarantees upon successful completion. It is the **routine's responsibility** to satisfy the postcondition.
      
      ### Examples
      
      **Python:**
      ```python
      def sort_list(items: list) -> list:
          result = sorted(items)
      
          # Postconditions
          assert len(result) == len(items), "Sort must preserve length"
          assert all(result[i] <= result[i+1] for i in range(len(result)-1)), (
              "Result must be sorted"
          )
          assert set(result) == set(items), "Sort must preserve elements"
      
          return result
      ```
      
      **Go:**
      ```go
      func Divide(a, b float64) float64 {
          // Precondition
          if b == 0 {
              panic("division by zero")
          }
      
          result := a / b
      
          // Postcondition
          if math.Abs(result*b - a) > 1e-10 {
              panic(fmt.Sprintf("postcondition failed: %f * %f != %f", result, b, a))
          }
      
          return result
      }
      ```
      
      ### Postcondition Patterns
      
      | Pattern | What It Checks | Example |
      |---------|---------------|---------|
      | **Preservation** | Output preserves a property of input | Sorted list has same length as input |
      | **Computation** | Result satisfies a mathematical relationship | `sqrt(x) * sqrt(x) ≈ x` |
      | **State change** | Object state changed correctly | Account balance decreased by exact transfer amount |
      | **No side effects** | Nothing unexpected changed | Other accounts' balances unchanged after transfer |
      | **Return type** | Result has expected structure | API response contains required fields |
      
      ---
      
      ## Class Invariants
      
      An invariant is a condition that must be true for every instance of a class at all times between method calls (it may temporarily be false during a method's execution).
      
      ### Examples
      
      ```python
      class BankAccount:
          def __init__(self, owner: str, initial_balance: float = 0):
              assert initial_balance >= 0, "Initial balance cannot be negative"
              self.owner = owner
              self._balance = initial_balance
              self._check_invariant()
      
          def _check_invariant(self):
              """Class invariant: balance is never negative."""
              assert self._balance >= 0, (
                  f"Invariant violated: balance={self._balance} for account {self.owner}"
              )
      
          def deposit(self, amount: float):
              assert amount > 0, f"Deposit must be positive: {amount}"  # precondition
              self._balance += amount
              self._check_invariant()
      
          def withdraw(self, amount: float):
              assert 0 < amount <= self._balance, (  # precondition
                  f"Invalid withdrawal: amount={amount}, balance={self._balance}"
              )
              self._balance -= amount
              self._check_invariant()
      
          @property
          def balance(self) -> float:
              return self._balance
      ```
      
      ### Common Invariant Patterns
      
      | Domain | Invariant |
      |--------|-----------|
      | **Financial** | Balance >= 0 (or >= overdraft limit) |
      | **Collection** | Size >= 0 and matches actual element count |
      | **Connection pool** | Active + idle = total allocated |
      | **State machine** | Current state is one of the defined states |
      | **Tree structure** | Every child has exactly one parent (except root) |
      | **Sorted container** | Elements are in order after every mutation |
      
      ---
      
      ## DBC in Dynamic Languages
      
      Languages like Python, JavaScript, and Ruby lack built-in contract support but can implement it through patterns:
      
      ### Guard Clauses
      
      The most common pattern -- check preconditions at the top of every function:
      
      ```python
      def process_order(order):
          if not order:
              raise ValueError("Order cannot be None")
          if not order.items:
              raise ValueError("Order must have at least one item")
          if order.total <= 0:
              raise ValueError(f"Order total must be positive: {order.total}")
      
          # Happy path follows...
      ```
      
      ### Decorator-Based Contracts (Python)
      
      ```python
      from functools import wraps
      
      def requires(condition_fn, message):
          def decorator(fn):
              @wraps(fn)
              def wrapper(*args, **kwargs):
                  if not condition_fn(*args, **kwargs):
                      raise PreconditionError(message)
                  return fn(*args, **kwargs)
              return wrapper
          return decorator
      
      def ensures(condition_fn, message):
          def decorator(fn):
              @wraps(fn)
              def wrapper(*args, **kwargs):
                  result = fn(*args, **kwargs)
                  if not condition_fn(result):
                      raise PostconditionError(message)
                  return result
              return wrapper
          return decorator
      
      @requires(lambda x: x >= 0, "Input must be non-negative")
      @ensures(lambda r: r >= 0, "Result must be non-negative")
      def sqrt(x):
          return x ** 0.5
      ```
      
      ### TypeScript Runtime Validation
      
      ```typescript
      import { z } from 'zod';
      
      const TransferInput = z.object({
        fromAccountId: z.string().uuid(),
        toAccountId: z.string().uuid(),
        amount: z.number().positive(),
      });
      
      function transferFunds(input: unknown) {
        // Precondition via schema validation
        const { fromAccountId, toAccountId, amount } = TransferInput.parse(input);
      
        // ...implementation
      }
      ```
      
      ---
      
      ## Assertive Programming
      
      Assertive programming extends DBC into a general philosophy: **if it can't happen, use assertions to ensure it doesn't.**
      
      ### The "It Can't Happen" Principle
      
      Every time you think "this can't happen," add an assertion:
      
      ```python
      def get_day_name(day_number):
          match day_number:
              case 1: return "Monday"
              case 2: return "Tuesday"
              case 3: return "Wednesday"
              case 4: return "Thursday"
              case 5: return "Friday"
              case 6: return "Saturday"
              case 7: return "Sunday"
              case _:
                  assert False, f"Invalid day number: {day_number}"  # "can't happen"
      ```
      
      ### Assertion Placement Guide
      
      | Location | What to Assert |
      |----------|---------------|
      | **Function entry** | Preconditions on parameters |
      | **Function exit** | Postconditions on return value |
      | **After external call** | Response is in expected format |
      | **Switch/match default** | "Impossible" cases |
      | **After complex computation** | Sanity check on intermediate results |
      | **After state mutation** | Class invariant still holds |
      
      ### Should Assertions Stay in Production?
      
      **Yes, with caveats.** The pragmatic approach:
      
      1. **Keep assertions that catch corruption** -- a negative bank balance, an invalid state transition, data integrity violations
      2. **Remove assertions that are performance-critical** -- only after benchmarking proves they matter
      3. **Never remove assertions just because "they slow things down"** -- measure first
      4. **Replace expensive assertions with cheaper approximations** if performance is genuinely impacted
      
      ---
      
      ## Dead Programs Don't Lie
      
      One of the most important pragmatic principles: **a program that crashes at the point of failure is far safer than one that limps along in an invalid state.**
      
      ### Why Crashing Is Better Than Continuing
      
      | Behavior | Consequence |
      |----------|------------|
      | Crash on invalid state | Bug found at the source, stack trace points to the problem |
      | Log a warning and continue | Invalid state propagates, corrupts data, discovered hours later |
      | Silently ignore the error | Data loss, security vulnerabilities, mysterious downstream failures |
      | Return a default value | Caller doesn't know something went wrong, makes decisions on bad data |
      
      ### Example: The Silent Corruption Problem
      
      ```python
      # DANGEROUS: silently handles bad data
      def get_user_age(user_data):
          try:
              return int(user_data.get("age", 0))
          except (ValueError, TypeError):
              return 0  # Silently returns 0 for invalid data
      
      # BETTER: crashes on bad data
      def get_user_age(user_data):
          age = user_data["age"]  # KeyError if missing
          if not isinstance(age, int) or age < 0:
              raise ValueError(f"Invalid age: {age}")
          return age
      ```
      
      The first version will happily process users with age 0, making them ineligible for age-restricted features, because the data was silently corrupted. The second version surfaces the problem immediately.
      
      ---
      
      ## Assertions vs. Error Handling
      
      This is a crucial distinction that many developers conflate:
      
      | Aspect | Assertions | Error Handling |
      |--------|-----------|---------------|
      | **For** | Things that should NEVER happen | Things that MIGHT happen |
      | **Examples** | Null pointer in non-nullable field, negative array index | Network timeout, file not found, invalid user input |
      | **Response** | Crash immediately | Recover gracefully |
      | **In production** | Keep (they indicate bugs) | Required (they handle expected failures) |
      | **Message audience** | Developers (debugging) | Users or calling code (error recovery) |
      
      ### Decision Guide
      
      ```
      Can the user cause this condition through normal use?
        → Error handling (validate input, show friendly message)
      
      Is this a bug in the code if it happens?
        → Assertion (crash with developer-friendly message)
      
      Can the system recover meaningfully?
        → Error handling (retry, fallback, degrade)
      
      Is recovery just "pretend it didn't happen"?
        → Assertion (don't hide bugs behind error handling)
      
      Is this an external system failure (network, disk, API)?
        → Error handling (these are expected in production)
      
      Is this a violation of an internal invariant?
        → Assertion (the system is in an invalid state)
      ```
      
      The pragmatic programmer uses both tools appropriately: assertions for "this should never happen" and error handling for "this might happen." The worst approach is using neither -- silently ignoring problems and hoping for the best.
      
    • dry-orthogonality.md 10.9 KB
      # DRY and Orthogonality
      
      Deep reference for the two most foundational principles of pragmatic programming. Load when deeper guidance is needed on eliminating duplication and designing decoupled systems.
      
      ## Table of Contents
      1. [DRY: Knowledge, Not Code](#dry-knowledge-not-code)
      2. [The Four Types of Duplication](#the-four-types-of-duplication)
      3. [Detecting DRY Violations](#detecting-dry-violations)
      4. [Orthogonality Defined](#orthogonality-defined)
      5. [Orthogonality in Design](#orthogonality-in-design)
      6. [Orthogonality in Coding](#orthogonality-in-coding)
      7. [Measuring Orthogonality](#measuring-orthogonality)
      8. [Benefits of Orthogonal Systems](#benefits-of-orthogonal-systems)
      
      ---
      
      ## DRY: Knowledge, Not Code
      
      The most common misunderstanding of DRY is treating it as "don't have similar-looking code." DRY is about **knowledge** -- every piece of knowledge must have a single, unambiguous, authoritative representation in the system.
      
      ### What Counts as Knowledge?
      
      - **Business rules**: "Users get 3 free trials" should exist in one place
      - **Data schemas**: The shape of a user record should be defined once
      - **Algorithms**: A discount calculation should have one implementation
      - **Configuration**: Database connection strings should come from one source
      - **API contracts**: The interface between systems should be defined once
      
      ### What Does NOT Count as Duplication?
      
      Two pieces of code may look identical but represent different knowledge:
      
      ```python
      # These are NOT DRY violations -- they serve different business rules
      
      def validate_billing_address(address):
          return len(address.zip_code) == 5
      
      def validate_shipping_address(address):
          return len(address.zip_code) == 5
      ```
      
      Today these look the same, but billing validation and shipping validation are governed by different business rules. When shipping starts supporting international addresses, they'll diverge. Merging them would create coupling between unrelated concepts.
      
      **The test:** If one changes, must the other change? If yes, it's duplication. If they could diverge independently, it's coincidence.
      
      ---
      
      ## The Four Types of Duplication
      
      ### 1. Imposed Duplication
      
      The environment or tooling forces duplication.
      
      **Examples:**
      - Language requires header files that repeat function signatures
      - Multiple platforms need the same validation (iOS, Android, web)
      - API documentation must match the implementation
      
      **Mitigations:**
      - Generate code from a single source (OpenAPI -> client + server + docs)
      - Use code generation for cross-platform shared logic
      - Keep documentation in code (docstrings, annotations) and generate external docs
      - Use database migrations as the single source of schema truth
      
      ### 2. Inadvertent Duplication
      
      Developers don't realize they're duplicating knowledge.
      
      **Examples:**
      - A `Line` class stores `start`, `end`, AND `length` -- length is derivable
      - The same business rule exists in the frontend form validation and the backend API
      - Configuration defaults are hard-coded in multiple services
      
      **Mitigations:**
      - Derive values instead of storing them: `@property def length(self): return self.end - self.start`
      - Share validation schemas between frontend and backend (e.g., Zod, JSON Schema)
      - Centralize configuration with a config service or shared env files
      
      ### 3. Impatient Duplication
      
      "I'll clean it up later." Developers know they're duplicating but choose speed.
      
      **Examples:**
      - Copy-pasting a utility function into a new service instead of extracting a shared library
      - Duplicating a SQL query with slight modifications instead of parameterizing
      - Hardcoding a value that already exists in a config file
      
      **Mitigations:**
      - Make the right thing easy: invest in shared libraries, package registries, and templates
      - Time-box the "right way" -- often it takes only 10 minutes more than copy-paste
      - Code review: flag copy-paste duplication as a blocker, not a nit
      
      ### 4. Inter-Developer Duplication
      
      Multiple developers or teams unknowingly build the same thing.
      
      **Examples:**
      - Two teams build their own date-formatting utility
      - Three microservices each implement user authentication logic
      - Frontend and backend teams both build a currency formatter
      
      **Mitigations:**
      - Establish shared libraries and make them discoverable (internal package registry)
      - Regular cross-team architecture reviews
      - Appoint a "librarian" or use a tech radar to track shared concerns
      - Use a monorepo or shared packages to make duplication visible
      
      ---
      
      ## Detecting DRY Violations
      
      ### Code-Level Signals
      
      | Signal | What It Suggests |
      |--------|-----------------|
      | Shotgun surgery (changing one thing requires touching 5+ files) | Knowledge is scattered |
      | "Find and replace" is your refactoring strategy | Same knowledge in multiple places |
      | Bug fix in one place doesn't fix it everywhere | Duplicated logic |
      | New developer asks "which one is the real one?" | Multiple sources of truth |
      | Enum values are defined in both code and database | Schema duplication |
      
      ### Architecture-Level Signals
      
      | Signal | What It Suggests |
      |--------|-----------------|
      | Multiple services validate the same business rule differently | Inter-service duplication |
      | Config values are hard-coded in multiple deployment scripts | Imposed duplication |
      | API documentation regularly drifts from implementation | Docs/code duplication |
      | "We need to update this in 3 places" | Knowledge not centralized |
      
      ---
      
      ## Orthogonality Defined
      
      In geometry, orthogonal lines meet at right angles -- moving along one axis doesn't affect your position on the other. In software, two components are orthogonal if changes in one have no effect on the other.
      
      **The helicopter analogy:** A traditional helicopter has four controls, all coupled -- changing collective pitch affects yaw, which requires compensating with the tail rotor, which changes roll. Flying a helicopter is hard because nothing is orthogonal. Good software should be the opposite: each control affects exactly one thing.
      
      ---
      
      ## Orthogonality in Design
      
      ### Layered Architecture
      
      The most common way to achieve orthogonality is through layers:
      
      ```
      ┌─────────────────────┐
      │   Presentation      │  (UI, API endpoints, CLI)
      ├─────────────────────┤
      │   Application       │  (Use cases, workflows)
      ├─────────────────────┤
      │   Domain            │  (Business rules, entities)
      ├─────────────────────┤
      │   Infrastructure    │  (DB, external APIs, filesystem)
      └─────────────────────┘
      ```
      
      **Test:** If you dramatically change the UI framework, how many layers need to change? Ideally, only the presentation layer. If domain logic lives in UI components, you have a coupling problem.
      
      ### Component Independence Checklist
      
      | Question | Good Answer |
      |----------|-------------|
      | Can I test this component in isolation? | Yes, with mocked dependencies |
      | If I remove this component, what breaks? | Only things that directly depend on it |
      | Does this component know about the deployment environment? | No, it receives config via injection |
      | Can two developers work on separate components without conflicts? | Yes, interfaces are stable |
      | Does this component import from more than one architectural layer? | No, it depends only on the layer below |
      
      ---
      
      ## Orthogonality in Coding
      
      ### Strategies for Keeping Code Orthogonal
      
      **1. Avoid global state.** Every piece of global state is a coupling point. Every module that reads or writes it is coupled to every other module that does the same.
      
      ```python
      # Bad: global coupling
      CURRENT_USER = None  # every module reads/writes this
      
      # Good: explicit parameter
      def process_order(order, user):
          ...
      ```
      
      **2. Avoid similar functions.** If two functions share significant structure, extract the commonality into a third function and have both call it.
      
      **3. Use the Shy Code rule.** Modules should not reveal anything unnecessary about themselves and should not rely on the implementation details of other modules.
      
      ```python
      # Bad: reaching through objects (Law of Demeter violation)
      user.address.city.zip_code
      
      # Good: ask, don't tell
      user.shipping_zip_code()
      ```
      
      **4. Prefer composition over inheritance.** Inheritance creates tight coupling between parent and child. Composition (using interfaces and delegation) keeps components independent.
      
      ```python
      # Inheritance: tightly coupled
      class AdminUser(User):
          def can_delete(self): return True
      
      # Composition: loosely coupled
      class User:
          def __init__(self, permissions: Permissions):
              self.permissions = permissions
          def can_delete(self):
              return self.permissions.allows("delete")
      ```
      
      ---
      
      ## Measuring Orthogonality
      
      ### The Change Impact Test
      
      For any proposed change, count the number of modules affected:
      
      | Change Scope | Modules Affected | Assessment |
      |-------------|------------------|------------|
      | Fix a bug in tax calculation | 1 (tax module) | Excellent orthogonality |
      | Change database from Postgres to MySQL | 1-2 (data layer) | Good orthogonality |
      | Add a new field to user profile | 3-5 (model, API, UI, migration, tests) | Acceptable (vertical feature) |
      | Change the logging library | 15+ modules | Poor orthogonality -- logging is coupled everywhere |
      
      ### The "Stranger" Test
      
      Could a developer unfamiliar with the codebase change one component without breaking others? If yes, your system is orthogonal. If they need to understand the full system to make any change, it is not.
      
      ---
      
      ## Benefits of Orthogonal Systems
      
      ### Productivity Gains
      
      - **Changes are localized:** fixing a bug in one component doesn't cause regressions elsewhere
      - **Reuse is easier:** self-contained components can be extracted and used in other projects
      - **Parallel development:** teams can work independently on different components
      - **Testing is simpler:** unit tests cover isolated components without elaborate setup
      
      ### Risk Reduction
      
      - **Diseased sections are isolated:** a poorly-written module doesn't infect neighboring code
      - **Less fragile:** the system doesn't shatter when one thing changes
      - **Better tested:** orthogonal components are inherently easier to test, so they get tested more
      - **Not tied to a vendor:** when the database is behind an interface, switching vendors is a bounded task
      
      ### The Compound Effect
      
      Orthogonality and DRY are multiplicative. A system that is both DRY and orthogonal sees dramatic improvements:
      
      | Property | Without DRY/Orthogonality | With Both |
      |----------|--------------------------|-----------|
      | Bug fix time | Hours (find all duplicates, test all couplings) | Minutes (one change, one test) |
      | Feature addition | High risk of regressions | Localized, predictable impact |
      | Onboarding time | Weeks to understand dependencies | Days to become productive |
      | Deployment confidence | "Deploy and pray" | "Deploy and verify" |
      
      The pragmatic programmer pursues both relentlessly -- not for theoretical purity, but because the compound effect saves enormous amounts of time and pain over the life of a project.
      
    • estimation-portfolio.md 13.6 KB
      # Estimation and Knowledge Portfolio
      
      Deep reference for reliable estimation techniques and continuous learning strategies. Load when guidance is needed on project estimation, PERT analysis, or managing a developer's knowledge portfolio.
      
      ## Table of Contents
      1. [How to Estimate](#how-to-estimate)
      2. [Understanding Scope](#understanding-scope)
      3. [Building a Model](#building-a-model)
      4. [PERT Estimation](#pert-estimation)
      5. [Decomposition Strategies](#decomposition-strategies)
      6. [Estimation Calibration](#estimation-calibration)
      7. [Knowledge Portfolio Management](#knowledge-portfolio-management)
      8. [Critical Thinking](#critical-thinking)
      
      ---
      
      ## How to Estimate
      
      Estimation is the pragmatic programmer's most valuable communication skill. A good estimate sets appropriate expectations. A bad estimate destroys trust.
      
      ### The Context Question
      
      Before estimating anything, ask: **"What is this estimate for?"** The answer determines the precision required:
      
      | Context | Precision Needed | Appropriate Format |
      |---------|-----------------|-------------------|
      | Budget planning (next year) | Order of magnitude | "6 months, give or take 3" |
      | Quarterly planning | Weeks | "4-8 weeks" |
      | Sprint planning | Days | "3-5 days" |
      | Daily standup | Hours | "About 4 hours remaining" |
      | Production incident | Minutes | "ETA: 30-60 minutes" |
      
      **Key insight:** Stating "it'll take 2 weeks" for a quarterly planning exercise implies false precision. Stating "1-3 weeks" is more honest and more useful.
      
      ### The Units Tell a Story
      
      Choose units that convey the right level of uncertainty:
      
      | Estimate | What the Listener Hears |
      |----------|------------------------|
      | "128 hours" | "They calculated this precisely; it should be exactly 128 hours" |
      | "About 3 weeks" | "Roughly 3 weeks, could be 2-4" |
      | "1-2 months" | "It's a big effort with meaningful uncertainty" |
      | "6 months, give or take" | "We're really not sure; this is a rough order of magnitude" |
      
      Use the roughest unit that matches your actual confidence level.
      
      ---
      
      ## Understanding Scope
      
      The first step in any estimate is understanding what you're estimating. This sounds obvious but is the most common source of estimation failure.
      
      ### Scope Clarification Questions
      
      | Question | Why It Matters |
      |----------|---------------|
      | What's included? | "Build a login page" -- does that include password reset? OAuth? 2FA? |
      | What's excluded? | Explicit exclusions prevent scope creep after estimation |
      | What can I assume? | "Can I use an existing component library or build from scratch?" |
      | What's the quality bar? | MVP vs. production-hardened vs. enterprise-grade |
      | Who's the audience? | Internal tool vs. public-facing vs. API for partners |
      | What are the dependencies? | "I need the design team to deliver mocks first" |
      
      ### The Scope Multiplier Table
      
      The same feature at different quality levels:
      
      | Quality Level | Multiplier | Includes |
      |--------------|-----------|----------|
      | **Spike/Prototype** | 1x | Happy path only, no tests, no error handling |
      | **MVP** | 2-3x | Happy path + basic error handling + basic tests |
      | **Production-ready** | 4-6x | Full error handling, monitoring, tests, documentation |
      | **Enterprise-grade** | 8-12x | Security audit, compliance, HA, disaster recovery, SLA |
      
      A "login page" that takes 2 days as a prototype takes 8-12 days as production-ready. Make sure you and the stakeholder agree on the quality level before estimating.
      
      ---
      
      ## Building a Model
      
      Good estimates come from models, not gut feelings. A model breaks the work into components whose effort you can reason about.
      
      ### Types of Models
      
      **Analogy-based:** "This is similar to the payment integration we built last quarter, which took 3 weeks. This is simpler, so 2 weeks."
      
      **Decomposition-based:** Break into tasks, estimate each, sum with a buffer. (See Decomposition Strategies below.)
      
      **Historical data-based:** "Our team averages 8 story points per sprint. This epic is ~40 points, so 5 sprints."
      
      **Three-point (PERT):** Estimate optimistic, most likely, and pessimistic. Calculate expected value. (See PERT Estimation below.)
      
      ### Model Validation
      
      After building a model, sanity-check it:
      
      | Check | How |
      |-------|-----|
      | **Comparison** | "Is this estimate in the same ballpark as similar past work?" |
      | **Gut check** | "Does this feel right based on my experience?" |
      | **Peer review** | "Does another senior engineer agree with this estimate?" |
      | **Boundary test** | "What's the absolute minimum time? What's the longest it could possibly take?" |
      
      ---
      
      ## PERT Estimation
      
      Program Evaluation and Review Technique (PERT) uses three estimates to produce a weighted average:
      
      ### The Formula
      
      ```
      Expected = (Optimistic + 4 × Most Likely + Pessimistic) / 6
      Standard Deviation = (Pessimistic - Optimistic) / 6
      ```
      
      ### Example
      
      Estimating a database migration:
      
      | Scenario | Value | Reasoning |
      |----------|-------|-----------|
      | **Optimistic (O)** | 3 days | Schema is simple, no data transformation needed |
      | **Most Likely (M)** | 7 days | Some data transformation, testing in staging |
      | **Pessimistic (P)** | 15 days | Complex data issues, rollback needed, multiple attempts |
      
      ```
      Expected = (3 + 4×7 + 15) / 6 = (3 + 28 + 15) / 6 = 46 / 6 ≈ 7.7 days
      Std Dev = (15 - 3) / 6 = 2 days
      ```
      
      Communicate: **"About 8 days, with a range of 6-10 days (one standard deviation). Worst case: 15 days."**
      
      ### PERT for Multiple Tasks
      
      When estimating a project with multiple tasks, PERT each task separately, then sum:
      
      | Task | O | M | P | Expected | Std Dev |
      |------|---|---|---|----------|---------|
      | Schema design | 1 | 2 | 5 | 2.3 | 0.7 |
      | Migration script | 2 | 4 | 8 | 4.3 | 1.0 |
      | Testing | 1 | 3 | 7 | 3.3 | 1.0 |
      | Rollback plan | 0.5 | 1 | 3 | 1.3 | 0.4 |
      | **Total** | | | | **11.2** | **1.6** |
      
      Total standard deviation for independent tasks: `sqrt(0.7² + 1.0² + 1.0² + 0.4²) = sqrt(0.49 + 1.0 + 1.0 + 0.16) = sqrt(2.65) ≈ 1.6 days`
      
      Communicate: **"About 11 days, likely 10-13. Worst case: 23 days."**
      
      ---
      
      ## Decomposition Strategies
      
      ### Work Breakdown Structure (WBS)
      
      Break work into progressively smaller pieces until each piece is estimable:
      
      ```
      Feature: User Authentication
      ├── Design
      │   ├── API design (0.5 days)
      │   └── Database schema (0.5 days)
      ├── Implementation
      │   ├── Registration endpoint (1 day)
      │   ├── Login endpoint (1 day)
      │   ├── Password hashing (0.5 days)
      │   ├── JWT token generation (0.5 days)
      │   ├── Token refresh (1 day)
      │   └── Password reset (1.5 days)
      ├── Testing
      │   ├── Unit tests (1 day)
      │   ├── Integration tests (1 day)
      │   └── Security testing (0.5 days)
      ├── Infrastructure
      │   ├── Database migration (0.5 days)
      │   └── Environment config (0.5 days)
      └── Documentation
          └── API docs (0.5 days)
      
      Subtotal: 10 days
      Buffer (20%): 2 days
      Total estimate: 12 days
      ```
      
      ### The Rule of Small Tasks
      
      - Break tasks until each is **1 day or less**
      - Tasks larger than 2 days are too vague to estimate reliably
      - If you can't break a task down, you don't understand it well enough to estimate it
      
      ### Buffer Strategy
      
      | Buffer Type | Amount | Rationale |
      |-------------|--------|-----------|
      | **Task buffer** | +20% per task | Unknown unknowns within the task |
      | **Integration buffer** | +10-20% of total | Time to connect components |
      | **Risk buffer** | +10-30% based on novelty | New tech, new domain, new team |
      | **Communication buffer** | +10% | Meetings, reviews, decisions |
      
      **Total project buffer** typically ranges from 30-50% on top of raw task estimates. This isn't padding -- it's realism.
      
      ---
      
      ## Estimation Calibration
      
      The secret to getting better at estimation: **track your accuracy and adjust.**
      
      ### The Estimation Log
      
      Keep a simple log:
      
      | Date | Task | Estimate | Actual | Ratio | Notes |
      |------|------|----------|--------|-------|-------|
      | Jan 5 | Payment integration | 5 days | 8 days | 1.6x | Underestimated API complexity |
      | Jan 15 | User dashboard | 3 days | 2.5 days | 0.8x | Reused more components than expected |
      | Jan 22 | Data migration | 2 days | 6 days | 3.0x | Unexpected data quality issues |
      
      ### Calibration Metrics
      
      After 20+ entries, calculate:
      
      | Metric | What It Tells You |
      |--------|------------------|
      | **Average ratio** | Your systematic bias (>1 = underestimate, <1 = overestimate) |
      | **Standard deviation** | Your consistency (lower = more reliable) |
      | **Pattern by task type** | Where you're consistently wrong (always underestimate infra work?) |
      | **Trend over time** | Are you getting better? |
      
      If your average ratio is 1.5x, multiply all future estimates by 1.5 until you recalibrate.
      
      ### Common Estimation Biases
      
      | Bias | Description | Fix |
      |------|-------------|-----|
      | **Optimism** | Assuming best case | Use PERT to force pessimistic thinking |
      | **Anchoring** | First number heard dominates | Estimate independently before discussing |
      | **Planning fallacy** | Ignoring past overruns | Consult your estimation log |
      | **Scope neglect** | Forgetting testing, deployment, documentation | Use a checklist of "hidden" work |
      | **Confidence bias** | Being too sure of your estimate | Always provide ranges |
      
      ---
      
      ## Knowledge Portfolio Management
      
      Hunt and Thomas argue that your knowledge and experience are your most important professional assets -- and they're *expiring* assets. Technology changes. What you know today becomes obsolete.
      
      ### The Portfolio Analogy
      
      Manage your knowledge like a financial portfolio:
      
      | Financial Principle | Knowledge Equivalent |
      |--------------------|---------------------|
      | **Invest regularly** | Learn something every week, even when busy |
      | **Diversify** | Don't only learn your current stack; explore adjacent areas |
      | **Manage risk** | Mix safe investments (deepen expertise) with speculative ones (learn something wild) |
      | **Buy low, sell high** | Learn emerging technologies early, before they're mainstream |
      | **Review and rebalance** | Periodically assess what you know and identify gaps |
      
      ### Practical Investment Strategies
      
      | Strategy | Time Commitment | Example |
      |----------|----------------|---------|
      | **Learn a new language every year** | 2 hours/week for 3 months | Learn Rust if you're a Python developer |
      | **Read a technical book every quarter** | 30 min/day | One book on architecture, one on a new domain |
      | **Take a course or workshop** | 1 day/quarter | Online course, conference workshop |
      | **Participate in open source** | 2 hours/week | Contribute to a project outside your comfort zone |
      | **Attend user groups or meetups** | 2 hours/month | Learn what other people are building and how |
      | **Experiment with different environments** | 1 day/quarter | Try Windows if you use macOS; try Linux if you use Windows |
      | **Stay current** | 15 min/day | Read technical newsletters, blogs, papers |
      
      ### The Portfolio Balance
      
      | Category | Investment | Examples |
      |----------|-----------|---------|
      | **Core expertise (50%)** | Deepen what you do daily | Advanced patterns in your primary language, deep database knowledge |
      | **Adjacent skills (30%)** | Expand your range | DevOps if you're a developer, UX if you're backend, security for everyone |
      | **Speculative bets (20%)** | Explore the frontier | New paradigms (functional, AI/ML, blockchain), new languages, new domains |
      
      ---
      
      ## Critical Thinking
      
      The pragmatic programmer doesn't just consume knowledge -- they evaluate it critically.
      
      ### Questions to Ask About What You Read and Hear
      
      | Question | Why It Matters |
      |----------|---------------|
      | **Who's saying it?** | A vendor promoting their own product has different motivations than an independent researcher |
      | **What's their context?** | "Microservices are the answer" -- at Google's scale, maybe. At your 5-person startup, probably not |
      | **When was it written?** | A 2015 article about JavaScript best practices may be obsolete |
      | **Why are they telling me this?** | Conference talks often promote the speaker's product or approach |
      | **What are the trade-offs?** | Every technique has downsides. If the source doesn't mention any, be skeptical |
      
      ### The "Works for Us" Fallacy
      
      Just because Netflix uses microservices doesn't mean you should. Critical thinking means understanding:
      
      - **Scale:** Their problems are not your problems
      - **Resources:** Their engineering team is not your engineering team
      - **Context:** Their domain constraints are not your domain constraints
      - **Survivorship bias:** You hear about the successes, not the failures
      
      ### Building a Bullshit Filter
      
      | Claim | Red Flag | Better Question |
      |-------|----------|----------------|
      | "X is dead" | Absolutism | "In what contexts is X still appropriate?" |
      | "Everyone is using Y" | Bandwagon | "What problem does Y solve that we actually have?" |
      | "Z scales to millions" | Irrelevant scale | "Does Z work well at our scale of thousands?" |
      | "You should always do W" | No nuance | "What are the trade-offs of W vs. alternatives?" |
      | "This is best practice" | Appeal to authority | "Best for whom, in what context, measured how?" |
      
      ### The Pragmatic Evaluation Framework
      
      When evaluating a new technology, methodology, or approach:
      
      1. **Understand the problem it solves** -- What pain does it address?
      2. **Understand the trade-offs** -- What does it cost (complexity, performance, learning curve)?
      3. **Consider your context** -- Does it solve a problem you actually have?
      4. **Try it small** -- Prototype or spike before committing
      5. **Measure the results** -- Did it actually help, or just feel modern?
      6. **Re-evaluate periodically** -- Is it still the right choice?
      
      The pragmatic programmer's goal is not to use the newest technology. It's to use the **most appropriate** technology for their specific context. Sometimes that's cutting-edge; sometimes it's boring and well-proven. The critical thinker knows the difference.
      
    • reversibility.md 13.9 KB
      # Reversibility and Flexible Architecture
      
      Deep reference for building systems where decisions can be changed without rewrites. Load when guidance is needed on decoupling, vendor abstraction, and keeping architectural options open.
      
      ## Table of Contents
      1. [There Are No Final Decisions](#there-are-no-final-decisions)
      2. [The Cost of Irreversibility](#the-cost-of-irreversibility)
      3. [Decoupling Strategies](#decoupling-strategies)
      4. [Vendor Lock-In Thinking](#vendor-lock-in-thinking)
      5. [The Forking Road](#the-forking-road)
      6. [Metadata-Driven Systems](#metadata-driven-systems)
      7. [Reversibility Patterns by Layer](#reversibility-patterns-by-layer)
      8. [When NOT to Optimize for Reversibility](#when-not-to-optimize-for-reversibility)
      
      ---
      
      ## There Are No Final Decisions
      
      The pragmatic programmer treats every architectural decision as temporary. Not because you expect to change everything, but because you acknowledge that you might be wrong -- and the cost of being wrong should be proportional to the size of the mistake, not the age of the codebase.
      
      ### Decisions That Often Change
      
      | Decision | Why It Changes | Frequency |
      |----------|---------------|-----------|
      | Database engine | Scale requirements change, licensing changes, team expertise shifts | Every 3-5 years |
      | Cloud provider | Pricing changes, new features elsewhere, compliance requirements | Every 2-5 years |
      | Frontend framework | Ecosystem evolves, hiring requirements shift, performance needs change | Every 2-4 years |
      | Authentication provider | Security requirements evolve, pricing changes, features needed | Every 2-3 years |
      | Payment processor | Better rates, geographic expansion, feature requirements | Every 1-3 years |
      | Message queue | Scale requirements, latency needs, operational complexity | Every 3-5 years |
      | Deployment model | Monolith to microservices, containers, serverless | Every 3-5 years |
      
      ### The Time Horizon Test
      
      Before embedding a decision into your architecture, ask: "How long will this decision be valid?"
      
      | Answer | Strategy |
      |--------|----------|
      | **Forever** (laws of physics, math) | Embed directly -- these don't change |
      | **Years** (language choice, core domain model) | Invest in the decision but maintain boundaries |
      | **Months** (vendor choice, specific library) | Abstract behind an interface |
      | **Weeks** (feature flags, A/B tests) | Make configurable at runtime |
      | **Unknown** | Default to abstraction |
      
      ---
      
      ## The Cost of Irreversibility
      
      When a decision is embedded throughout the codebase, the cost of changing it grows with every line of code that depends on it:
      
      ### The Dependency Fan-Out Problem
      
      ```
      Decision: Use MongoDB
      
      Direct dependencies:
        → 5 repository classes (manageable)
      
      Indirect dependencies:
        → 20 services that use MongoDB query syntax in their logic
        → 15 tests that depend on MongoDB-specific behavior
        → 3 scripts that use MongoDB CLI tools
        → 2 monitoring dashboards with MongoDB-specific metrics
      
      Total cost to change: Weeks of work, high risk of regressions
      ```
      
      Compare to:
      
      ```
      Decision: Use MongoDB, behind Repository interface
      
      Direct dependencies:
        → 5 repository implementations (change these)
      
      Indirect dependencies:
        → None (everything uses the Repository interface)
      
      Total cost to change: Days of work, low risk
      ```
      
      ### Measuring Irreversibility
      
      | Metric | How to Measure | What It Means |
      |--------|---------------|---------------|
      | **Fan-out** | Count files that import the dependency directly | Higher = harder to change |
      | **Coupling depth** | How many layers does the dependency penetrate? | Deeper = more expensive |
      | **Test dependence** | How many tests break if you swap the dependency? | More broken tests = more risk |
      | **Config surface** | How many config values reference the dependency? | More config = more places to update |
      
      ---
      
      ## Decoupling Strategies
      
      ### The Adapter Pattern
      
      The primary tool for reversibility. Place your own interface between your code and any external dependency:
      
      ```python
      # Your interface (never changes)
      class MessageBroker(Protocol):
          def publish(self, topic: str, message: dict) -> None: ...
          def subscribe(self, topic: str, handler: Callable) -> None: ...
      
      # Current implementation (swappable)
      class KafkaMessageBroker:
          def __init__(self, bootstrap_servers: list[str]):
              self.producer = KafkaProducer(bootstrap_servers=bootstrap_servers)
      
          def publish(self, topic: str, message: dict) -> None:
              self.producer.send(topic, json.dumps(message).encode())
      
          def subscribe(self, topic: str, handler: Callable) -> None:
              consumer = KafkaConsumer(topic, bootstrap_servers=self.servers)
              for msg in consumer:
                  handler(json.loads(msg.value))
      
      # Future implementation (just implement the same interface)
      class RabbitMQMessageBroker:
          def publish(self, topic: str, message: dict) -> None:
              # RabbitMQ-specific implementation
              ...
      ```
      
      ### The Repository Pattern
      
      Separate data access logic from business logic:
      
      ```python
      # Business logic knows nothing about the database
      class OrderService:
          def __init__(self, order_repo: OrderRepository):
              self.order_repo = order_repo
      
          def place_order(self, customer_id: str, items: list[Item]) -> Order:
              order = Order(customer_id=customer_id, items=items)
              order.calculate_total()
              self.order_repo.save(order)
              return order
      
      # Database-specific implementation
      class PostgresOrderRepository(OrderRepository):
          def save(self, order: Order) -> None:
              self.db.execute(
                  "INSERT INTO orders (id, customer_id, total) VALUES (%s, %s, %s)",
                  (order.id, order.customer_id, order.total)
              )
      
      # Alternative implementation
      class DynamoDBOrderRepository(OrderRepository):
          def save(self, order: Order) -> None:
              self.table.put_item(Item={
                  'id': order.id,
                  'customer_id': order.customer_id,
                  'total': str(order.total)
              })
      ```
      
      ### Event-Driven Decoupling
      
      Services communicate through events rather than direct calls:
      
      ```
      Direct coupling (hard to reverse):
        OrderService → calls → InventoryService.reserve()
        OrderService → calls → NotificationService.sendEmail()
        OrderService → calls → AnalyticsService.track()
      
      Event-driven decoupling (easy to reverse):
        OrderService → publishes → "order.placed" event
        InventoryService → subscribes → "order.placed" (reserves inventory)
        NotificationService → subscribes → "order.placed" (sends email)
        AnalyticsService → subscribes → "order.placed" (tracks event)
      ```
      
      With events, OrderService doesn't know or care who's listening. You can add, remove, or replace subscribers without touching the publisher.
      
      ---
      
      ## Vendor Lock-In Thinking
      
      ### The "Vendor Lock-In" Trap
      
      Many developers fear vendor lock-in, but the response is often worse than the problem:
      
      | Overreaction | Problem |
      |-------------|---------|
      | Build everything yourself to avoid dependencies | Reinventing the wheel, maintenance burden |
      | Abstract everything from day one | Over-engineering, YAGNI violation |
      | Never commit to any vendor | Analysis paralysis, delayed delivery |
      | Use the lowest common denominator across all vendors | Miss out on the best features of each |
      
      ### The Pragmatic Approach
      
      Not all vendor coupling is equal. Evaluate based on switching cost:
      
      | Coupling Level | Example | Switching Cost | Strategy |
      |---------------|---------|---------------|----------|
      | **Low** | Logging library | Hours | Use directly, don't abstract |
      | **Medium** | Payment processor | Days | Thin adapter, abstract the interface |
      | **High** | Database engine | Weeks | Repository pattern, avoid vendor-specific SQL |
      | **Very high** | Cloud provider (using 10+ services) | Months | Accept the coupling, negotiate contracts |
      | **Extreme** | Custom hardware, proprietary protocols | Quarters | Strategic decision, not a technical one |
      
      ### The 80/20 Rule of Abstraction
      
      Abstract the 20% of vendor features that, if changed, would require touching 80% of your code. Don't abstract everything -- that's expensive and often unnecessary.
      
      ```
      Abstract (high fan-out):
        ✓ Database queries (used everywhere)
        ✓ Authentication (used in every request)
        ✓ Message publishing (used by many services)
      
      Don't abstract (low fan-out):
        ✗ Monitoring dashboard configuration (one place)
        ✗ CI/CD pipeline scripts (one place)
        ✗ Infrastructure-as-code templates (one place)
      ```
      
      ---
      
      ## The Forking Road
      
      Every decision point is a fork in the road. The pragmatic programmer's goal is to keep as many roads open as possible, for as long as possible, without paying too much for the optionality.
      
      ### The Decision Framework
      
      ```
                               ┌─ Low cost to reverse?
                               │   → Make the decision and move on
      Is this decision         │
      reversible? ────────────┤
                               │   → High cost to reverse?
                               │     ├─ Is the evidence clear?
                               │     │   → Commit but build an abstraction layer
                               │     │
                               │     └─ Is the evidence unclear?
                               │         → Delay the decision (tracer bullet first)
                               └
      ```
      
      ### Strategies for Keeping Roads Open
      
      | Strategy | When to Use | Example |
      |----------|------------|---------|
      | **Delay the decision** | Evidence is insufficient | "We'll choose between Kafka and RabbitMQ after the tracer bullet" |
      | **Make it configurable** | Decision might change at runtime | Feature flags, A/B tests, runtime config |
      | **Build an abstraction** | Decision will eventually change | Repository pattern for database, adapter for vendor API |
      | **Prototype both options** | Two options seem equally valid | Spend 2 days prototyping each, then decide with data |
      | **Accept the coupling** | Cost of abstraction exceeds cost of future change | Using 15 AWS services? Accept AWS coupling |
      
      ---
      
      ## Metadata-Driven Systems
      
      One of the most powerful reversibility tools: drive behavior from metadata (configuration) rather than code.
      
      ### What Can Be Metadata
      
      | Aspect | Hard-Coded | Metadata-Driven |
      |--------|-----------|-----------------|
      | **Feature availability** | `if (isAdmin) { showFeature() }` | Feature flag in config service |
      | **Validation rules** | `if (age < 18) throw Error` | Rule engine reads rules from config |
      | **Workflow steps** | Hard-coded state machine | State transitions defined in YAML/JSON |
      | **UI layout** | Components in JSX | Layout defined in a CMS or config |
      | **Business rules** | `if (total > 100) applyDiscount(10)` | Rules engine with configurable thresholds |
      | **API endpoints** | Hard-coded URLs | Service discovery or config file |
      
      ### Benefits of Metadata-Driven Design
      
      - **No redeployment** for business rule changes
      - **Non-engineers can make changes** through admin interfaces
      - **A/B testing** becomes configuration, not code
      - **Rollback** is changing a config value, not deploying old code
      - **Auditing** is straightforward -- config changes are logged
      
      ### Risks of Over-Doing It
      
      - **Complexity:** A metadata-driven rules engine is itself complex software that needs testing and maintenance
      - **Debugging:** "Why did the system do X?" requires tracing through config values, not just reading code
      - **Validation:** Bad config can cause outages just like bad code
      - **Testing:** Config combinations create a large test surface
      
      **Rule of thumb:** Use metadata for values that change more frequently than deployments. Use code for values that change at the same pace as deployments.
      
      ---
      
      ## Reversibility Patterns by Layer
      
      ### Presentation Layer
      
      | Pattern | Reversibility Benefit |
      |---------|----------------------|
      | Component library | Swap styling framework without rewriting components |
      | BFF (Backend for Frontend) | Change frontend without changing APIs |
      | Design tokens | Change visual design via token updates, not code changes |
      | Server-driven UI | Change UI layout without app store deployments |
      
      ### Application Layer
      
      | Pattern | Reversibility Benefit |
      |---------|----------------------|
      | Use case classes | Swap one workflow implementation for another |
      | Event sourcing | Rebuild state from events if model changes |
      | CQRS | Optimize reads and writes independently |
      | Saga pattern | Change transaction coordination strategy |
      
      ### Infrastructure Layer
      
      | Pattern | Reversibility Benefit |
      |---------|----------------------|
      | Containers | Run on any orchestrator (ECS, K8s, bare metal) |
      | Infrastructure as Code | Reproduce or modify environment from declarations |
      | Service mesh | Change networking/security policies without code changes |
      | Blue-green deployment | Roll back in seconds |
      
      ---
      
      ## When NOT to Optimize for Reversibility
      
      Reversibility has a cost. The pragmatic programmer knows when it's not worth it:
      
      ### Skip Abstraction When
      
      - **The project is a prototype** that will be thrown away
      - **The dependency is trivially replaceable** (swapping one JSON library for another takes 30 minutes)
      - **The abstraction costs more than the future switch** (building a database abstraction layer for a weekend project)
      - **You have strong evidence the decision won't change** (using HTTP for a web server)
      - **YAGNI applies** -- you're abstracting against a change you have no reason to expect
      
      ### The Pragmatic Test
      
      Before adding an abstraction layer for reversibility, ask:
      
      1. **How likely is this to change?** (Be honest, not paranoid)
      2. **What would it cost to change without the abstraction?** (Often less than you think)
      3. **What does the abstraction cost now?** (Design, implementation, testing, maintenance)
      4. **Does the abstraction actually provide reversibility?** (Sometimes abstractions are leaky and don't help)
      
      If (1) is low and (2) is moderate while (3) is high -- skip the abstraction. Build it when you actually need it.
      
      The goal is not perfect reversibility everywhere. The goal is proportional reversibility: invest in flexibility where change is likely and expensive, and accept coupling where change is unlikely or cheap.
      
    • tracer-bullets.md 11.8 KB
      # Tracer Bullets and Prototypes
      
      Deep reference for two distinct approaches to uncertainty: tracer bullets (keep the code) and prototypes (throw it away). Load when guidance is needed on which approach to use and how to execute each.
      
      ## Table of Contents
      1. [The Tracer Bullet Metaphor](#the-tracer-bullet-metaphor)
      2. [Tracer Bullet Development](#tracer-bullet-development)
      3. [Prototyping](#prototyping)
      4. [Tracer Bullets vs. Prototypes](#tracer-bullets-vs-prototypes)
      5. [Shooting in the Dark](#shooting-in-the-dark)
      6. [Iterating on Tracer Code](#iterating-on-tracer-code)
      7. [Walking Skeletons](#walking-skeletons)
      8. [Common Pitfalls](#common-pitfalls)
      
      ---
      
      ## The Tracer Bullet Metaphor
      
      In military usage, tracer bullets are loaded at regular intervals alongside regular ammunition. When fired in the dark, they leave a visible trail showing the path of fire. If the tracer misses, you adjust your aim and fire again. The feedback loop is immediate.
      
      In software, tracer bullet development serves the same purpose: you build something thin but real that travels through all the layers of the system, giving you immediate feedback on whether you're hitting the target.
      
      ---
      
      ## Tracer Bullet Development
      
      ### What It Is
      
      A tracer bullet is a thin, end-to-end implementation that connects all the major components of the system. It is **production code** -- not throwaway. It may be minimal, but it is real.
      
      ### Characteristics of Tracer Bullet Code
      
      | Property | Description |
      |----------|-------------|
      | **End-to-end** | Touches every layer: UI, API, business logic, data store |
      | **Functional** | Actually works, even if only for one scenario |
      | **Production quality** | Written with proper error handling, tests, and structure |
      | **Incomplete** | Handles one path through the system, not all edge cases |
      | **Extensible** | Built as a framework that other features can fill in |
      
      ### Example: Building a New Web Application
      
      Instead of building the full database schema, then all the API endpoints, then the full UI, a tracer bullet approach:
      
      1. **Pick one feature** (e.g., "user creates an account")
      2. **Build the UI** -- a single form with email and password
      3. **Build the API** -- one `POST /users` endpoint
      4. **Build the data layer** -- one `users` table with two columns
      5. **Connect them** -- form submits to API, API writes to DB, response confirms success
      6. **Deploy** -- to the real production environment (or staging)
      
      You now have a working system. It only does one thing, but all the layers are connected, the deployment pipeline works, and you can see the real behavior. Every subsequent feature fills in more of the skeleton.
      
      ### When to Use Tracer Bullets
      
      - Requirements are vague or rapidly changing
      - You're using a new technology stack you haven't worked with before
      - The architecture involves multiple integrated components (services, queues, databases)
      - Stakeholders need to see something real early
      - You want to validate that all the pieces connect before investing in each individually
      
      ---
      
      ## Prototyping
      
      ### What It Is
      
      A prototype is a focused investigation of one specific aspect of the system. Unlike tracer bullets, prototypes are **disposable** -- they are built to learn, not to keep.
      
      ### What to Prototype
      
      | Aspect | Question It Answers |
      |--------|-------------------|
      | **Algorithm** | Is this approach fast enough? Does it produce correct results? |
      | **UI/UX** | Does this interaction model make sense to users? |
      | **Architecture** | Can these components communicate at the required scale? |
      | **Third-party tool** | Does this library/service meet our requirements? |
      | **Performance** | Can this database handle our query patterns at load? |
      
      ### Characteristics of Prototype Code
      
      | Property | Description |
      |----------|-------------|
      | **Focused** | Explores one question, ignores everything else |
      | **Incomplete** | No error handling, no edge cases, no tests |
      | **Disposable** | Will be thrown away -- this must be explicit and agreed upon |
      | **Fast** | Built to answer a question quickly, not to last |
      | **Unrestricted** | Can use any language, tool, or shortcut |
      
      ### What to Ignore When Prototyping
      
      When building a prototype, you can and should ignore:
      
      - **Correctness:** Dummy data and hard-coded values are fine
      - **Completeness:** Handle the happy path only
      - **Robustness:** No error handling or recovery
      - **Style:** No need for clean code, proper naming, or documentation
      - **Performance:** Unless performance IS the question being investigated
      
      ### Example: Prototyping a Recommendation Engine
      
      Before building a real recommendation system, prototype:
      
      ```python
      # PROTOTYPE - DO NOT SHIP
      # Question: Does collaborative filtering produce useful recommendations
      # from our dataset?
      
      import pandas as pd
      from scipy.sparse import csr_matrix
      from sklearn.neighbors import NearestNeighbors
      
      # Load raw data (no proper ETL pipeline)
      df = pd.read_csv("raw_purchases.csv")
      
      # Quick and dirty pivot
      matrix = df.pivot(index='user_id', columns='product_id', values='purchased').fillna(0)
      
      # Fit nearest neighbors
      model = NearestNeighbors(metric='cosine')
      model.fit(csr_matrix(matrix.values))
      
      # Test with one user
      distances, indices = model.kneighbors(matrix.iloc[0:1], n_neighbors=5)
      print("Similar users:", indices)
      print("Their top products:", matrix.iloc[indices[0]].sum().nlargest(10))
      ```
      
      This prototype answers: "Does collaborative filtering work with our data?" The code is not production-quality and should never ship. But the *learning* it produces guides the real implementation.
      
      ---
      
      ## Tracer Bullets vs. Prototypes
      
      This is the critical distinction:
      
      | Aspect | Tracer Bullet | Prototype |
      |--------|--------------|-----------|
      | **Purpose** | Build the real framework, thin but complete | Explore a specific risk or question |
      | **Code quality** | Production quality | Throwaway quality |
      | **Scope** | End-to-end across all layers | Focused on one aspect or component |
      | **After completion** | Keep and extend | Throw away completely |
      | **Team visibility** | Shows real progress | Shows research findings |
      | **Risk addressed** | Integration risk, architectural unknowns | Technical feasibility, design questions |
      | **Deliverable** | Working (minimal) system | Knowledge and a decision |
      
      ### Decision Guide
      
      ```
      Is the question about whether the pieces fit together?
        → Tracer Bullet
      
      Is the question about whether one piece works at all?
        → Prototype
      
      Are you building a new feature in an existing system?
        → Tracer Bullet (add the feature end-to-end, then iterate)
      
      Are you evaluating a new technology or algorithm?
        → Prototype (test it in isolation, then decide)
      
      Do stakeholders need to see/use something?
        → Tracer Bullet (it's real, they can interact with it)
      
      Do you need to answer a technical question quickly?
        → Prototype (optimize for speed of learning)
      ```
      
      ---
      
      ## Shooting in the Dark
      
      Tracer bullets are most valuable when you can't see the target clearly:
      
      ### Unclear Requirements
      
      Users say "I want a dashboard." What does that mean? Build a tracer: one chart, one data source, deployed and accessible. Show it to users. Their reaction tells you more than any requirements document.
      
      ### New Technology Stack
      
      Team is adopting Rust for the first time. Don't spend three months building the data layer in isolation. Build a tracer: one request, from HTTP endpoint through business logic to database response, in Rust. You'll discover the pain points (borrow checker, async runtime, ORM maturity) immediately.
      
      ### Complex Integration
      
      Your system needs to coordinate five microservices, two queues, and a third-party API. Build a tracer: one transaction that flows through all of them. Integration problems surface immediately, not three months into development.
      
      ---
      
      ## Iterating on Tracer Code
      
      When a tracer bullet misses the target (stakeholders don't like what they see, performance is wrong, the architecture doesn't work), you adjust and fire again:
      
      ### The Iteration Cycle
      
      1. **Build** -- thin end-to-end implementation
      2. **Show** -- demonstrate to stakeholders and the team
      3. **Learn** -- collect feedback, observe behavior, measure performance
      4. **Adjust** -- modify the implementation based on learnings
      5. **Repeat** -- fire again with improved aim
      
      ### What "Missing" Looks Like
      
      | Miss Type | Symptom | Adjustment |
      |-----------|---------|------------|
      | Wrong feature | Users don't use it | Pivot to a different feature |
      | Wrong UX | Users are confused | Redesign the interaction model |
      | Wrong architecture | Performance is unacceptable | Restructure the layers |
      | Wrong technology | Library doesn't scale | Swap the component (orthogonality helps here) |
      | Wrong integration | Services don't coordinate well | Redesign the communication pattern |
      
      The cost of each adjustment is low because you only built a thin slice. Compare this to building the full system and discovering at the end that the architecture is wrong.
      
      ---
      
      ## Walking Skeletons
      
      A walking skeleton is a special case of tracer bullet development applied to the project's infrastructure:
      
      ### What It Includes
      
      - Source control repository setup
      - Build pipeline (compile, lint, test)
      - Deployment pipeline (staging, production)
      - Monitoring and logging
      - One trivial feature (health check endpoint, hello world page)
      
      ### Why It Matters
      
      The walking skeleton proves that your entire delivery pipeline works before you write any real features. This is enormously valuable because:
      
      - Infrastructure problems are the most painful to fix when discovered late
      - CI/CD pipeline issues block the entire team
      - Deployment automation is complex and error-prone
      - Monitoring gaps are invisible until production incidents
      
      ### Walking Skeleton Checklist
      
      | Component | Verification |
      |-----------|-------------|
      | Source control | Code is tracked, branches work, PRs are reviewed |
      | Build | `make build` (or equivalent) produces an artifact |
      | Unit tests | `make test` runs and passes (even with one trivial test) |
      | Integration tests | At least one test hits a real dependency |
      | Linting | Code style is enforced automatically |
      | Staging deploy | One command deploys to a staging environment |
      | Production deploy | Same pipeline deploys to production |
      | Monitoring | Logs are collected, metrics are reported |
      | Alerting | At least one alert fires on failure (health check) |
      
      ---
      
      ## Common Pitfalls
      
      ### Pitfall 1: Prototype Becomes Production
      
      The most dangerous mistake. A prototype is built quickly, stakeholders see it, love it, and demand it ship. The throwaway code becomes permanent.
      
      **Prevention:**
      - Write "PROTOTYPE - NOT FOR PRODUCTION" in the README, code comments, and PR description
      - Use a different repository or branch for prototypes
      - Present findings, not the code -- show screenshots, not live demos when possible
      - Make it ugly on purpose -- if it looks polished, people will want to ship it
      
      ### Pitfall 2: Tracer Bullet Becomes Big Design Up Front
      
      Teams sometimes use "tracer bullet" as justification for spending months on architecture before writing any features.
      
      **Prevention:** A true tracer bullet should be deployable within days, not weeks. If it's taking longer, you're building too much. Narrow the scope to the thinnest possible slice.
      
      ### Pitfall 3: Confusing the Two Approaches
      
      Using tracer bullet code quality for a prototype (wasting time on production quality for throwaway code) or prototype quality for a tracer bullet (shipping hack code as the foundation of the system).
      
      **Prevention:** Decide upfront which approach you're using and communicate it clearly to the team. The choice determines code quality expectations, review standards, and what happens to the code afterward.
      
      ### Pitfall 4: Never Iterating on the Tracer
      
      Building a tracer bullet and then treating it as the final architecture. The whole point is to iterate -- if you're not adjusting based on feedback, you're not using the technique correctly.
      
      **Prevention:** Plan for at least 2-3 iterations. Budget time for adjustment after each demonstration. Expect the first tracer to miss.
      
  • SKILL.md 16.2 KB
    ---
    name: pragmatic-programmer
    description: 'Apply meta-principles of software craftsmanship: DRY, orthogonality, tracer bullets, and design by contract. Use when the user mentions "best practices", "pragmatic approach", "broken windows", "tracer bullet", "software craftsmanship", "avoid technical debt", "code ownership", or "how do I become a better developer". Also trigger when evaluating build-vs-buy decisions, designing estimation approaches, or choosing between reversible and irreversible architectural decisions. Covers estimation, domain languages, and reversibility. For code-level quality, see clean-code. For refactoring techniques, see refactoring-patterns.'
    license: MIT
    metadata:
      author: wondelai
      version: "1.4.0"
    ---
    
    # The Pragmatic Programmer Framework
    
    A systems-level approach to software craftsmanship from Hunt & Thomas' "The Pragmatic Programmer" (20th Anniversary Edition). Apply these meta-principles when designing systems, reviewing architecture, writing code, or advising on engineering culture -- how to think about software, not just how to write it.
    
    ## Core Principle
    
    **Care about your craft.** Software development demands continuous learning, disciplined practice, and personal responsibility -- pragmatic programmers think beyond the immediate problem to context, trade-offs, and long-term consequences. Great software comes from great habits: avoid duplication ruthlessly, keep components orthogonal, and treat every line of code as a living asset that must earn its place. The goal is not perfection -- it is systems that are easy to change, easy to understand, and easy to trust.
    
    ## Scoring
    
    **Goal: 10/10.** Score against the seven Quick Diagnostic rows: award ~1.4 points per row answered "yes" (7 yes = 10). Then band the result:
    - **9-10**: every principle holds -- DRY knowledge, orthogonal layers, a working tracer slice, contracts at boundaries, no broken windows, reversible vendor/DB choices, ranged estimates.
    - **5-6**: 1-2 violations that cost real change-effort (e.g. business logic coupled to the DB, single-point estimates).
    - **<=3**: pervasive duplication, global state, or accumulated broken windows -- entropy is winning.
    
    Always state the score, name the failing diagnostic rows, and give the specific fix from the Action column to reach 10/10.
    
    ## The Seven Meta-Principles
    
    Seven principles for building software that lasts:
    
    ### 1. DRY (Don't Repeat Yourself)
    
    **Core concept:** Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. DRY is about knowledge, not code -- duplicated logic, business rules, or configuration are far more dangerous than duplicated syntax.
    
    **Why it works:** Duplicated knowledge must be changed in multiple places; eventually one gets missed, introducing inconsistency. DRY reduces the surface area for bugs and makes systems easier to change.
    
    **Key insights:**
    - DRY applies to knowledge and intent, not textual similarity -- two identical code blocks serving different business rules are NOT duplication
    - Four types of duplication: imposed (environment forces it), inadvertent (developers don't realize), impatient (too lazy to abstract), inter-developer (multiple people duplicate)
    - Comments that restate the code violate DRY -- explain *why*, not *what*
    - Database schemas, API specs, and documentation duplicate knowledge unless generated from a single source
    - The opposite of DRY is WET: "Write Everything Twice" or "We Enjoy Typing"
    
    **Code applications:**
    
    | Context | Pattern | Example |
    |---------|---------|---------|
    | **Config values** | Single source of truth | DB connection in one env file, referenced everywhere |
    | **Validation rules** | Shared schema | One JSON Schema or Zod schema for client and server |
    | **API contracts** | Generate from spec | OpenAPI spec generates types, docs, and client code |
    
    See: [references/dry-orthogonality.md](references/dry-orthogonality.md) when classifying a specific duplication or deciding whether two code blocks are truly the same knowledge -- per-type examples and mitigations for the four duplication types.
    
    ### 2. Orthogonality
    
    **Core concept:** Two components are orthogonal if changes in one do not affect the other. Design systems where components are self-contained, independent, and have a single, well-defined purpose.
    
    **Why it works:** Decoupling localizes change -- a fix in one module can't ripple into unrelated ones, so blast radius stays bounded. Change the database layer and the UI should not break; change the auth provider and business logic should not care.
    
    **Key insights:**
    - Ask: "If I dramatically change the requirements behind a function, how many modules are affected?" The answer should be one
    - Eliminate effects between unrelated things -- a logging change should never break billing
    - Layered architectures promote orthogonality: presentation, domain logic, data access
    - Avoid global data -- every consumer of global state is coupled to it
    - Frameworks that force you to inherit from their classes reduce orthogonality
    
    **Code applications:**
    
    | Context | Pattern | Example |
    |---------|---------|---------|
    | **Architecture** | Layered separation | Controller -> Service -> Repository, each replaceable |
    | **Dependencies** | Dependency injection | Pass a `Notifier` interface, not a `SlackClient` concrete class |
    | **Testing** | Isolated unit tests | Test business logic without database, network, or filesystem |
    
    See: [references/dry-orthogonality.md](references/dry-orthogonality.md) when measuring coupling or refactoring toward decoupled layers -- the change-impact and stranger tests, layered-architecture diagram, and the helicopter analogy.
    
    ### 3. Tracer Bullets and Prototypes
    
    **Core concept:** Tracer bullets are end-to-end implementations connecting all layers of the system with minimal functionality. Unlike prototypes (which are throwaway), tracer bullet code is production code -- thin but real.
    
    **Why it works:** Tracer bullets give immediate end-to-end feedback before you invest in filling out every feature. Users see something real, developers have a framework to build on, and integration issues surface early.
    
    **Key insights:**
    - Tracer bullet: thin but complete path through the system (UI -> API -> DB) -- you keep it
    - Prototype: focused exploration of a single risky aspect -- you throw it away
    - Use tracer bullets when "shooting in the dark" -- vague requirements, unproven architecture
    - If a tracer misses, adjust and fire again -- the cost of iteration is low
    - Label prototypes clearly as throwaway -- never let one become production code
    
    **Code applications:**
    
    | Context | Pattern | Example |
    |---------|---------|---------|
    | **New project** | Vertical slice | One feature end-to-end: button -> API -> DB -> response |
    | **Uncertain tech** | Spike prototype | Test WebSocket performance before committing |
    | **Microservice** | Walking skeleton | Hello-world service through the full CI/CD pipeline |
    
    See: [references/tracer-bullets.md](references/tracer-bullets.md) when deciding tracer vs. prototype on a new project or building a walking skeleton -- the shooting-in-the-dark decision, iteration loop, and common pitfalls.
    
    ### 4. Design by Contract and Assertive Programming
    
    **Core concept:** Define and enforce the rights and responsibilities of software modules through preconditions (what must be true before), postconditions (what is guaranteed after), and invariants (what is always true). When a contract is violated, fail immediately and loudly.
    
    **Why it works:** Contracts make assumptions explicit. Instead of silently corrupting data or limping along in an invalid state, the system crashes at the point of the problem -- dead programs tell no lies.
    
    **Key insights:**
    - Preconditions: caller's responsibility -- "I accept only positive integers"
    - Postconditions: routine's guarantee -- "I will return a sorted list"
    - Invariants: always true -- "Account balance never goes negative"
    - Crash early: a dead program does far less damage than a crippled one
    - Use assertions for things that should never happen; error handling for things that might
    - In dynamic languages, implement contracts through runtime checks and guard clauses
    
    **Code applications:**
    
    | Context | Pattern | Example |
    |---------|---------|---------|
    | **Function entry** | Precondition guard | `assert age >= 0, "Age cannot be negative"` at function start |
    | **Class state** | Invariant validation | `validate!` called after every state mutation |
    | **API boundary** | Schema validation | Validate request body against schema before processing |
    
    See: [references/contracts-assertions.md](references/contracts-assertions.md) when adding contracts to a routine or deciding assertion vs. error handling -- worked pre/post/invariant patterns, dynamic-language guard clauses, and the assertions-vs-error-handling boundary.
    
    ### 5. The Broken Window Theory
    
    **Core concept:** One broken window -- a badly designed piece of code, a poor management decision, a hack that "we'll fix later" -- starts the rot. Once a system shows neglect, entropy accelerates and discipline collapses.
    
    **Why it works:** Psychology. When code is clean, developers feel social pressure to keep it that way; when code is already messy, the threshold for adding more mess drops to zero. Quality is a team habit, not an individual heroic effort.
    
    **Key insights:**
    - Don't leave broken windows (bad designs, wrong decisions, poor code) unrepaired
    - If you can't fix it now, board it up: a TODO with a ticket, a disabled feature, a stub
    - Be a catalyst for change: show people a working glimpse of the future (stone soup)
    - Watch for slow degradation (boiled frog) -- monitor tech debt metrics over time
    - The first hack is the most expensive because it gives permission for all subsequent hacks
    
    **Code applications:**
    
    | Context | Pattern | Example |
    |---------|---------|---------|
    | **Legacy code** | Board up windows | Wrap bad code in a clean interface before adding features |
    | **Code review** | Zero-tolerance for new debt | Reject PRs adding `// TODO: fix later` without a ticket |
    | **Tech debt** | Debt budget | Allocate 20% of each sprint to fixing broken windows |
    
    See: [references/broken-windows.md](references/broken-windows.md) when a team is normalizing neglect or you need to drive a turnaround -- repair strategies, the stone-soup catalyst play, and building a culture of quality.
    
    ### 6. Reversibility and Flexibility
    
    **Core concept:** There are no final decisions. Build systems that make it easy to change your mind about databases, frameworks, vendors, architecture, and deployment targets -- the cost of change should be proportional to the scope of change.
    
    **Why it works:** Requirements change, vendors get acquired, technologies fall out of favor. If your architecture hard-codes assumptions about any of these, every change becomes a rewrite; flexible architecture treats decisions as configuration, not structure.
    
    **Key insights:**
    - Abstract third-party dependencies behind your own interfaces -- never let vendor APIs leak into business logic
    - The "forking road" test: could you switch from Postgres to DynamoDB in a week? If not, you're coupled
    - Metadata-driven systems (config files, feature flags) are more flexible than hard-coded logic
    - YAGNI applies to premature abstraction too -- don't build flexibility you don't need yet
    - Reversibility is not predicting the future; it's not painting yourself into a corner
    
    **Code applications:**
    
    | Context | Pattern | Example |
    |---------|---------|---------|
    | **Database** | Repository pattern | Business logic calls `repo.save(user)`, not `pg.query(...)` |
    | **External API** | Adapter/wrapper | `PaymentGateway` interface wraps Stripe; swap to Braintree later |
    | **Feature flags** | Runtime toggles | New checkout flow behind a flag, rollback in seconds |
    
    See: [references/reversibility.md](references/reversibility.md) when committing to a vendor or framework, or weighing how reversible a decision must be -- per-layer reversibility patterns, the forking-road test, and when NOT to optimize for reversibility.
    
    ### 7. Estimation and Knowledge Portfolio
    
    **Core concept:** Learn to estimate reliably by understanding scope, building models, decomposing into components, and assigning ranges. Manage your learning like a financial portfolio: invest regularly, diversify, and rebalance.
    
    **Why it works:** Honest estimation builds trust with stakeholders ("1-3 weeks" beats a confidently wrong "2 weeks"). A knowledge portfolio keeps you relevant as technologies shift -- the programmer who stops learning stops being effective.
    
    **Key insights:**
    - Ask "what is this estimate for?" -- context determines precision (budget planning vs. sprint planning)
    - Use PERT: (Optimistic + 4x Most Likely + Pessimistic) / 6
    - Decompose into components and estimate each; the sum is more accurate than a single guess
    - Keep an estimation log: compare estimates to actuals and calibrate
    - Portfolio rules: invest regularly (learn weekly), diversify beyond your stack, mix safe and speculative bets, learn emerging tech early (buy low)
    
    **Code applications:**
    
    | Context | Pattern | Example |
    |---------|---------|---------|
    | **Sprint planning** | Range estimates | "3-5 days" with confidence level, not a single number |
    | **New technology** | Time-boxed spike | "2 days evaluating; then I can estimate properly" |
    | **Learning** | Weekly investment | 1 hour/week on a new language, tool, or domain |
    
    See: [references/estimation-portfolio.md](references/estimation-portfolio.md) when producing an estimate you'll be held to or calibrating past misses -- the PERT and decomposition procedures, an estimation-log calibration loop, and portfolio rebalancing.
    
    ## Common Mistakes
    
    | Mistake | Why It Fails | Fix |
    |---------|-------------|-----|
    | DRY-ing similar-looking code that serves different purposes | Couples unrelated concepts; changes to one break the other | Only DRY knowledge, not coincidental code similarity |
    | Skipping tracer bullets, building layer-by-layer | Integration issues surface late; no end-to-end feedback | Build one thin vertical slice first |
    | Ignoring broken windows "because we'll refactor later" | Entropy accelerates; later never comes; morale drops | Fix immediately or board up with a tracked ticket |
    | Estimates as single-point commitments | False precision erodes trust when missed | Always give ranges with confidence levels |
    | Making everything "flexible" upfront | Over-engineering; abstraction without evidence of need | Add flexibility when you have concrete evidence you'll need it |
    | Removing production assertions "for performance" | Bugs assertions would catch now silently corrupt data | Keep critical assertions; benchmark before removing any |
    | Global state "for convenience" | Destroys orthogonality; everything coupled to everything | Use dependency injection and explicit parameters |
    
    ## Quick Diagnostic
    
    | Question | If No | Action |
    |----------|-------|--------|
    | Can I change the database without touching business logic? | Orthogonality violation | Introduce repository/adapter pattern |
    | Do I have an end-to-end slice working? | Missing tracer bullet | Build one vertical slice before expanding |
    | Is every business rule defined in exactly one place? | DRY violation | Identify the authoritative source; remove duplicates |
    | Would a new developer call this codebase "clean"? | Broken windows present | Schedule a dedicated cleanup sprint |
    | Do my estimates include ranges and confidence levels? | Estimation problem | Switch to PERT or range-based estimates |
    | Can I roll back this deployment in under 5 minutes? | Reversibility gap | Add feature flags and blue-green deploys |
    | Am I learning something new every week? | Knowledge portfolio stagnant | Schedule weekly learning time and track it |
    
    ## Further Reading
    
    - [The Pragmatic Programmer: Your Journey to Mastery, 20th Anniversary Edition](https://www.amazon.com/Pragmatic-Programmer-journey-mastery-Anniversary/dp/0135957052?tag=wondelai00-20) by Andrew Hunt and David Thomas
    
    ## About the Authors
    
    **Andrew Hunt** and **David Thomas** co-founded the Pragmatic Bookshelf and were among the 17 original authors of the Agile Manifesto. Thomas coined "DRY" and "Code Kata" and co-authored *Programming Ruby* (the Pickaxe book); Hunt focuses on how teams learn, communicate, and maintain quality. Together they wrote *The Pragmatic Programmer*, one of the most influential software books ever published.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related