Writing Code by Hand Is Really About Owning the Architecture
The phrase “going back to writing code by hand” sounds like a rejection of AI coding. The more useful reading is narrower and more technical: the author is going back to doing the design work by hand before letting any tool fill in implementation.
That distinction matters.
The case study is k10s, a terminal UI for Kubernetes GPU clusters. It began as a focused tool: inspect NVIDIA-heavy Kubernetes environments without carrying the full weight of a general-purpose dashboard. After seven months and hundreds of AI-assisted commits, the project still worked, but the code had become hard to reason about. The author archived the repository and decided to rewrite it from scratch.
The interesting part is not that AI produced bad code. The interesting part is that AI produced enough useful code to keep the project moving while the architecture quietly collapsed underneath it.
A working demo can hide a structural failure
The failure mode was concrete: a 1,690-line model.go, one large Model struct holding too many responsibilities, and an enormous Update() function dispatching everything through a forest of cases.
That shape is familiar even without AI. A terminal UI often starts with one model, one update loop, and a few modes. Then it needs pod views, node views, log streaming, describe panes, mouse handling, navigation history, filters, cluster clients, cached state, background refreshes, and special behavior for GPU resources. If nobody stops the growth, the first convenient object becomes the place where every new feature lands.
AI makes this worse because it lowers the friction of adding the next feature. When the prompt is “add X,” the model optimizes for fitting X into the existing shape. It does not stop and say, “This file is becoming the architecture.” It is rewarded for local success: make the tests pass, preserve the behavior, avoid a large rewrite, return a useful patch.
That is exactly how a god object grows. Not through one bad decision, but through many reasonable local decisions that nobody forces into a global design.
The tool did not control scope; it amplified it
The original k10s idea had a clear product boundary: a GPU-aware Kubernetes TUI. That is a useful niche. It implies opinionated views, fast access to GPU health, and workflows that general Kubernetes tools do not prioritize.
But when feature generation feels cheap, scope becomes slippery. The project can drift from “GPU fleet dashboard” toward “another k9s clone with GPU features.” Every additional view seems individually reasonable. Pods? Of course. Logs? Needed. Describe? Useful. Mouse support? Nice. More resource types? Why not.
The trouble is that product scope and code architecture are coupled. A narrow tool can keep a narrow architecture. A broad tool needs explicit module boundaries, state ownership rules, and extension points. If the scope expands while the architecture remains the original prototype shell, the codebase starts lying about what it is.
This is one of the hidden costs of AI coding. It makes scope expansion feel like progress. But every feature still has to live somewhere, interact with state somehow, and be understood by a future maintainer. A model can make the marginal cost feel low while the accumulated design cost keeps rising.
The missing artifact was architecture
The author’s proposed fix is not “never use AI.” It is “write the architecture down first.”
Not as a vague design document. The useful artifact is concrete:
- interfaces between views,
- message types,
- ownership rules for state,
- boundaries around background work,
- rules about which module may mutate which data,
- naming conventions that encode domain concepts,
- and constraints that the model can repeatedly see.
In an AI-assisted project, these rules need to be written because the model has no persistent taste. It may remember the current prompt and visible files, but it does not own the system. It does not feel the pain of a cross-view dependency. It does not get tired of reading a 500-line update function. It does not wake up six months later and maintain the code.
Architecture is the part of programming where responsibility cannot be delegated away cleanly. You can ask an assistant for alternatives. You can ask it to critique a boundary. You can ask it to generate boilerplate after you define the shape. But someone with actual ownership has to decide what the system is allowed to become.
Generated code needs smaller boxes
The obvious lesson is not “write every line manually.” The better lesson is “make the boxes smaller before generating code.”
An AI assistant works best when the target is constrained. If the instruction is “add fleet view to the app,” the model has a lot of room to smear state across existing files. If the instruction is “implement this interface in FleetView, emit only these message types, and do not read state from sibling views,” the model has a much smaller space in which to make a mess.
That means the human workflow changes:
- Decide the module boundary before generation.
- Define the data that crosses the boundary.
- Write the ownership rule in plain language.
- Ask the tool to implement inside that box.
- Review the diff against the boundary, not just against behavior.
This is stricter than ordinary prompt-driven coding. It is also closer to normal software engineering. The difference is that AI increases the rate at which boundary mistakes enter the repo, so the boundary has to be more explicit earlier.
The review standard has to include shape
A feature can be correct and still make the system worse.
That is the review gap exposed by the k10s story. If review only asks “does it work?”, generated code will often pass. If review asks “did this preserve the architecture?”, the answer may be very different.
For AI-generated code, a serious review should include structural questions:
- Did this add state to the right owner?
- Did it create a new implicit mode flag instead of a real type?
- Did it reach across module boundaries because that was convenient?
- Did it duplicate a pattern that should have been centralized?
- Did it turn a view into a controller?
- Did it make the next feature easier or harder?
These questions are not anti-AI. They are the minimum price of using a tool that can generate large patches faster than humans can fully internalize them.
The “gun to your head” rule from the surrounding discussion is useful: only accept generated code you could have written or repaired yourself. If the model gives you code that works but you cannot explain, that is not velocity. That is hidden debt.
Rewriting can be a rational reset
Archiving seven months of work sounds extreme. Sometimes it is.
But when the architecture is wrong at the root, incremental cleanup can become a trap. If the central model owns too much state, every refactor has to pass through it. If views are coupled by accidental fields, every extraction exposes another dependency. If background tasks mutate UI state directly, every concurrency fix touches behavior.
At that point, a rewrite is not an admission that the project failed. It can be the cheapest way to preserve what was learned:
- the product idea is clearer,
- the dangerous abstractions are known,
- the scope boundary is easier to draw,
- the second architecture can be designed around real use cases,
- and the team can encode rules that did not exist during the prototype.
AI-assisted prototypes may make this pattern more common. The first version discovers the product quickly. The second version has to be built with the discipline the first version skipped.
Rust is not the real fix
The author plans to rewrite k10s in Rust. That may help in specific ways: stronger types, ownership discipline, explicit concurrency, and fewer casual shared-mutable-state paths.
But language choice is not the core lesson. Go did not force a 1,690-line model file. Rust will not automatically prevent a badly designed central enum or a giant state object. Types can encode boundaries, but only after a human decides what the boundaries are.
The real fix is architectural intent. Rust can make that intent harder to violate. It cannot invent the intent for you.
This is worth stating because teams often respond to AI-generated mess by reaching for a stricter toolchain. Stricter tools help. They do not replace product judgment, module design, or review discipline.
A practical AI coding rule
The k10s lesson can be reduced to one rule:
Use AI after you know where the code belongs.
If you do not know the boundary yet, do not ask for the implementation. Ask for design options. Ask for failure modes. Ask for a sketch of message flow. Ask for a critique of two architectures. Then choose.
Once the shape is chosen, use the model for the parts where it is strong:
- filling in repetitive interface implementations,
- generating tests around explicit behavior,
- translating a pattern from one module to another,
- finding edge cases,
- writing small adapters,
- and checking whether a patch violates documented rules.
That keeps the assistant in the role of accelerator instead of accidental architect.
The real return to hand-written code
“Writing code by hand” does not have to mean typing every character without assistance. It means returning authorship to the human parts of programming: naming, boundaries, scope, invariants, taste, deletion, and responsibility.
The keyboard is not the sacred object. Ownership is.
AI can help produce code. It can help review code. It can help explore designs. But it will happily keep adding feature after feature to a shape that should have been replaced months ago.
The human job is to notice when the shape is wrong, stop the feature treadmill, and write down the architecture before the next line lands.