← Writing

The Code Worked. The Design Was Still Wrong.

  • #ai
  • #software-architecture
  • #event-driven
  • #software-design
  • #agentic-engineering

I asked an AI coding agent to build a feature. It designed it, implemented it, wrote tests, and shipped it. It worked.

It was still wrong. And what caught it wasn’t a tool, a test, or a better prompt. It was classic software design knowledge — the kind we’ve had names for since long before LLMs.

One thing up front: this wasn’t a weak model having a bad day. Everything below came out of Claude’s frontier tier — Fable 5, Opus 4.8. The code looked senior: clean naming, sensible structure, tests, a written-out design doc. It met every requirement I gave it. That’s exactly what makes this worth telling.

The context: Aura is an app I’m building to talk with Claude Code agentic sessions running on a remote machine, through a conversational chat UI. On top of that sit first-class, characterized assistants — each with its own soul and skills — that shape how the agent behaves and keep sessions resumable and grouped per assistant. The feature was simple to state: let me send a second message while the agent is still answering the first, instead of disabling the input box.

The design that worked — and described a world that doesn’t exist

The agent’s first version tagged my second message as Queued, showed a little “in queue” chip on it, and released it when the agent freed up. Under the hood it was procedural control flow: a central orchestrator stitching messages to executions to replies, hand-managing a virtual queue of tasks — even the component whose only job is running the agent knew about promoting the next message when it finished. Tightly coupled orchestration with good variable names. Spaghetti in a suit, quietly on its way to becoming a God object.

And here’s the uncomfortable part: it worked. I tested it. It did exactly what I’d asked.

Then I read it again, away from the screen, and something bothered me that took a while to put into words: my message was never queued. The instant I hit send, it was sent — as sent as a message can be. The design was telling me that sending was still in progress, and that’s simply false. The only thing pending was the reply.

It met my requirements, but artificially — by taking the state of one thing (the agent’s backlog) and painting it onto another (my message). The feature worked; the model lied.

A design can satisfy every requirement and still describe a world that doesn’t exist. That’s the kind of wrong no test catches.

Model the world, not the requirement

Think about how this actually works between two people. I text a friend, and my part ends right there — message sent, phone back in my pocket. Somewhere, their phone pings. They’ll look when they can: after dinner, after the meeting, whenever. Their reply arrives on its own, and it never retroactively changes what my sending was. Two independent acts, connected by a notification.

That is event-driven architecture, literally. Sending a message is one small, atomic act that completes immediately. It emits an event — a message arrived that needs attention. Whoever subscribes to that event reacts when they’re able. The answer is a side effect, produced asynchronously, by someone else.

So that’s how we rebuilt it: every message is accepted unconditionally, the moment it’s sent. An event fires. A separate component — subscribed to “message arrived” and “run finished” — decides when the agent actually picks it up. Choreography instead of orchestration: no central brain holding the whole flow in one hand, just pieces reacting to what happened, each with its one job. The software now behaves like the conversation it’s modeling.

The most honest designs are the ones that model how the world already behaves. Reality has excellent architecture.

Why this matters beyond the UI

Modeling faithfully gives you a truthful UI, sure. But the deeper win is structural: it preserves decoupling. This codebase had deliberately separated “a message got recorded” from “an agent processed it” months earlier — a seam cut on purpose. The agent’s design stitched it back together without noticing, just by letting one concept’s state live on the other’s model. And when you couple things that don’t need to be coupled, you’re mortgaging the future: every change to sending now has to think about processing, and the other way around. That’s exactly how software gets rigid.

It happened twice, in fact. Mid-rewrite I had to stop the agent again: it was carefully preserving an API endpoint that blocked until the answer was ready — the same coupling, one layer up. It agreed instantly and cut it.

That’s the detail worth sitting with. Twice the agent built the coupling; twice it recognized the problem the moment it was named. The knowledge was all there. The judgment of when to apply it was mine to supply.

The idea

The agent was genuinely good. It rewrote a nontrivial concurrency feature fast and cleanly, twice, and argued convincingly for the result. This is not a story about AI being bad at code.

But it is a story about a trap. It has never looked more like anyone can build anything: a frontier model hands you a working, complex, senior-looking system in an afternoon, and the demo is flawless. The bill arrives later, when you need to change it — couplings you never noticed start pulling on each other, nobody can say exactly what the system does anymore, and one day you realize you’re not evolving your software, you’re negotiating with it. Nothing about the demo warned you.

An LLM makes building look easy. It’s changing that stays expensive — and change is where all software spends its life.

The outcome here was decided by design knowledge: noticing that a model claimed something false about the world, knowing that events model a conversation better than a queue, recognizing a seam that had been cut on purpose. None of it showed up in a diff, and none of it came from the model — it came from the engineer holding the leash.

Writing code was never the hard part. Knowing what the code should say — and noticing when it says something slightly different — still is. That knowledge got more valuable, not less, the moment code became cheap.

Design still matters.