Confessions of a Stubborn Engineer: Why I Finally Stopped Writing Code by Hand

Confessions of a Stubborn Engineer: Why I Finally Stopped Writing Code by Hand

2026.09.07
~12 min read
Career AI WebArchitecture Drupal Productivity
Share•with caption

Confessions of a Stubborn Developer

For fifteen years, my professional identity was forged in the rhythm of a mechanical keyboard.

I took immense, almost defensive pride in my speed. I knew the exact array structure of Drupal render arrays without opening the documentation. I could write a complex multi-join entity query in my sleep. I remembered which Symfony events fired in what order during the Drupal kernel request lifecycle, and I could spot an unescaped SQL parameter across a dimly lit room.

To me, writing code by hand was not just a job; it was an artisan craft. It was woodworking. It was chiseling stone.

When the first generation of AI coding assistants showed up, my reaction was immediate, visceral, and dismissive:

“Cute party trick. It writes generic JavaScript calculators and buggy regex. Let me see it handle an enterprise Drupal migration with circular entity references, multilingual node translations, and custom access control handlers. Real engineers write their own code.”

For nearly three years, I wore that skepticism like a badge of honor. When younger developers on our team started singing the praises of AI assistants in our daily standups, I smiled politely with the quiet condescension of someone who believed they possessed the “real” secret knowledge. I told myself that relying on an LLM was a shortcut that rotted your fundamental skills.

Then came a Tuesday afternoon in early 2026 that completely shattered my ego.


The Tuesday Afternoon That Broke My Pride

It was 3:30 PM. I was on a tight enterprise deadline to migrate 85,000 legacy records from a horrifying, undocumented proprietary MySQL database into Drupal 11.

The source schema was an architectural crime scene: comma-separated values stored inside text fields, timestamps stored as formatted French strings with inconsistent capitalization, and nested parent-child taxonomies stored in three separate un-indexed junction tables.

I rolled up my sleeves, brewed my third espresso of the day, and prepared for battle. For three straight hours, I hunched over my monitor, meticulously handwriting a custom Migrate process plugin.

Line 42: array_map callback.
Line 78: DateTime::createFromFormat with three fallback try-catch blocks.
Line 124: Custom recursive taxonomy lookup to prevent duplicate term creation.

By 6:45 PM, my neck was stiff, my eyes were burning, and my brain was running on empty. I finally ran the migration test on a sample batch of 100 rows:

[error] TypeError: Cannot assign null to property Drupal\custom_migrate\Plugin\migrate\process\NormalizeFrenchDates::$timezone of type string
[error] Integrity constraint violation: 1062 Duplicate entry '1402-0' for key 'taxonomy_term_field_data.PRIMARY'

I groaned aloud into my empty home office. I was looking at another two hours of stepping through Xdebug breakpoints just to fix subtle array index typos and edge-case null references.

Sitting on my second monitor was an AI agent interface that a colleague had configured in our shared repository. Out of sheer, desperate exhaustion—the kind of tiredness where you just want to shut your laptop and walk into the sea—I copied the raw SQL schema dump, pasted three problematic sample rows, and typed:

“Write a complete Drupal 11 custom migrate process plugin in PHP 8.4 that normalizes these French date strings into UTC ISO-8601 timestamps, handles nulls gracefully, deduplicates the taxonomy hierarchy, and write a full Pest/PHPUnit test suite covering leap years and malformed inputs.”

I pressed Enter. I picked up my mug to take a sip of cold coffee.

Before the mug touched my lips—literally twenty-four seconds later—the screen finished generating.

I leaned in, squinting, waiting to pounce on its inevitable stupid mistakes. Where did it hallucinate the core API? Where did it use an outdated Drupal 8 method?

I scrolled.

// It used PHP 8.4 match expressions.
// It implemented modern Drupal 11 attribute-based plugin discovery.
// It cached taxonomy term lookups in a static runtime cache to prevent N+1 queries.
// It handled non-breaking spaces in the French month names.
// It wrote 14 individual unit tests with edge-case data providers.

I copied the plugin into my module. I ran the test suite:

PASS  Tests\Unit\Plugin\migrate\process\NormalizeFrenchDatesTest
✓ it normalizes standard french dates
✓ it handles irregular non breaking spaces
✓ it falls back safely on corrupted timestamps
✓ it prevents duplicate taxonomy term insertion under high concurrency

