"The Pragmatic Programmer" by Andy Hunt and Dave Thomas remains one of the most influential books in software engineering, not just for its technical advice, but for its memorable stories and analogies that make complex principles stick. These narratives transcend mere programming tips—they offer profound insights into how we approach problems, work with teams, and evolve as professionals.
Let me share some of the most impactful stories from this classic text and explore how they continue to guide developers in our modern landscape.
One of the cornerstones of pragmatic philosophy is taking responsibility for yourself and your actions. When things inevitably go wrong on a project—late deliveries, unexpected technical issues, or system failures—there's often a temptation to deflect blame onto external factors like vendors, programming languages, management, or coworkers.
The authors paint a vivid picture: "If there was a risk that the vendor wouldn't come through for you, then you should have had a contingency plan. If your mass storage melts—taking all of your source code with it—and you don't have a backup, it's your fault. Telling your boss 'the cat ate my source code' just won't cut it."
Above all, your team needs to trust and rely on you. In a healthy environment based on trust, you can safely speak your mind, present ideas, and rely on team members who can in turn rely on you. Without trust, even the most skilled team becomes like "a high-tech, stealth ninja team infiltrating the villain's lair, but they can't trust each other"—doomed to failure.
The Lesson: When you accept responsibility for an outcome, expect to be held accountable. When mistakes happen (and they will), admit them honestly and offer options. Don't make excuses—provide solutions. Test your excuse by talking to "the rubber duck on your monitor, or the cat" before presenting it.
While software development is immune from almost all physical laws, entropy hits us hard. Entropy—the amount of "disorder" in a system—inexorably increases, and when disorder increases in software, we call it "software rot" or "technical debt."
There are many factors contributing to software rot, but the most important seems to be psychology and culture. Despite the best-laid plans and best people, projects can experience ruin and decay. Yet other projects, despite enormous difficulties and constant setbacks, successfully fight entropy and survive.
The difference often comes down to broken windows. Psychologists have shown that hopelessness can be contagious, like a flu virus in close quarters. Ignoring clearly broken situations reinforces ideas that perhaps nothing can be fixed, that no one cares, that all is doomed—negative thoughts that spread among team members, creating a vicious spiral.
The authors share a striking example: Andy once knew someone obscenely rich whose house was immaculate, loaded with priceless antiques and art. One day, a tapestry hanging too close to a fireplace caught fire. The fire department rushed in to save the day—but before dragging their big, dirty hoses into the house, they stopped with the fire raging to roll out a mat between the front door and the fire source. They didn't want to mess up the carpet.
The Lesson: Don't live with broken windows. Fix each one as soon as it's discovered. If insufficient time exists for proper fixes, "board it up"—comment out offending code, display "Not Implemented" messages, or substitute dummy data. Take action to prevent further damage and show you're on top of the situation. One broken window is one too many.
The classic folk tale tells of three hungry soldiers returning from war who encounter a village where doors are locked and windows closed. After years of war, villagers are short of food and hoard what they have.
Undeterred, the soldiers boil a pot of water and carefully place three stones into it. Amazed villagers come out to watch. "This is stone soup," the soldiers explain. "Is that all you put in it?" ask the villagers. "Absolutely—although some say it tastes even better with a few carrots..." A villager runs off, returning with carrots from his hoard.
Minutes later, villagers ask again, "Is that it?" "Well," say the soldiers, "a couple of potatoes give it body." Off runs another villager. Over the next hour, soldiers list more ingredients that would enhance the soup: beef, leeks, salt, herbs. Each time a different villager runs off to raid personal stores.
Eventually they produce a large pot of steaming soup. The soldiers remove the stones, and the entire village sits down to enjoy the first square meal any had eaten in months.
The soldiers act as catalysts, bringing the village together to jointly produce something none could have done alone—a synergistic result where everyone wins.
The Lesson: Sometimes you know exactly what needs doing and how to do it. The entire system appears before your eyes—you know it's right. But ask permission to tackle the whole thing and you'll meet delays and blank stares. People form committees, budgets need approval, things get complicated. Everyone guards their resources. Work out what you can reasonably ask for. Develop it well. Once you've got it, show people and let them marvel. Then say, "of course, it would be better if we added..." Pretend it's not important. People find it easier to join ongoing success.
The stone soup story is also about gentle, gradual deception and focusing too tightly. The villagers think about the stones and forget about the rest of the world. We all fall for it every day. Things just creep up on us.
Projects slowly and inexorably get out of hand. Most software disasters start too small to notice, and most project overruns happen a day at a time. Systems drift from specifications feature by feature, while patch after patch gets added to code until nothing of the original remains. It's often the accumulation of small things that breaks morale and teams.
"They" say that if you take a frog and drop it into boiling water, it will jump straight back out. However, if you place the frog in cold water, then gradually heat it, the frog won't notice the slow temperature increase and will stay put until cooked.
The frog's problem differs from broken windows. In broken window theory, people lose the will to fight entropy because they perceive no one else cares. The frog just doesn't notice the change.
The Lesson: Don't be like the fabled frog. Keep an eye on the big picture. Constantly review what's happening around you, not just what you personally are doing. Remember the big picture.
There's an old joke about a company placing an order for 100,000 integrated circuits with a Japanese manufacturer. Part of the specification was the defect rate: one chip in 10,000. A few weeks later the order arrived: one large box containing thousands of ICs, and a small one containing just ten. Attached to the small box was a label reading: "These are the faulty ones."
If only we had this kind of control over quality. But the real world won't let us produce much that's truly perfect, particularly not bug-free software. Time, technology, and temperament all conspire against us.
Programming is like painting in some ways. You start with a blank canvas and basic raw materials. You use science, art, and craft to determine what to do with them. You sketch overall shape, paint underlying environment, then fill in details. You constantly step back with a critical eye to view what you've done. Sometimes you'll throw a canvas away and start again.
But artists know that all hard work is ruined if you don't know when to stop. If you add layer upon layer, detail over detail, the painting becomes lost in the paint.
The Lesson: Don't spoil a perfectly good program by overembellishment and overrefinement. Move on, and let your code stand in its own right for a while. It may not be perfect. Don't worry: it could never be perfect. Make quality a requirements issue, working with users to define what "good enough" means for their needs.
Picture this scenario: You're on a helicopter tour of the Grand Canyon when the pilot, who made the obvious mistake of eating fish for lunch, suddenly groans and faints. Fortunately, he left you hovering 100 feet above the ground.
As luck would have it, you'd read a Wikipedia page about helicopters the previous night. You know helicopters have four basic controls. The cyclic is the stick you hold in your right hand—move it, and the helicopter moves in the corresponding direction. Your left hand holds the collective pitch lever—pull up to increase pitch on all blades, generating lift. At the end of the pitch lever is the throttle. Finally, you have two foot pedals that vary tail rotor thrust and help turn the helicopter.
What you probably didn't realize (because the Wikipedia page was too short) is that all these controls are interconnected. Move the cyclic forward to increase forward speed, and the nose drops, the helicopter descends, and it spirals to the left. To stay straight and level, you have to push the collective down, decrease the throttle, and press the right pedal. As you compensate, the new settings throw you off again, and you need to keep making further adjustments.
Helicopter controls are decidedly not orthogonal—they're tightly coupled, making the machine incredibly complex to control and modify.
The Lesson: Design orthogonal systems where components are self-contained and focused on single responsibilities. When you change one component in an orthogonal system, you don't need to worry about the others. This leads to increased productivity, reduced risk, and easier testing. Eliminate effects between unrelated things.
The Dodo bird, known for its placid nature, was unable to adapt to new threats and became extinct. This serves as a cautionary tale against rigidity and lack of adaptability in software systems.
The Lesson: Don't let your project (or your career) go the way of the dodo. To remain flexible and adaptable in the face of change, minimize hard-coded details. Move as much changeable information as possible—credentials, logging levels, tax rates, user interface styles—out of your main codebase and into external configuration files. This allows for easier and safer adjustments without altering code.
You're in your favorite diner. You finish your main course and ask your server if there's any apple pie left. He looks over his shoulder, sees one piece in the display case, and says yes. You order it and sigh contentedly.
Meanwhile, on the other side of the restaurant, another customer asks their server the same question. She also looks, confirms there's a piece, and that customer orders too.
One of the customers is going to be disappointed.
Swap the display case for a joint bank account, and turn the waitstaff into point-of-sale devices. You and your partner both decide to buy a new phone at the same time, but there's only enough in the account for one. Someone—the bank, the store, or you—is going to be very unhappy.
The problem is shared state. Each server looked into the display case without regard for the other. Each point-of-sale device looked at an account balance without regard for the other.
Let's examine this as code:
if display_case.pie_count > 0
promise_pie_to_customer()
display_case.take_pie()
give_pie_to_customer()
end
Waiter 1 gets the current pie count and finds it's one. He promises the pie to the customer. But at that point, waiter 2 runs. She also sees the pie count is one and makes the same promise to her customer. One then grabs the last piece of pie, and the other waiter enters some kind of error state (involving much groveling).
The problem isn't that two processes can write to the same memory. The problem is that neither process can guarantee its view of that memory is consistent.
Say the diner decides to fix the pie problem with a physical semaphore. They place a plastic Leprechaun on the pie case. Before any waiter can sell a pie, they must be holding the Leprechaun. Once their order is completed (delivering pie to the table), they return the Leprechaun to guard the treasure of the pies, ready to mediate the next order.
The Lesson: Shared state is incorrect state. Concurrent access to mutable shared state is a primary source of bugs. Ensure updates to shared resources are atomic, or ideally, eliminate shared mutable state by using semaphores, transactional resources, or alternative concurrency models.
Joe Armstrong, creator of the Erlang programming language, perfectly captured the inheritance problem: "You wanted a banana but what you got was a gorilla holding the banana and the entire jungle."
This illustrates the problems of using inheritance in object-oriented programming, where inheriting from a class (the "banana") brings along unwanted dependencies and complexity (the "gorilla" and "entire jungle"). Not only is the child class coupled to the parent, the parent's parent, and so on, but code using the child is also coupled to all ancestors.
The Lesson: Delegate to services—has-a trumps is-a. Use mixins to share functionality. Inheritance creates tight coupling and makes code harder to change. Prefer composition and delegation over inheritance for sharing code.
Imagine programming a robotic piña colada maker. You're told the steps are:
However, a bartender would lose their job if they followed these steps one by one, in order. Even though described serially, many could be performed in parallel. The top-level tasks (1, 2, 4, 10, and 11) can all happen concurrently up front. Tasks 3, 5, and 6 can happen in parallel later. If you were in a piña colada-making contest, these optimizations might make all the difference.
The Lesson: Analyze workflow to improve concurrency. Identify activities that can be performed in parallel rather than sequentially. Breaking temporal coupling allows for more flexible, responsive, and potentially faster systems.
Consider how detectives might use a blackboard to coordinate and solve a murder investigation. The chief inspector sets up a large blackboard in the conference room. On it, she writes a single question: "H. Dumpty (Male, Egg): Accident? Murder?"
Did Humpty really fall, or was he pushed? Each detective contributes to this potential murder mystery by adding facts, witness statements, forensic evidence, and so on. As data accumulates, a detective might notice a connection and post that observation or speculation as well. This process continues across all shifts, with many different people and agents, until the case is closed.
Key features of the blackboard approach:
The Lesson: Use blackboards to coordinate workflow. This pattern facilitates laissez-faire concurrency where independent processes or agents share information indirectly. Blackboards (or modern messaging systems with persistence and pattern matching) offer significant decoupling, making systems more flexible and allowing components to be added or replaced dynamically.
It's late at night, dark, pouring rain. The two-seater whips around tight curves of twisty little mountain roads, barely holding the corners. A hairpin comes up and the car misses it, crashing through the skimpy guardrail and soaring to a fiery crash in the valley below. State troopers arrive.
In software development, our "headlights" are similarly limited. We can't see too far ahead into the future, and the further off-axis you look, the darker it gets. So Pragmatic Programmers have a firm rule: Take small steps—always.
Always take small, deliberate steps, checking for feedback and adjusting before proceeding. Consider that the rate of feedback is your speed limit. You never take on a step or task that's "too big."
What's a task that's too big? Any task requiring "fortune telling." Just as car headlights have limited throw, we can only see into the future perhaps one or two steps, maybe a few hours or days at most. Beyond that, you quickly get past educated guess into wild speculation. You might find yourself slipping into fortune telling when you have to:
The Lesson: Take small steps—always. Work in small, deliberate steps, constantly seeking feedback and adjusting your course. Avoid fortune-telling by making long-term predictions for an uncertain future. Instead, design for replaceability, allowing for easy changes as new information becomes available.
Do you ever watch old black-and-white war movies? The weary soldier advances cautiously out of the brush. There's a clearing ahead: are there any land mines, or is it safe to cross? There aren't any indications it's a minefield—no signs, barbed wire, or craters. The soldier pokes the ground ahead with his bayonet and winces, expecting an explosion. There isn't one. So he proceeds painstakingly through the field for a while, prodding and poking as he goes. Eventually, convinced the field is safe, he straightens up and marches proudly forward, only to be blown to pieces.
The soldier's initial probes for mines revealed nothing, but this was merely lucky. He was led to a false conclusion—with disastrous results.
As developers, we also work in minefields. There are hundreds of traps waiting to catch us each day. Remembering the soldier's tale, we should be wary of drawing false conclusions.
The Lesson: Don't program by coincidence. We should avoid programming by coincidence—relying on luck and accidental successes—in favor of programming deliberately. Never rely on accidental successes or undocumented behavior. Always understand why your code works. If you don't understand the underlying principles, you won't know why it fails, and you'll be vulnerable to changes in context or environment.
As a program evolves, it becomes necessary to rethink earlier decisions and rework portions of code. This process is perfectly natural. Code needs to evolve; it's not a static thing.
Unfortunately, the most common metaphor for software development is building construction. Using construction as the guiding metaphor implies these steps:
Well, software doesn't quite work that way. Rather than construction, software is more like gardening—it is more organic than concrete. You plant many things in a garden according to an initial plan and conditions. Some thrive, others are destined to end up as compost. You may move plantings.
The gardening metaphor is much closer to the realities of software development. Perhaps a certain routine has grown too large, or is trying to accomplish too much—it needs to be split into two. Things that don't work out as planned need to be weeded or pruned.
The Lesson: Refactor early, refactor often. Software development is like gardening, an organic process of continuous care and adjustment. A gardener constantly tends to plants, pruning, relocating, and nourishing, just as a programmer should continuously refine and improve code.
Refactoring your code—moving functionality around and updating earlier decisions—is really an exercise in pain management. Let's face it, changing source code can be pretty painful: it was working, maybe it's better to leave well enough alone. Many developers are reluctant to go in and re-open a piece of code just because it isn't quite right.
You might want to explain this principle using a medical analogy: think of code that needs refactoring as "a growth." Removing it requires invasive surgery. You can go in now and take it out while it is still small. Or, you could wait while it grows and spreads—but removing it then will be both more expensive and more dangerous. Wait even longer, and you may lose the patient entirely.
The Lesson: Refactor early, refactor often. Address code problems when they are small and manageable. Procrastinating refactoring leads to accumulated "technical debt" and makes future fixes much more difficult and costly.
Think of our set of test suites as an elaborate security system, designed to sound the alarm when a bug shows up. How better to test a security system than to try to break in?
After you have written a test to detect a particular bug, cause the bug deliberately and make sure the test complains. This ensures that the test will catch the bug if it happens for real.
If you are really serious about testing, take a separate branch of the source tree, introduce bugs on purpose, and verify that the tests will catch them. At a higher level, you can use something like Netflix's Chaos Monkey to disrupt (i.e., "kill") services and test your application's resilience.
The Lesson: Use saboteurs to test your testing. Don't just rely on tests passing; actively challenge them by introducing known faults. This ensures that your tests are robust and will reliably detect issues if they occur in the future.
There's science behind the idea that names are deeply meaningful. The brain can read and understand words really fast: faster than many other activities. This means words have certain priority when we try to make sense of something. This can be demonstrated using the Stroop effect.
Look at a panel with a list of color names or shades, each shown in a color or shade. But the names and colors don't necessarily match. Say aloud the name of each color as written: "BLUE" written in red ink, "GREEN" in blue ink, etc. When naming things, you're constantly looking for ways of clarifying what you mean, and that act of clarification will lead you to better understanding of your code as you write it.
The Lesson: Names are very, very important, because they reveal a lot about your intent and belief. Given the brain's strong response to words, choose names for variables, functions, modules, and systems carefully to clearly and accurately convey their role and purpose. Meaningless or misleading names can cause significant cognitive friction and confusion for anyone reading the code.
Let's take a simple example. You work for a publisher of paper and electronic books. You're given a new requirement: "Shipping should be free on all orders costing $50 or more."
Stop for a second and imagine yourself in that position. What's the first thing that comes to mind? The chances are very good that you had questions:
That's what we do. When given something that seems simple, we annoy people by looking for edge cases and asking about them.
The chances are the client will have already thought of some of these and just assumed the implementation would work that way. Asking the question just flushes that information out.
But other questions will likely be things the client hadn't previously considered. That's where things get interesting, and where a good developer learns to be diplomatic.
You: We were wondering about the $50 total. Does that include what we'd normally charge for shipping?
Client: Of course. It's the total they'd pay us.
You: That's nice and simple for our customers to understand: I can see the attraction. But I can see some less scrupulous customers trying to game that system.
Client: How so?
You: Well, let's say they buy a book for $25, and then select overnight shipping, the most expensive option. That'll likely be about $30, making the whole order $55. We'd then make the shipping free, and they'd get overnight shipping on a $25 book for just $25.
(At this point the experienced developer stops. Deliver facts, and let the client make the decisions.)
Client: Ouch. That certainly wasn't what I intended; we'd lose money on those orders. What are the options?
And this starts an exploration. Your role in this is to interpret what the client says and to feed back to them the implications. This is both an intellectual process and a creative one: you're thinking on your feet and you're contributing to a solution that is likely to be better than one that either you or the client would have produced alone.
The Lesson: Requirements are learned in a feedback loop. Initial requirements are often incomplete or based on assumptions. Your role is to help clients understand the implications of their stated needs by providing feedback, often through mockups, prototypes, or direct conversations. This iterative process refines understanding and leads to better solutions.
Gordius, the King of Phrygia, once tied a knot that no one could untie. It was said that whoever solved the riddle of the Gordian Knot would rule all of Asia. So along comes Alexander the Great, who chops the knot to bits with his sword. Just a little different interpretation of the requirements, that's all... And he did end up ruling most of Asia.
Every now and again, you will find yourself embroiled in the middle of a project when a really tough puzzle comes up: some piece of engineering that you just can't get a handle on, or perhaps some bit of code that is turning out to be much harder to write than you thought. Maybe it looks impossible. But is it really as hard as it seems?
Consider real-world puzzles—those devious little bits of wood, wrought iron, or plastic that seem to turn up as Christmas presents or at garage sales. All you have to do is remove the ring, or fit the T-shaped pieces in the box, or whatever.
So you pull on the ring, or try to put the Ts in the box, and quickly discover that the obvious solutions just don't work. The puzzle can't be solved that way. But even though it's obvious, that doesn't stop people from trying the same thing—over and over—thinking there must be a way.
Of course, there isn't. The solution lies elsewhere. The secret to solving the puzzle is to identify the real (not imagined) constraints, and find a solution therein. Some constraints are absolute; others are merely preconceived notions. Absolute constraints must be honored, however distasteful or stupid they may appear to be.
On the other hand, as Alexander proved, some apparent constraints may not be real constraints at all. Many software problems can be just as sneaky.
The Lesson: Don't think outside the box—find the box. When confronted with an intractable problem, identify the real constraints, which may be far broader or different than initially perceived. Challenge preconceived notions, enumerate all possible solutions (no matter how absurd they seem), and then rigorously prove why certain paths are truly impossible. Often, the solution lies within an unexamined "larger box" of possibilities.
You were probably once given problems such as "If it takes 4 workers 6 hours to dig a ditch, how long would it take 8 workers?" In real life, however, what factors affect the answer if the workers were writing code instead? In how many scenarios is the time actually reduced?
This traditional management puzzle implies a simple linear relationship, but in software, adding more workers to a task (especially a late one) doesn't always proportionally reduce time; it can even make it longer due to increased communication overhead.
The Lesson: Team productivity is not simply a matter of adding more people. Many factors beyond head count affect how quickly a software project can be completed, including communication pathways, individual skill sets, and the nature of the tasks.
The native islanders had never seen an airplane before, or met people such as these strangers. In return for use of their land, the strangers provided mechanical birds that flew in and out all day long on a "runway," bringing incredible material wealth to their island home. The strangers mentioned something about war and fighting. One day it was over and they all left, taking their strange riches with them.
The islanders had seen the external form of what the strangers had done. So, in hopes of bringing back the "cargo" they had lost, the islanders built facsimiles of the runways and control towers out of local materials, and they lined up to watch for the planes to return. They imitated the form, but not the content.
In the software world, we often fall into the same trap. People read a book, attend a conference, or watch a video about some hot new methodology, and immediately try to copy it, hoping to get some of the magic for themselves.
For example, we have personally seen teams that claim to be using Scrum. But, upon closer examination, it turned out they were doing a daily stand up meeting once a week, with four-week iterations that often turned into six- or eight-week iterations. They felt that this was okay because they were using a popular "agile" scheduling tool. They were only investing in the superficial artifacts—and even then, often in name only, as if "stand up" or "iteration" were some sort of incantation for the superstitious. Unsurprisingly, they, too, failed to attract the real magic.
The Lesson: Do what works, not what's fashionable. Don't blindly imitate the practices or methodologies of other successful companies (like Spotify or Netflix) without considering your unique context, constraints, and culture. Instead, experiment with different approaches, keep what genuinely works for your team, and discard anything that becomes mere "waste or overhead."
These stories remain relevant because they address timeless aspects of software development: human psychology, system complexity, and organizational dynamics. While technologies evolve rapidly, these fundamental challenges persist.
Consider how you might apply these insights:
"The Pragmatic Programmer" endures because its stories capture universal truths about software development. These narratives stick with us because they connect abstract principles to concrete, memorable scenarios.
The broken window reminds us that small compromises compound into big problems. The stone soup shows us how to build momentum for change. The helicopter's coupled controls teach us about system design. Each story provides not just instruction, but wisdom earned through experience.
As software engineers, we're not just writing code—we're solving human problems with digital tools, working within organizational constraints, and managing complexity that grows beyond individual comprehension. These stories guide us in navigating those challenges with pragmatic wisdom.
The next time you face a difficult decision in your development work, remember these stories. They might just provide the perspective you need to find an elegant solution.
What story from "The Pragmatic Programmer" has most influenced your development approach? Share your experiences and insights with the community.