Code was nooit het makkelijke deel: waarom programmeervakmanschap telt in het AI-tijdperk
Hacker News explodeerde rondom een blog dat 'code was never the hard part' bestempelt als belediging voor programmeurs. Waarom codekwaliteit, type safety en architectuurdenken er meer toe doen dan ooit, met concrete C#-voorbeelden.
Jean-Pierre Broeders
Freelance .NET Developer
Er staat deze week een artikel hoog op Hacker News met een titel die precies het juiste raak schopt: "'Code was never the hard part' is an insult to all programmers." 885 punten, 540 reacties. Het raakte een zenuwtrekje dat de afgelopen jaren steeds meer pijn is gaan doen.
De claim kennen we allemaal. Hij klinkt in keynotes, in LinkedIn-posts van mensen die zichzelf "product thinker" noemen, en tegenwoordig vooral in de context van AI: coden is het makkelijke stuk van software development. De echte uitdaging is het begrijpen van de klant, het stellen van de juiste vraag, de strategie. Code is alleen de uitvoering.
Hij klopt van geen kant. En de manier waarop hij niet klopt, wordt interessanter naarmate AI-gereedschap beter wordt.
Wat de claim eigenlijk beweert
De auteur van het stuk is helder: als coderen echt makkelijk was, zouden programmeurs geen hoge salarissen verdienen, zou burnout geen beroepsziekte zijn, en zouden boeken als Clean Code en SICP geen veertig jaar oud worden zonder hun belang te verliezen. Dat klopt.
Er zit een tweede probleem in de claim. Ze stelt een valse tegenstelling in: ofwel is code het moeilijke deel, ofwel is het begrijpen van de klantbehoefte het moeilijke deel. Een goede ingenieur doet allebei. De claim creëert een rangorde die niet bestaat in het werk zelf, maar heel handig is voor degenen die programmeurs willen betalen om minder na te denken en alleen maar uit te voeren.
Wat code daadwerkelijk moeilijk maakt
Een .NET-codebase van enige omvang bevat honderden plaatsen waar de complexiteit sluipend toeslaat. Type safety is een voorbeeld. C# geeft je nullability-annotaties, records en discriminated unions via patroonherkenning. Als je ze negeert, koopt je code vijftien toekomstige NullReferenceExceptions en een product dat vertelt dat het "gewoon werkt" tot een edge case hem neerhaalt.
// Fragiele variant - null lekt door
public Order? GetOrder(string id) => _db.Orders.Find(id);
// Maar de aanroeper checkt niet:
var order = repo.GetOrder(customerId);
Console.WriteLine(order.Status); // op een maandag, bij de klant
Met een Result<T> of OneOf-type forceer je de aanroeper na te denken over het faalpad:
public Result<Order, NotFound> GetOrder(string id)
{
var order = _db.Orders.Find(id);
return order is null ? new NotFound(id) : order;
}
// Het happy-path compileert pas nadat je het faalpad hebt afgehandeld.
var result = repo.GetOrder(customerId);
if (result.TryPickT1(out var notFound, out var order))
return Results.NotFound(notFound.Id);
Console.WriteLine(order.Status); // veilig
Dit is geen syntactische suiker. Dit is de compiler als reviewpartner die je dwingt de gebruiker te beschermen. Dat denken kost tijd en expertise. Het is moeilijk.
De mythe dat prompt engineering programmeren vervangt
De meest actuele versie van de claim luidt: AI schrijft de code, jij hoeft alleen nog te weten wat je wilt. De ervaring van de afgelopen twee jaar leert iets anders. AI-gegenereerde code is gemiddeld syntactisch correct en semantisch plausibel op het eerste oog. Maar hij introduceert systematisch drie soorten problemen.
Neem stille race conditions. Een AI-gegenereerde async-methode in .NET pakt de HttpClient of DbContext niet altijd op de juiste manier op. Services worden als Singleton geregistreerd terwijl ze Scoped zouden moeten zijn. De test slaagt omdat hij nooit twee requests tegelijk stuurt.
Dan is er ontbrekende cancellation. AI vergeet CancellationToken systematisch door te geven. In een Azure Function of ASP.NET-controller levert dat een handler die niet stopt als de client al lang weg is, resources vasthoudt en je rekening opdrijft.
En dan foute invarianten. Een AI-gegenereerde Money-klasse gebruikt decimal in plaats van long cents. Dat werkt prima tot je twee bedragen optelt en een floating-point afronding in een factuur krijgt.
Elk van deze bugs is onzichtbaar voor wie niet weet wat er moeilijk is aan concurrent code, async lifecycle management of financiële rekenkunde. Goed prompten is geen vervanging voor het weten wat je controleert.
Het valse onderscheid tussen techniek en begrip
De kern van de claim is dat de kwaliteit van de uitvoering los staat van de kwaliteit van het denken. Maar de architectuurkeuze is het begrip. Als ik een webhook-ontvanger bouw en besluit de signature te verifiëren over de gedeserialiseerde payload in plaats van de ruwe bytes, is dat een begripsfout over wat een HMAC-signature garandeert. Het is tegelijk een codekeuze en een probleembegripskeuze. Die twee zijn niet separeerbaar.
In .NET zie je dit steeds terug. EF Core-queries die N+1 veroorzaken zijn geen onzorgvuldigheid; ze zijn een indicatie dat de engineer niet begrijpt hoe IQueryable versus IEnumerable werkt in de context van lazy loading. Een misconfigureerde Polly retry policy die zonder jitter zijn requests synchroniseert en een downstream service neerhaalt: geen slechte code in de enge zin, maar onbegrip van het systeem dat de code beschrijft.
// N+1: elke order laadt zijn klant met een aparte query
var orders = await _db.Orders.ToListAsync();
foreach (var order in orders)
Console.WriteLine(order.Customer.Name); // lazy load per rij
// Correct: één query met Include
var orders = await _db.Orders
.Include(o => o.Customer)
.ToListAsync();
Wie zegt dat code het makkelijke deel is, heeft simpelweg de prijs van slechte code nooit volledig betaald. Of hij was de manager, niet de ingenieur die het weekend werkte om de N+1-query terug te vinden in een productiedashboard dat van 2 naar 45 seconden was gegaan.
Wat AI echt verandert
AI maakt de boilerplate sneller. De scaffolding van een controller, het opzetten van een migratie, de eerste versie van een unit test: dat gaat sneller. Dat is echt en waardevol. Maar het verschuift de moeilijkheid; het verwijdert hem niet.
Als de boilerplate sneller gaat, stijgt de waarde van de engineer die begrijpt wat er boven die boilerplate zit. Architectuurkeuzes, performance-trade-offs, consistentiegaranties, error-strategie, testbaarheid. Dit zijn de stukken die niet in de eerste codegeneratie zitten, die AI systematisch niet goed afhandelt zonder begeleiding, en waarvoor je iemand nodig hebt die weet wat er moeilijk aan is.
Een senior .NET-engineer die AI-tooling gebruikt, levert meer dan ooit. Een junior die AI gebruikt als vervanging voor begrip, scheept meer bugs op dan voor het AI-tijdperk. De bugs zijn nu generieker en harder te debuggen.
Typen als documentatie van intentie
Er is een patroon in .NET dat ik steeds meer gebruik, juist omdat AI-tools het consistent missen: het uitdrukken van domein-intentie in het typesysteem. Neem een simpele OrderStatus. Als je het als string doorgeeft, kan AI eindeloos varianten genereren die "afgekeurd" en "rejected" en "REJECTED" door elkaar halen. Een discriminated union sluit dat af.
// String: alles mag, niets is gegarandeerd
public void Process(string status) { /* wat is hier geldig? */ }
// Type: de compiler weet het, en de documentatie schrijft zichzelf
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("Geen korting op afgewezen order"),
_ => throw new UnreachableException()
};
Wat dit oplevert: de compiler waarschuwt als je een nieuwe status toevoegt maar de switch vergeet bij te werken. AI gegenereerde code die status == "approved" schrijft, compileert niet meer. En de volgende developer die de code leest, ziet in de typen precies welke toestanden er bestaan en welke data per toestand beschikbaar is.
Dit is werk dat AI je niet uit handen neemt. AI genereert graag een class met properties. De beslissing welke properties er moeten zijn, welke staten geldig zijn en welke overgangen de compiler voor je bewaakt: dat is ingenieursoordeel. En het is precies het soort oordeel dat betere software maakt.
Wat dit voor jou betekent als .NET-developer
Stop met je verantwoorden voor de complexiteit van je werk. Thread CancellationToken all the way down, altijd. Maak null een compile-time probleem, los het op voordat het een crash in productie wordt. Geef foutpaden expliciete typen. Schrijf tests die de invarianten van je domein beschrijven.
Dit zijn keuzes die AI niet automatisch maakt. Dit is de expertise die waarde heeft. De claim dat coderen het makkelijke deel is, is daarmee niet alleen beledigend: het is een gevaarlijke simplificatie die betere software in de weg staat.
Veelgestelde vragen over codekwaliteit en AI in .NET
Wordt programmeerexpertise minder waard nu AI code genereert? Nee, de waarde verschuift naar de lagen boven de boilerplate. AI versnelt het scaffolden van code, maar het inzicht in concurrency, error-strategie, domein-invarianten en systeemarchitectuur wordt juist schaarser en waardevoller naarmate er meer code wordt geproduceerd die door niemand volledig wordt begrepen.
Wanneer gebruik je Result<T> in plaats van een exception in C#?
Gebruik Result<T> of een discriminated-union-type voor verwachte faalpaden zoals "niet gevonden", "validatie mislukt" of "quota overschreden." Bewaar exceptions voor onverwachte situaties zoals een DB-verbindingsbreuk of een bug. Op die manier dwingt de compiler de aanroeper het faalpad te behandelen en lekt het nooit stilletjes door naar een onverwachte NullReferenceException.
Hoe detecteer ik N+1-queries in EF Core in productie?
Activeer Application Insights of OpenTelemetry met een SQL-sampler en stel drempelwaarden in voor queries die te lang duren of te vaak per request worden uitgevoerd. Gebruik in development LogTo(Console.WriteLine, LogLevel.Information) op de DbContext-opties om elke gegenereerde SQL zichtbaar te maken en schrijf integratietests met Testcontainers zodat het patroon al in de pipeline opvalt.
Moet ik AI-gegenereerde code altijd reviewen? Ja, en specifiek op de patronen die AI systematisch mist: CancellationToken-propagatie, Scoped-vs-Singleton levensduur in DI, idempotentie van schrijfoperaties, en floating-point rekenfouten bij geldbedragen. Een korte mentale checklist op deze vier punten vangt de meeste sluipende fouten voordat ze productie halen.
Verder lezen
- Clean Code Is Niet Dood: Wat Nieuw Onderzoek Zegt Over Coding Agents en je Tokenrekening
- Context rot: waarom een groter context window je LLM-feature niet redt
- Toen een AI-agent inbrak bij Hugging Face: machine-speed aanvallen en je .NET-infra
Conclusie: De claim dat coderen het makkelijke deel is, verbergt een gevaarlijk misverstand over hoe software-complexiteit werkt. In het AI-tijdperk wordt de boilerplate sneller; de waarde van expertise in architectuur, type safety en domeinbegrip neemt toe. Schrijf de code die de compiler je laat schrijven, niet de code die net compileert.
Bronnen: "Code was never the hard part" is an insult to all programmers · Hacker News-discussie.