Tests:  14 passed (32 assertions)
Duration: 0.18s

I ran the migration against the test dataset. 100 rows processed in 0.4 seconds. Zero errors.

I sat back in my chair. The room was completely silent except for the hum of my desk fan.

A heavy, uncomfortable sensation settled in my chest. It was the realization that I had just spent three grueling, stress-filled hours doing something that an algorithm completed with higher precision in twenty-four seconds.

I was not being a noble craftsman. I was not preserving the sanctity of computer science. I was just digging a ditch with a silver spoon when an excavator was parked behind me with the engine running.

That was the day I surrendered.


The 5 Stages of Senior Developer Grief

Talking to other senior engineers and engineering managers over the past few months, I realized my stubbornness was not unique. We all go through the exact same psychological five stages of grief when confronting AI:

The 5 Stages of Developer Evolution

Look at the progression mapped out in the visual journey above. How many of these sound painfully familiar?

StageThe Internal MonologueWhat Is Actually Happening Under the Hood
1. Denial”It is just advanced autocomplete. It only knows how to write generic to-do lists in React.”Fear of irrelevance disguised as intellectual superiority. Clinging to the illusion that raw syntax memorization equals engineering skill.
2. Nitpicking”Look! It generated a deprecated function from Drupal 9! See? It is dangerous and unusable in production.”Actively searching for failure cases to validate your ego. Ignoring the fact that you also make typos and use deprecated methods.
3. The Wall (Boilerplate Burnout)“I have to write 25 nearly identical entity schemas, 12 SDC components, and 40 unit tests by Thursday or we miss launch.”Physical and mental exhaustion. The realization that 70% of daily programming is tedious, repetitive assembly work.
4. The Epiphany”Wait… it just scaffolded a flawless 400-line data transform and 15 edge-case tests in 20 seconds while I blinked.”Humility. The sudden shock that the machine can handle structural execution faster than human fingers can twitch.
5. Transcendence (The Director)“My value was never typing speed. My value is system architecture, security boundaries, and domain judgment.”Liberation. Stepping back from typing every semicolon to orchestrate, review, and guide software systems at 10x leverage.

What AI Actually Does Better Than You (And It Is Okay to Admit It)

Let us be completely honest. Admitting that a machine does certain parts of our job better than we do does not diminish our worth. A mathematician is not humiliated because a pocket calculator multiplies 12-digit numbers faster than their brain. A carpenter is not ashamed of using a power planer instead of smoothing a 50-foot beam with hand sandpaper.

Here is what modern AI coding tools do vastly better than any human developer on their best day:

1. Zero-Boredom Stamina

Humans get bored. When you write your first Drupal Single Directory Component (SDC) of the week, you write clean, beautiful YAML schemas with descriptive labels and detailed examples.

By the fourteenth component on Thursday afternoon, you are exhausted. You start skipping documentation, leaving props untyped, and writing "test description" in placeholder fields just to get the pull request out.

AI does not experience cognitive fatigue. The 50th schema definition it writes has the exact same meticulous JSON schema validation, typed properties, and edge-case null handling as the first.

2. Instant, Exhaustive Test Coverage

Be honest: when was the last time you voluntarily wrote 30 comprehensive unit tests for a utility class before pushing your branch?

Usually, deadline pressure forces a compromise: we write two happy-path tests, manually verify one error case in the browser, and tell ourselves, “I will add more test coverage in a follow-up ticket.” (That ticket remains in the backlog for the next four years until the project is decommissioned).

AI thrives on the combinatorial grind of test generation. It will gladly generate datasets with zero-width characters, null bytes, leap year dates, negative integers, and SQL injection payloads without complaining once.

3. Cross-Domain Syntax Instant Recall

Between PHP 8.4, Twig 3, TypeScript 5.8, Tailwind CSS v4, and Drupal 11’s core API changes, the amount of arbitrary syntax a senior full-stack developer is expected to retain in active memory is absurd.

Why waste mental RAM remembering whether array_search takes the needle or the haystack first, or how to declare an asymmetric visibility property in PHP 8.4 (public private(set)), when an AI agent produces it natively and accurately in milliseconds?


The Paradigm Shift: From Syntax Typist to Software Director

Once you let go of the stubborn need to write every line yourself, your entire relationship with software development transforms.

