Home / Notebooks / Software Engineering
Software Engineering
intermediate

The Pragmatic Programmer

Key principles and tips from The Pragmatic Programmer by David Thomas & Andrew Hunt — a timeless guide to becoming a better software craftsman

April 28, 2026
Updated regularly

The Pragmatic Programmer

A distillation of the core principles from the landmark book by David Thomas & Andrew Hunt — written for developers who care about their craft.

> "We who cut mere stones must always be envisioning cathedrals." — Medieval quarryman's creed

---

Chapter 1: A Pragmatic Philosophy

Take Responsibility

Pragmatic programmers own their outcomes. When something goes wrong, don't make excuses — provide options:

❌ "The server went down and I couldn't deploy."
✅ "The deployment failed. Here's my plan to recover and prevent it next time."

Tip: Before delivering bad news, think through what you'll do about it. Come with a solution, not just a problem.

Don't Live with Broken Windows

> "Don't leave broken windows (bad designs, wrong decisions, poor code) unrepaired."

The Broken Window Theory applied to software:

One broken window → Neglect spreads → Team morale drops → Quality collapses
                         ↑
              "No one else cares, why should I?"

Fix bad code when you see it. If you can't fix it immediately, board it up — add a comment, open a ticket, flag it. The act of acknowledging it matters.

Be a Catalyst for Change

Don't ask for permission to improve things. Start small, show results, let others want to join:

Boiled Frog Problem:
  Sudden change  → People notice and resist
  Gradual change → People adapt without noticing the problem

Apply in reverse:
  Small improvement → Others see value → Momentum builds

Invest in Your Knowledge Portfolio

Your knowledge and experience are your most important professional assets — and they depreciate. Treat them like a financial portfolio:

