Code was never the easy part: why programming craftsmanship still matters in the AI age
Hacker News erupted around a blog post calling 'code was never the hard part' an insult to programmers. Why code quality, type safety and architectural thinking matter more than ever, with concrete C# examples.
Jean-Pierre Broeders
Freelance .NET Developer
There's a post near the top of Hacker News this week with a title that hits exactly the right nerve: "'Code was never the hard part' is an insult to all programmers." 885 points, 540 comments. It touched a reflex that has been hurting more and more over the past few years.
We've all heard the claim. It shows up in keynotes, in LinkedIn posts by people who call themselves "product thinkers," and lately especially in AI conversations: coding is the easy part of software development. The real challenge is understanding the customer, asking the right question, the strategy. Code is merely execution.
That's wrong. And the way it's wrong gets more interesting as AI tooling gets better.
What the claim actually says
The author of the post is clear: if coding were truly easy, programmers wouldn't command high salaries, burnout wouldn't be an occupational disease, and books like Clean Code and SICP wouldn't still be relevant forty years later. That's correct.
There's a second problem in the claim. It sets up a false dichotomy: either code is the hard part, or understanding customer needs is the hard part. A good engineer does both. The claim creates a ranking that doesn't exist in the work itself, but is very convenient for those who want to pay programmers to execute without thinking.
What actually makes code hard
A .NET codebase of any size contains hundreds of places where complexity creeps in. Type safety is one example. C# gives you nullability annotations, records and discriminated unions through pattern matching. If you ignore them, your code buys fifteen future NullReferenceExceptions and a product that says it "just works" until an edge case brings it down.
// Fragile version - null leaks through
public Order? GetOrder(string id) => _db.Orders.Find(id);
// But the caller doesn't check:
var order = repo.GetOrder(customerId);
Console.WriteLine(order.Status); // on a Monday, at a client's site
With a Result<T> or OneOf type you force the caller to think about the failure path:
public Result<Order, NotFound> GetOrder(string id)
{
var order = _db.Orders.Find(id);
return order is null ? new NotFound(id) : order;
}
// Now the happy path doesn't compile until you handle the failure path.
var result = repo.GetOrder(customerId);
if (result.TryPickT1(out var notFound, out var order))
return Results.NotFound(notFound.Id);
Console.WriteLine(order.Status); // safe
This isn't syntactic sugar. This is the compiler acting as a review partner, forcing you to protect the user. That thinking takes time and expertise. It's hard.
The myth that prompt engineering replaces programming
The current version of the claim goes: AI writes the code, you only need to know what you want. Two years of experience says otherwise. AI-generated code is on average syntactically correct and semantically plausible at first glance. But it systematically introduces three kinds of problems.
Take silent race conditions. An AI-generated async method in .NET doesn't always handle HttpClient or DbContext lifetime correctly. Services get registered as Singleton when they should be Scoped. The test passes because it never sends two requests concurrently.
Then there's missing cancellation. AI forgets to thread CancellationToken through the call stack, consistently. In an Azure Function or ASP.NET controller, that gives you a handler that doesn't stop when the client is long gone, holding resources and inflating your bill.
And then wrong invariants. An AI-generated Money class uses decimal instead of long cents. That works fine until you add two amounts and get a floating-point rounding error in an invoice.
Each of these bugs is invisible to someone who doesn't know what's hard about concurrent code, async lifecycle management or financial arithmetic. Learning to prompt well is not a substitute for knowing what you're checking.
The false distinction between technique and understanding
The core of the claim is that execution quality is separate from thinking quality. But the architectural choice is the understanding. If I build a webhook receiver and verify the signature over the deserialized payload instead of the raw bytes, that's a failure to understand what an HMAC signature guarantees. It's simultaneously a code choice and a problem-understanding choice. Those two aren't separable.
In .NET you see this constantly. EF Core queries that cause N+1 aren't carelessness; they're a sign that the engineer doesn't understand how IQueryable versus IEnumerable works in the context of lazy loading. A misconfigured Polly retry policy that synchronizes its retries without jitter and takes down a downstream service: not bad code in the narrow sense, but a misunderstanding of the system the code describes.
// N+1: each order loads its customer with a separate query
var orders = await _db.Orders.ToListAsync();
foreach (var order in orders)
Console.WriteLine(order.Customer.Name); // lazy load per row
// Correct: one query with Include
var orders = await _db.Orders
.Include(o => o.Customer)
.ToListAsync();
Anyone who says code is the easy part has simply never fully paid the price of bad code. Or they were the manager, not the engineer who worked the weekend finding the N+1 query in a production dashboard that had gone from 2 to 45 seconds.
What AI actually changes
AI makes boilerplate faster. Scaffolding a controller, setting up a migration, writing the first pass of a unit test: that goes quicker. That's real and valuable. But it shifts the difficulty; it doesn't remove it.
When boilerplate gets faster, the value of the engineer who understands what sits above that boilerplate goes up. Architectural choices, performance trade-offs, consistency guarantees, error strategy, testability. These are the pieces that aren't in the first code generation, that AI systematically handles poorly without guidance, and that require someone who knows what's hard about them.
A senior .NET engineer using AI tooling delivers more than ever. A junior using AI as a substitute for understanding ships more bugs than before the AI era. The bugs are now more generic and harder to debug.
Types as documentation of intent
There's a pattern in .NET I've been leaning on more and more, precisely because AI tools consistently miss it: expressing domain intent through the type system. Take a simple OrderStatus. If you pass it around as a string, AI can endlessly generate variants where "declined" and "rejected" and "REJECTED" get mixed up. A discriminated union closes that off.
// String: anything goes, nothing is guaranteed
public void Process(string status) { /* what's valid here? */ }
// Type: the compiler knows, and the documentation writes itself
public abstract record OrderStatus
{
public record Pending : OrderStatus;
public record Approved(DateTimeOffset At) : OrderStatus;
public record Rejected(string Reason) : OrderStatus;
}
public decimal CalculateDiscount(OrderStatus status) => status switch
{
OrderStatus.Approved { At: var at } when at < DateTimeOffset.UtcNow.AddDays(-30)
=> 0.05m,
OrderStatus.Approved => 0m,
OrderStatus.Pending => 0m,
OrderStatus.Rejected => throw new InvalidOperationException("No discount on rejected order"),
_ => throw new UnreachableException()
};
What you get out of this: the compiler warns you when you add a new status but forget to update the switch. AI-generated code that writes status == "approved" no longer compiles. And the next developer reading the code sees exactly which states exist and which data is available per state.
This is work AI doesn't take off your hands. AI happily generates a class with properties. The decision about which properties to have, which states are valid, and which transitions the compiler guards for you: that's engineering judgment. And it's exactly the kind of judgment that makes better software.
What this means for you as a .NET developer
Stop apologizing for the complexity of your work. Thread CancellationToken all the way down, always. Make null a compile-time concern, solve it before it becomes a production crash. Give failure paths explicit types. Write tests that describe your domain's invariants.
These are choices AI won't make for you. This is the expertise that has value. The claim that coding is the easy part is therefore insulting and a dangerous oversimplification that stands in the way of better software.
Frequently asked questions about code quality and AI in .NET
Is programming expertise becoming less valuable now that AI generates code? No, the value shifts to the layers above the boilerplate. AI accelerates scaffolding code, but insight into concurrency, error strategy, domain invariants and system architecture becomes scarcer and more valuable as more code gets produced that nobody fully understands.
When do you use Result<T> instead of an exception in C#?
Use Result<T> or a discriminated-union type for expected failure paths like "not found", "validation failed" or "quota exceeded." Reserve exceptions for unexpected situations like a DB connection failure or a bug. That way the compiler forces the caller to handle the failure path and it doesn't quietly leak into an unexpected NullReferenceException.
How do I detect N+1 queries in EF Core in production?
Enable Application Insights or OpenTelemetry with a SQL sampler and set thresholds for queries that take too long or fire too often per request. In development, use LogTo(Console.WriteLine, LogLevel.Information) on DbContext options to make every generated SQL visible and write integration tests with Testcontainers so the pattern shows up in the pipeline before it reaches production.
Should I always review AI-generated code? Yes, and specifically for the patterns AI systematically misses: CancellationToken propagation, Scoped-vs-Singleton lifetime in DI, idempotency of write operations, and floating-point errors with monetary amounts. A quick mental checklist on these four points catches most of the creeping bugs before they hit production.
Further reading
- Clean Code Isn't Dead: What a New Study Says About Coding Agents and Token Bills
- Context Rot: Why a Bigger Context Window Won't Save Your LLM Feature
- When an AI Agent Broke Into Hugging Face: Machine-Speed Attacks and Your .NET Infra
Conclusion: The claim that coding is the easy part conceals a dangerous misunderstanding of how software complexity works. In the AI era, boilerplate gets faster; the value of expertise in architecture, type safety and domain understanding increases. Write the code the compiler lets you write, not the code that just compiles.
Sources: "Code was never the hard part" is an insult to all programmers · Hacker News discussion.