You stop being a Syntax Typist and become a Software Director.

The Syntax Typist vs The Software Director

Look closely at the split-screen diagram above.

On the left is where I spent the first fifteen years of my career: hunched over monitors, eyes bloodshot, wrestling with missing semicolons, YAML indentation errors, and tracking down array key typos.

On the right is where engineering lives today: standing at the architectural control deck.

Think of a film director. Christopher Nolan does not carry the lighting rigs, sew the period costumes, or operate the camera crane during a stunt scene. He has master craftsmen and specialized tools for that. His job is vision:

  • Does this scene serve the story?
  • Is the pacing right?
  • Are the emotional and structural arcs coherent?
  • Does this shot fit within the broader narrative of the film?

When you work with AI, you are the director. The AI is your virtuoso technical crew. It can build the set and lay the tracks at lightning speed, but it has no idea what story you are trying to tell until you give it vision, architectural guardrails, and quality control.


Real-World Code Walkthrough: Before and After the Surrender

To make this concrete, let us inspect what this shift looks like in actual production code.

Scenario: Building a Modern Drupal 11 Single Directory Component (SDC) with PHP 8.4 Data Transformation

Imagine you need to build an interactive Hero Pricing Calculator component for Drupal 11. It needs:

  1. A strict SDC metadata schema (component.yml).
  2. An accessible, semantic Twig template.
  3. A backend plugin to transform user-selected tier data with business validation rules.
  4. A complete automated test suite.

The Old Way (The 4-Hour Hand-Crafted Grind)

You spent 45 minutes looking up the exact JSON schema specification for SDC props. You spent another hour handwriting Twig filters. You spent two hours debugging why a boolean toggle in YAML was evaluating to a string in PHP. You wrote zero automated tests because it was already 6:00 PM.

The Modern Augmented Way (12 Minutes Total)

You write a concise architectural specification defining the domain rules, prop types, and constraints. You hand the structural generation to the AI.

Here is the code the AI generated, which I reviewed, refined, and deployed in less than fifteen minutes:

1. The Machine-Precision Schema: pricing-card.component.yml

$schema: https://git.drupalcode.org/project/drupal/-/raw/11.x/core/assets/schemas/v1/metadata.schema.json
name: Pricing Tier Card
description: 'Enterprise tier card with billing cycle toggles and feature matrix.'
status: stable
group: Monetization

props:
  type: object
  required:
    - plan_id
    - plan_name
    - monthly_price
    - annual_price
    - cta_url
  properties:
    plan_id:
      type: string
      title: 'Plan Machine Identifier'
      examples: ['enterprise_tier']
    plan_name:
      type: string
      title: 'Display Name'
      examples: ['Enterprise Dedicated']
    is_featured:
      type: boolean
      title: 'Featured Badge'
      default: false
    monthly_price:
      type: integer
      title: 'Monthly Rate (USD)'
      minimum: 0
    annual_price:
      type: integer
      title: 'Annual Rate (USD per month billed yearly)'
      minimum: 0
    features:
      type: array
      title: 'Feature List'
      items:
        type: string
    cta_url:
      type: string
      title: 'Checkout Destination'

slots:
  badge_graphic:
    title: 'Custom Vector Badge'
    description: 'Slot for custom SVG or promotion chip.'

2. The Semantic Twig Template: pricing-card.twig