Knowledge Portfolio Principles:
├── Invest regularly (consistent learning beats occasional cramming)
├── Diversify (languages, domains, paradigms, soft skills)
├── Rebalance periodically (what's valuable changes over time)
└── Manage risk (cutting-edge is high risk, foundational is low risk)

Goals to set:

  • Learn one new language every year
  • Read one technical book every quarter
  • Take courses outside your current stack
  • Participate in user groups, conferences, communities
  • Experiment — actually write code in new technologies
  • Communicate Effectively

    The best code in the world fails if you can't explain it. Know your audience:

    WISDOM acronym for planning communication:
      W → What do you want them to learn?
      I → What is their Interest in what you've got to say?
      S → How Sophisticated are they?
      D → How much Detail do they want?
      O → Whom do you want to Own the information?
      M → How can you Motivate them to listen?
    

    ---

    Chapter 2: A Pragmatic Approach

    DRY — Don't Repeat Yourself

    > "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."

    DRY is not just about code duplication. It applies to:

    Types of duplication:
    ├── Imposed duplication   → Two places express the same business rule
    ├── Inadvertent duplication → Two structs represent the same concept
    ├── Impatient duplication  → Copied because it was "faster"
    └── Interdeveloper duplication → Two people solved the same problem
    

    DRY violations are expensive:

    Knowledge in two places:
      One changes → Other doesn't → Bug
      Both change → Have to find all places → Risk of missing one
    

    Orthogonality

    Two systems are orthogonal when changing one does not affect the other.

    Non-orthogonal system:
      Change the database schema → Must also change the UI
      Change the UI layout      → Must also update data model
    
    Orthogonal system:
      Database layer    → Independent
      Business logic    → Independent
      Presentation layer → Independent
    

    Benefits:

  • Bugs are isolated — a change in component A can't break component B
  • Easier to test — each component can be tested in isolation
  • Faster development — teams can work in parallel
  • Test for orthogonality: "If I change this module, how many other modules change?" The answer should be zero or one.

    Tracer Bullets vs. Prototypes

    Two different tools for navigating uncertainty:

    ┌──────────────────────────────────────────────────────┐
    │  TRACER BULLETS           │  PROTOTYPES              │
    ├──────────────────────────────────────────────────────┤
    │  Production quality code  │  Throwaway code          │
    │  Incomplete but working   │  Complete but fake       │
    │  Hits real targets        │  Explores the unknown    │
    │  Kept and built upon      │  Discarded when done     │
    │  Shows progress           │  Answers a question      │
    └──────────────────────────────────────────────────────┘
    

    Use a tracer bullet when you need to hit a target in the dark — build an end-to-end skeleton that actually works, then fill it in.

    Use a prototype when you need to explore a specific risk area — UI mockup, spike solution, algorithm test. Always throw it away.

    Estimating

    All estimates are approximations. Choose your units to communicate precision:

    Duration          Units to Use
    ─────────────────────────────
    1–15 days       → Days
    3–8 weeks       → Weeks
    8–30 weeks      → Months
    30+ weeks       → Think hard before committing
    
    "When will you be done?"
    → Better: "I'll have a better estimate after I understand X"
    → Better: Give a range — "between 4 and 6 weeks"
    

    Estimation process:

  • Understand what's being asked
  • Build a rough model of the system
  • Break the model into components
  • Assign estimates to each component
  • Track your record and refine
  • ---

    Chapter 3: The Basic Tools

    The Power of Plain Text

    Store knowledge in plain text. It survives longer than any binary format:

    Binary data   → Tied to the application that created it
                   → Unreadable without the right tool
                   → Hard to diff and version control
    
    Plain text    → Human-readable
                   → Works with every tool (grep, awk, diff, git)
                   → Survives software changes
                   → Self-documenting
    

    Use plain text for: configuration, data exchange, documentation, logs, scripts.

    Master Your Tools

    A craftsman knows their tools deeply. For developers:

    Invest in mastering:
    ├── Shell       → Automate repetitive tasks, chain tools
    ├── Editor      → Learn every shortcut; it compounds over a career
    ├── Source control → git is non-optional; know branching and history
    ├── Debugger    → Step through; don't just add print statements
    └── Build tools → Understand what runs and when
    

    The goal: reduce friction. Every time you manually do something a script could do, you're paying a tax on every future occurrence.

    ---

    Chapter 4: Pragmatic Paranoia

    > "Pragmatic programmers don't trust themselves either."

    Design by Contract (DbC)

    Every function has a contract. Make it explicit:

    Preconditions  → What must be true before calling me?
    Postconditions → What will be true after I return?
    Class invariants → What is always true about this object?
    
    function withdraw(amount: number): void {
      // Precondition:  amount > 0, balance >= amount
      // Postcondition: balance = old(balance) - amount
    }
    

    If a precondition fails → caller's bug. If a postcondition fails → function's bug.

    Dead Programs Tell No Lies

    Crash early. Don't continue when something impossible has happened:

    ❌ Defensive programming:
    if (value != null) {
      // silently ignore the error and continue
    }
    
    ✅ Pragmatic paranoia:
    assert(value != null, "Expected non-null value in processOrder")
    // If this fires, we learn something real
    

    A program that crashes is less dangerous than one that silently produces wrong output.

    Assertive Programming

    Use assertions for things that should never happen:

    // Assertions document assumptions and catch violations:
    assert(list.length > 0);        // "I assumed this was non-empty"
    assert(user.role !== undefined); // "User must always have a role"
    

    Turn assertions on in production (at least for critical paths). Disabling them is hiding bugs.

    How to Balance Resources

    Every resource you acquire must be released. LIFO order for release:

    Allocate in this order:  A → B → C
    Release in this order:   C → B → A  (LIFO)
    
    In code:
      open file
        try:
          open database
            try:
              process(file, database)
            finally:
              close database
        finally:
          close file
    

    Never rely on callers to clean up. Use finally, with, RAII, or destructors.

    ---

    Chapter 5: Bend, Don't Break

    Decoupling

    Tightly coupled code is hard to change. Keep your modules shy:

    Coupled code:
      A calls B which calls C which calls D
      Change D → affects C → affects B → affects A
    
    Decoupled code:
      A → [interface] ← B
      Change B's internals → A doesn't care
    
    Law of Demeter (Principle of Least Knowledge):
    A method should only call:
      ├── Methods on itself
      ├── Methods on objects passed as parameters
      ├── Methods on objects it creates
      └── Methods on direct component objects
    
    ❌ customer.wallet.money.amount  (train wreck)
    ✅ customer.getBalance()
    

    Tell, Don't Ask

    Don't ask for state to make decisions outside an object — tell the object what to do:

    ❌ Ask:
    if (account.getBalance() > amount) {
      account.setBalance(account.getBalance() - amount);
    }
    
    ✅ Tell:
    account.withdraw(amount);  // The object owns its own logic
    

    Avoid Global State

    Global state creates hidden coupling between unrelated modules:

    Hidden dependency:
      Module A writes to global config
      Module B reads from global config
      → A and B are coupled without any obvious connection
      → Testing B requires setting up A's state first
      → Changing A's output silently breaks B
    

    ---

    Chapter 6: While You Are Coding

    Programming by Coincidence

    Don't code by accident. Understand why your code works:

    Coincidental programming:
      "I added this library call and it started working"
      → You don't know why it works
      → You don't know when it will break
      → You can't explain it to a colleague
    
    Deliberate programming:
      "This works because X calls Y which transforms Z"
      → You can reason about edge cases
      → You can adapt it when requirements change
    

    Signs you're programming by coincidence:

  • You copy-paste without reading what the code does
  • You keep adding things until it stops crashing
  • You're afraid to touch working code
  • You can't explain why something is the way it is
  • Algorithm Complexity

    Know the Big O of what you're writing. An O(n²) algorithm is invisible at n=100 but catastrophic at n=100,000:

    n = 1,000,000 operations:
    
    O(1)       → 1 op
    O(log n)   → 20 ops
    O(n)       → 1,000,000 ops
    O(n log n) → 20,000,000 ops
    O(n²)      → 1,000,000,000,000 ops  ← 1 trillion
    

    Profile before optimizing. Don't guess.

    Refactor Early, Refactor Often

    Refactoring is not rewriting. It is improving the design of existing code without changing its behavior.

    When to refactor:
      ├── Duplication discovered
      ├── Non-orthogonal design
      ├── Outdated knowledge
      ├── Performance problems
      └── Tests allow it safely
    
    Refactoring is not:
      ├── Adding new features simultaneously
      ├── Fixing bugs simultaneously
      └── Rewriting from scratch
    

    Refactor in small, tested steps. Never refactor and add features in the same commit.

    Write Code That's Easy to Test

    Code that's hard to test is hard to change. Testability is a design quality:

    Hard to test (symptoms):
      ├── Functions with many side effects
      ├── Hardcoded dependencies (new Database() inside a function)
      ├── Global state modifications
      └── Functions that do too many things
    
    Easy to test (design for it):
      ├── Inject dependencies (pass them in)
      ├── Pure functions (same input → same output)
      ├── Small, focused functions
      └── Separated concerns
    

    Rubber Duck Debugging

    Explain your problem out loud — to a rubber duck, a colleague, yourself:

    Process:
      1. State the problem clearly
      2. Explain what the code does, line by line
      3. Often: you find the bug mid-explanation
    
    Why it works:
      Explaining forces you to examine assumptions you skipped over
      You're forced to say what the code "should" do vs. what it does
    

    ---

    Chapter 7: Before the Project

    The Requirements Pit

    Requirements are rarely given — they are discovered through iteration:

    "Make the system faster"
    → What does faster mean? 200ms? 50ms?
    → For which users? Which operations?
    → Under which load conditions?
    → How do we measure success?
    
    Real requirement: "The checkout page must load in under 300ms for 95% of users under peak load"
    

    Requirements dig themselves out of the ground. Your job is to ask the right questions:

    Useful questions:
      ├── What are you trying to achieve (not just what do you want)?
      ├── What does success look like?
      ├── What happens if this isn't built?
      └── Who else is affected by this?
    

    Impossible Puzzles

    When you're stuck, look for constraints you've assumed but haven't verified:

    The key question: "Does it have to be done this way?"
    
    Common false constraints:
      ├── "We have to use this technology"  → Do you?
      ├── "It must be done this week"       → Says who?
      ├── "It has to work like this"        → Why?
      └── "We can't change the API"         → Have you asked?
    
    Step back → List the constraints explicitly → Challenge each one
    

    ---

    Chapter 8: Pragmatic Projects

    Ubiquitous Automation

    Manual procedures breed inconsistency. Automate everything repeatable:

    Automate:
      ├── Build            → Make/Gradle/npm scripts
      ├── Testing          → Run on every commit, not just before release
      ├── Deployment       → CI/CD pipelines, not manual steps
      ├── Code quality     → Linters, formatters on save/commit
      └── Environment setup → Docker, dev containers, scripts
    
    If a human runs it by hand, it will eventually be done wrong.
    

    Ruthless Testing

    Tests that aren't run are worthless. Tests that don't find bugs tell you nothing:

    Testing levels:
    ┌──────────────────────────────────────────────────┐
    │  Unit Tests      → Test individual functions     │
    │  Integration     → Test component interactions   │
    │  Validation      → Are we building the right thing│
    │  Performance     → Does it meet SLAs?            │
    │  Usability       → Can real users use it?        │
    └──────────────────────────────────────────────────┘
    
    Test state coverage, not just code coverage:
      100% line coverage ≠ tested all behaviors
      Edge cases: empty, null, max values, concurrent access
    

    The three questions for every bug:

  • Was this covered by a test?
  • Why did the test not catch it?
  • What test do I add now so it never returns?
  • Pragmatic Teams

    Everything that applies to individual programmers applies to teams:

    Team DRY: Avoid knowledge silos
      → Every piece of knowledge has one owner, but everyone can find it
      → Documentation lives where code lives
      → Shared vocabulary for the domain
    
    Team Orthogonality:
      → Teams own components, not tasks
      → Minimize cross-team dependencies
      → Clear interfaces between team responsibilities
    
    Team Automation:
      → Build pipelines run identically for everyone
      → No "works on my machine" — use containers, locked deps
    

    ---

    The 100 Tips — Quick Reference

    Selected tips from the book:

    #Tip
    1Care about your craft
    2Think about your work
    3You have agency — don't say "I can't"
    4Provide options, not excuses
    5Don't live with broken windows
    6Be a catalyst for change
    7Remember the big picture
    8Make quality a requirements issue
    9Invest regularly in your knowledge portfolio
    10Critically analyze what you read and hear
    11It's both what you say and the way you say it
    12English is just another programming language
    14There are no final decisions
    17Program close to the problem domain
    19Solve the underlying problem, not just the symptom
    22Don't repeat yourself
    23Make it easy to reuse
    24Isolate concerns to minimize coupling
    28Use tracer bullets to find the target
    29Prototype to learn
    36You can't write perfect software
    37Design with contracts
    38Crash early
    40Use assertions to prevent the impossible
    43Finish what you start
    44Act locally
    45Take small steps — always
    51Don't program by coincidence
    55Test your software, or your users will
    62Test state coverage, not code coverage
    66Find bugs once
    67Treat English as just another programming language
    89Don't use manual procedures
    93Sign your work
    ---

    Key Mental Models

    The Broken Window Theory
      One ignored problem becomes many → Fix it or flag it immediately
    
    The Boiled Frog
      Gradual decay is invisible → Step back and look at the big picture
    
    Tracer Bullets
      Build thin slices end-to-end → Validate before filling in
    
    DRY Principle
      Every piece of knowledge → one authoritative place
    
    Orthogonality
      Change here shouldn't affect there → Design for independence
    
    Design by Contract
      Make expectations explicit → Catch violations at the boundary
    
    Dead Programs Tell No Lies
      Crash on impossible states → Silent wrong output is worse than a crash
    
    Rubber Duck Debugging
      Explain it aloud → The explanation surfaces the assumption
    

    ---

    Key Takeaways

    ConceptCore Idea
    Care about your craftYou are a craftsman. Standards matter.
    Broken WindowsSmall decays accelerate; fix or acknowledge them
    Knowledge PortfolioTreat learning like investing — consistent, diversified
    DRYOne source of truth for every piece of knowledge
    OrthogonalityChanges should be local; decouple aggressively
    Tracer BulletsBuild a thin end-to-end slice before filling in detail
    Design by ContractMake preconditions and postconditions explicit
    Crash EarlyA dead program causes less damage than a corrupted one
    Automate EverythingIf humans run it, humans will eventually get it wrong
    Ruthless TestingTests protect you and your future self
    ---

    Resources

  • The Pragmatic Programmer, 20th Anniversary Edition — David Thomas & Andrew Hunt
  • pragprog.com — Publisher's site with errata and extras
  • Clean Code — Robert C. Martin
  • A Philosophy of Software Design — John Ousterhout
  • Refactoring — Martin Fowler
  • Topics

    Software EngineeringBest PracticesClean CodeCareerCraftsmanship

    Found This Helpful?

    If you have questions or suggestions for improving these notes, I'd love to hear from you.