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:
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:
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:
---
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:
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:
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 |
|---|---|
| 1 | Care about your craft |
| 2 | Think about your work |
| 3 | You have agency — don't say "I can't" |
| 4 | Provide options, not excuses |
| 5 | Don't live with broken windows |
| 6 | Be a catalyst for change |
| 7 | Remember the big picture |
| 8 | Make quality a requirements issue |
| 9 | Invest regularly in your knowledge portfolio |
| 10 | Critically analyze what you read and hear |
| 11 | It's both what you say and the way you say it |
| 12 | English is just another programming language |
| 14 | There are no final decisions |
| 17 | Program close to the problem domain |
| 19 | Solve the underlying problem, not just the symptom |
| 22 | Don't repeat yourself |
| 23 | Make it easy to reuse |
| 24 | Isolate concerns to minimize coupling |
| 28 | Use tracer bullets to find the target |
| 29 | Prototype to learn |
| 36 | You can't write perfect software |
| 37 | Design with contracts |
| 38 | Crash early |
| 40 | Use assertions to prevent the impossible |
| 43 | Finish what you start |
| 44 | Act locally |
| 45 | Take small steps — always |
| 51 | Don't program by coincidence |
| 55 | Test your software, or your users will |
| 62 | Test state coverage, not code coverage |
| 66 | Find bugs once |
| 67 | Treat English as just another programming language |
| 89 | Don't use manual procedures |
| 93 | Sign 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
| Concept | Core Idea |
|---|---|
| Care about your craft | You are a craftsman. Standards matter. |
| Broken Windows | Small decays accelerate; fix or acknowledge them |
| Knowledge Portfolio | Treat learning like investing — consistent, diversified |
| DRY | One source of truth for every piece of knowledge |
| Orthogonality | Changes should be local; decouple aggressively |
| Tracer Bullets | Build a thin end-to-end slice before filling in detail |
| Design by Contract | Make preconditions and postconditions explicit |
| Crash Early | A dead program causes less damage than a corrupted one |
| Automate Everything | If humans run it, humans will eventually get it wrong |
| Ruthless Testing | Tests protect you and your future self |