{#
/**
 * @file
 * pricing-card.twig
 * Fully accessible, zero-hydration pricing tier component.
 */
#}
<div {{ attributes.addClass('pricing-card', is_featured ? 'pricing-card--featured' : '') }} data-plan="{{ plan_id }}">
  {% if is_featured %}
    <div class="pricing-card__highlight-banner" aria-label="{{ 'Most popular plan'|t }}">
      {% if badge_graphic %}
        {{ badge_graphic }}
      {% else %}
        <span class="pricing-card__badge-text">{{ 'Recommended'|t }}</span>
      {% endif %}
    </div>
  {% endif %}

  <div class="pricing-card__header">
    <h3 class="pricing-card__title">{{ plan_name }}</h3>
    <div class="pricing-card__pricing-group">
      <span class="pricing-card__currency">$</span>
      <span class="pricing-card__amount" data-monthly="{{ monthly_price }}" data-annual="{{ annual_price }}">
        {{ monthly_price }}
      </span>
      <span class="pricing-card__period">/{{ 'month'|t }}</span>
    </div>
  </div>

  <ul class="pricing-card__features" role="list">
    {% for feature in features %}
      <li class="pricing-card__feature-item">
        <svg class="pricing-card__check-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
          <path d="M13.3334 4L6.00008 11.3333L2.66675 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
        </svg>
        <span>{{ feature }}</span>
      </li>
    {% endfor %}
  </ul>

  <div class="pricing-card__footer">
    <a href="{{ cta_url }}" class="pricing-card__cta-button">
      {{ 'Select Plan'|t }}
    </a>
  </div>
</div>

3. The Backend Value Object in Modern PHP 8.4: PricingTier.php

Notice how modern and clean this is. Instead of archaic getters and setters, it leverages PHP 8.4 Property Hooks and Asymmetric Visibility to protect invariant business rules:

<?php

declare(strict_types=1);

namespace Drupal\custom_monetization\ValueObject;

/**
 * Immutable pricing tier representation with modern PHP 8.4 property hooks.
 */
final class PricingTier {

  /**
   * Discount percentage calculated automatically when billing annually.
   */
  public int $annualDiscountPercentage {
    get => $this->monthlyPrice > 0 
      ? (int) round((1 - ($this->annualPrice / $this->monthlyPrice)) * 100)
      : 0;
  }

  public function __construct(
    public private(set) string $planId,
    public private(set) string $planName,
    public private(set) int $monthlyPrice,
    public private(set) int $annualPrice,
    public private(set) array $features = [],
    public private(set) bool $isFeatured = false,
  ) {
    if ($this->monthlyPrice < 0 || $this->annualPrice < 0) {
      throw new \InvalidArgumentException('Prices cannot be negative values.');
    }
    if ($this->annualPrice > $this->monthlyPrice) {
      throw new \DomainException('Annual monthly-rate cannot exceed standard monthly rate.');
    }
  }

}

4. The Comprehensive Pest Test Suite: PricingTierTest.php

And look at the automated tests generated right alongside the code:

<?php

declare(strict_types=1);

use Drupal\custom_monetization\ValueObject\PricingTier;

it('calculates the annual discount percentage correctly', function (int $monthly, int $annual, int $expectedDiscount) {
  $tier = new PricingTier(
    planId: 'pro',
    planName: 'Professional',
    monthlyPrice: $monthly,
    annualPrice: $annual,
  );

  expect($tier->annualDiscountPercentage)->toBe($expectedDiscount);
})->with([
  [100, 80, 20],
  [50, 40, 20],
  [200, 150, 25],
  [0, 0, 0],
]);

it('throws an exception when prices are negative', function () {
  new PricingTier(
    planId: 'invalid',
    planName: 'Broken',
    monthlyPrice: -10,
    annualPrice: 50,
  );
})->throws(\InvalidArgumentException::class, 'Prices cannot be negative values.');

it('prevents annual price from exceeding monthly price', function () {
  new PricingTier(
    planId: 'gouging',
    planName: 'Bad Deal',
    monthlyPrice: 50,
    annualPrice: 60,
  );
})->throws(\DomainException::class, 'Annual monthly-rate cannot exceed standard monthly rate.');

Notice the difference in my role as the engineer.

I did not spend thirty minutes typing out public private(set) or setting up data provider arrays in Pest. I spent my time reviewing the domain invariants. I verified that the negative price exception made sense for our finance team. I verified that the discount math held up for zero-dollar free tiers.

I was doing real engineering, not digital clerical work.


The Delegation Matrix: What to Hand to AI vs. Where Humans Must Guard the Gate

Let us make one thing crystal clear: letting AI do what it does best does NOT mean turning your brain off.

If you accept AI-generated code without rigorous review, you are not a Software Director; you are a reckless liability. AI will happily hallucinate database column names, introduce subtle N+1 query loops, or miss critical Drupal entity access checks (->access('view')) unless you enforce strict architectural rules.

Here is the exact delegation matrix I use on my teams today:

Task CategoryDelegate to AI (Full Speed)Human Architect Must Own (Zero Compromise)
System Design & Boundaries❌ Never let AI choose system topology blind✅ Defining service boundaries, caching layers, and database models
Boilerplate & Scaffolding✅ YAML schemas, SDC templates, DTOs, value objects❌ Don’t waste human keystrokes on boilerplate
Security & Permissions❌ Never trust an LLM on authorization checks✅ Auditing RBAC, node access grants, and secret token isolation
Automated Test Generation✅ Generating 25+ edge-case test cases, mocks, and fixtures❌ Reviewing test assertions to ensure they test business reality
Complex Regex & Transforms✅ Date parsing, string normalization, math routines❌ Verifying unicode safety and locale edge cases
Code Review & Mentorship❌ Automated PR comments are not a substitute for care✅ Explaining architectural trade-offs to teammates in human terms
Refactoring Legacy Syntax✅ Converting procedural hooks to PHP 8.4 attributes❌ Sanity-checking performance under heavy concurrency

The Time & Energy Audit: Where Did My 40 Hours Go?

When you stop fighting the tool and start mastering the collaboration, the impact on your weekly life is staggering.

Here is what an actual 40-hour work week looks like for a senior web engineer before and after giving up the keyboard:

Weekly ActivityThe Old Way (2021)The Augmented Way (Late 2026)Real-World Impact
Typing Boilerplate & Setup16 hours / week2 hours / week14 hours saved per week
Hunting Syntax Typos & Mappings6 hours / week1 hour / week5 hours saved
Writing Tests (or feeling guilty about skipping them)3 hours / week4 hours / week (95% coverage)Higher quality with zero guilt
System Architecture & Data Modeling5 hours / week14 hours / weekNearly 3x more focus on real design
Mentoring Junior & Mid-level Peers3 hours / week8 hours / weekMentorship and team leadership expand
Exhaustion & Mental Fog by FridaySevereMinimalYou finish the week with energy to spare

Look at the bottom rows of that table.

Where did those recovered 14 hours go? They did not vanish into thin air. They went into thinking. They went into talking to product managers, understanding what users actually needed, designing cleaner API boundaries, and sitting down with junior developers to explain why a particular design pattern was chosen.

Writing code fast is not productivity. Building the right thing cleanly the first time is productivity.


3 Iron Rules for Coding with AI Without Losing Your Mind (or Skills)

If you are ready to put down your pride and embrace this way of working, here are three non-negotiable rules to live by:

Rule 1: The Verification Imperative

Never commit a single line of code you cannot clearly explain to a teammate in a pull request review.

If the AI generates a clever one-line regex or an intricate bitwise operation, and you do not understand how it works, stop. Do not hit commit. Ask the AI to explain the logic line by line. If you cannot explain why it works and where it fails, rewrite it. You own the code in production, not the model.

Rule 2: Architect First, Prompt Second

If you feed chaos into an AI, it will give you back industrial-grade chaos in five seconds.

AI is an amplifier. If you give it vague requirements, it will generate plausible-looking garbage. Before you touch your AI assistant, spend ten minutes with a pen and paper. Define your data entities, write down your validation rules, sketch your component props, and specify your edge cases. When you feed a structured specification into an AI, the output is pure gold.

Rule 3: Use AI as a Sparring Partner, Not an Oracle

The best developers do not just ask AI for code; they argue with it.

When I am architecting a new system, I often tell the AI: “Here is my proposed caching strategy for this multi-tenant Drupal setup. Tear it apart. What are the three most likely failure modes under high write concurrency?”

Using the model as a relentless, zero-ego architectural reviewer will sharpen your engineering thinking ten times faster than treating it as a code dispenser.


Conclusion: The Craft Did Not Die—It Elevated

If you have spent your career taking pride in your ability to manually type clean code, it is completely natural to feel a pang of grief when watching an algorithm generate your afternoon’s work in twenty seconds.

It feels like something precious was taken away.

But step back and look at the history of our industry. When assemblers replaced punch cards, engineers worried the craft was dead. When C replaced assembly, purists claimed real programmers managed registers by hand. When high-level garbage-collected languages like Python and PHP arrived, people claimed software was doomed. When modern frameworks, package managers, and automated CI/CD took over, the same chorus repeated.

The craft of software engineering was never about typing syntax into a text buffer.

The craft was always about empathy, clarity, and problem-solving. It was about understanding a human business problem, translating it into a resilient architecture, protecting users from security failures, and building something that makes someone’s life easier.

Giving up the keyboard did not make me less of an engineer. It freed me to finally be the architect I always wanted to be.

Put down the silver spoon. The excavator is running. Go build something extraordinary.