May 25, 2025
Read Time: 8 minutes

Medium: https://medium.com/@cangokdere/pragmatic-programmer-lessons-in-the-age-of-ai-614d2a144c78

An AI Generated Image depicting a software developer with book Pragmatic Programmer

Pragmatic Programmer Lessons in the Age of AI

I recently finished reading the second edition of The Pragmatic Programmer a classic book on software design, architecture, and craftsmanship. While the book was written long before AI became widespread, it struck me how incredibly relevant its lessons remain, especially now, in a world where AI is rapidly transforming how we write software.

When I first picked up the book, I expected timeless wisdom on topics like design, architecture, and craftsmanship. What I didn’t expect was how essential these principles feel today, in the age of AI-assisted development.

Sure, AI can write code faster than ever. It can scaffold entire classes, suggest patterns, and even generate tests. But here's the thing: AI is trained on the same codebases, problems, and practices that led experts like Hunt and Thomas to write The Pragmatic Programmer in the first place.

In other words, AI can help you go fast with the caveat that it’s up to you to build well.

Here are a few key takeways for me from the book that feel even more important today, as AI becomes a regular part of our development workflows:

Assertive Programming: Define Contracts

Assertive programming is about writing code that clearly defines its expectations. Instead of silently handling invalid input or edge cases deep within the system, you make the preconditions and postconditions explicit. Documenting assumptions through assertions, contracts, or type guarantees.

How many times we have spent hours debugging issues where we were following the breadcrumbs, constantly telling ourselves: "This should not happen" and eventually revealing the root cause of problem was at least six jumps away where an invalid state made its way deeper and deeper to the system ? Failing fast not only prevents such cases but also increases code readability + traceability.

For each code piece you are writing, by making assumptions explicit, you reduce the likelihood of subtle bugs that can propagate into production, especially in complex systems with many integration points.

Now, consider AI assisted code generation. AI often lacks the business context to define these assumptions correctly. It may generate functions that seem valid but omit crucial precondition checks. Without assertive programming, AI generated code can introduce silent failures that only surface much later. AI can assist in coding, but it’s up to developers to define the contracts that keep systems safe and predictable.

DRY: Refactoring Beyond Duplication

From my experience, as well as from authors' examples, I can easily observe DRY is one of the most misunderstood principle in the codebases. DRY is not about blindly preventing every duplicated line in code. It is about de-duplication of knowledge, which goes beyond just code. Every documentation, wiki, runbook, database schema is in the scope of DRY principle. For example,

// Our api allows at most 30 characters for username
const isValidUsername = (username: string) => {
    return username && username.length > config.MAX_USERNAME_LENGTH;
}

Is a violation of DRY. Next time someone needs to update the code for this limit, they need to update both code/config as well as comment surrounding this function.

The other mistake I see is that, not every duplication is a duplication of knowledge or violation of DRY. Same code may exist under different domains where this duplication is acceptable and necessary.

To give an example, assume in your system you have password validation and at another part you have post condition check for client secret generation:

function validatePassword(password: string): boolean {
  const hasMinLength = password.length >= 15;
  const hasUpper = /[A-Z]/.test(password);
  const hasLower = /[a-z]/.test(password);
  const hasNumber = /[0-9]/.test(password);
  const hasSymbol = /[!@#$%^&*()_\-+={}[\]:;"'<>,.?/\\|`~]/.test(password);

  return hasMinLength && hasUpper && hasLower && hasNumber && hasSymbol;
}
...
function isClientSecretValid(password: string): boolean {
  const hasMinLength = password.length >= 8;
  const hasUpper = /[A-Z]/.test(password);
  const hasLower = /[a-z]/.test(password);
  const hasNumber = /[0-9]/.test(password);
  const hasSymbol = /[!@#$%^&*()_\-+={}[\]:;"'<>,.?/\\|`~]/.test(password);

  return hasMinLength && hasUpper && hasLower && hasNumber && hasSymbol;
}

First instinct for some developers here will be to extract the shared logic to a third "util" function and re-use it what I call "lazy-dry".

Similarly, if we use an AI agent to modify this code, first thing it does the same de-duplication:

function hasUpperCase(str: string): boolean {
  return /[A-Z]/.test(str);
}

function hasLowerCase(str: string): boolean {
  return /[a-z]/.test(str);
}

function hasSymbol(str: string): boolean {
  return /[!@#$%^&*()_\-+={}[\]:;"'<>,.?/\\|`~]/.test(str);
}

function validatePasswordStrength(password: string, minLength: number): boolean {
  return (
    password.length >= minLength &&
    hasUpperCase(password) &&
    hasLowerCase(password) &&
    hasSymbol(password)
  );
}

function validatePassword(password: string): boolean {
  return validatePasswordStrength(password, 15);
}

function isClientSecretValid(password: string): boolean {
  return validatePasswordStrength(password, 8);
}

Doing that now couples these two completely unrelated function as well as opens possibilities for future bugs when a new requirement for either of these domains.

With AI generated code, it falls to us to DRY principles are properly applied. When modules and domains are different, lots of boilerplate code will be generated by AI. Thus it could be beneficial that AI will eliminate the "lazy-dry". At the same time, when everyone in team uses AI for their work, there is a good chance that there will be code duplication, which should be closely monitored in review time. Teams may even use an AI agent to help them de-dup during code reviews.

Tracer Bullet Development: Rapid Feedback for the Right Path

Tracer Bullet Development is about building a thin, functional slice of your system that touches all layers, frontend, backend, database, and integrations, without perfecting every detail. The goal is to validate assumptions early, discover technical risks, and expose integration points before you invest heavily in a design that might not work.

When I was reading the book, I saw this section as a perfect example which AI can boost very significantly. AI excels at generating prototypes like tracer bullets. It can scaffold services, APIs, models, and tests in minutes.

Therefore, more and more developers should embrace the approach of starting their project with a proof of concept and proceed in small increments rather than all in before any decision they make becomes irreversible. AI can help you build quickly, but validating direction and adjusting course as you learn is a human skill.

Automated Testing: Challenging Assumptions, Not Just Checking Boxes

Testing is about more than just writing tests; it’s about challenging assumptions and verifying that your system behaves correctly under a variety of conditions. Good testing includes edge cases, error handling, performance scenarios, and integration points.

For example, in a financial application, you would test for currency rounding errors, timezone discrepancies, and race conditions in concurrent updates. A naive AI-generated test suite might cover happy paths but miss these crucial scenarios that can lead to severe bugs.

AI can help scaffold tests, but it cannot replace the strategic thinking behind what to test and why. Deciding which scenarios are mission critical, which areas need fuzzing or property based testing, and how to simulate realistic failures is where your experience and judgment are essential. This also highlights another discussion point where quantity of tests vs quality of tests. You can use AI to generate 100% code coverage tests, but this can be misleading. Code coverage only indicates which lines of code were executed during a test, not whether they were tested with meaningful inputs or verified for correctness. Consider a module that processes user uploads: the code might be covered, but if no tests simulate large file sizes, invalid encodings, or concurrent requests, critical issues can remain undetected until there is a production impacting issue.

I want to highlight this quote from the book:

"We believe that the major benefits of testing happen when you think about and write the tests, not when you run them" — Andrew Hunt and David Thomas, The Pragmatic Programmer (2nd Edition)

Master Your Tools

Mastery of your development tools i.e. IDEs, version control systems, build pipelines, debugging techniques, is a cornerstone of productive software engineering.

While The Pragmatic Programmer predates the spike in AI usage that is recent, I’d like to extend this principle to AI agents themselves.

Invest time in configuring your IDE + AI integration for maximum productivity. This includes figuring out how to feed information to the AI agent to make its suggestions more relevant. With the emergence of the MCP Protocol, there are growing numbers of MCP servers that can enhance your LLM models. Explore and integrate the ones that suit your workflow.

For teams, I’d recommend versioning prompts within your repository so everyone can contribute and refine them for your specific context. This also promotes consistency in AI-generated outputs.

Why Reading Books Matters More Than Ever

Here’s the paradox: AI is trained on the collective output of decades of software engineering, including the insights that led to books like The Pragmatic Programmer. These books distilled the hard-won lessons of thousands of developers into timeless principles.

In the AI era, books remain vital because they teach the why behind the code. AI accelerates execution, but it does not reason, reflect, or design at a system level. As AI gets better at writing code, your role is to understand architecture, trade-offs, and the business context, skills honed through study, reflection, and learning from the masters.

Having said this, I strongly recommend reading The Pragmatic Programmer. The principles and key learnings from the book are even more important today and they are still relevant school you to become a pragmatic programmer.

Integrating these learnings to your work life will help you utilize AI effectively, without surrendering your responsibility as a software craftsman.

AI can code, but it is still up to us to build systems that are robust, secure, and well-architected. The future belongs to those who can combine AI’s speed with principled, thoughtful engineering.