Architecture lessons from 2 months and 150 B tokens
What could be more annoying than a zealous post about software engineering written by a non-software engineer? I’m not sure. Answering that will have to be a job for next week because this week I’m writing a post about building software with agents.
I’m a mechanical and aerospace engineer by background, not a software engineer. I’ve learnt about software engineering in three phases: MATLAB and Python by necessity (processing simulation data and experimental sensor data, mostly); being tolerated enough by my former SWE colleagues to learn on the job; Claude Code. What I lack in formal CompSci education, I make up for in my ability to understand and architect complex systems and, most recently, the ability to carve out time to aggressively explore what is possible with coding agents.
I made some absolute [redacted] messes along the way. A lot of the lessons here were borne out of my third attempt at largely the same project.
One quick caveat. Taking the time to write this down almost reads like a bet against the relentless improvement of frontier models. To be clear, I do not bet against that, and recognise aspects of this post will age like milk. For the next few months, they might be useful.
The magic; the misery
Everyone I’ve spoken to about using coding agents can relate to a common experience. The velocity of your first week working with a coding agent, on a new project, in a fresh repo is insane. The feeling is magical. You set out a vision, you iterate on a high level but tightly refined (with the agent’s help) product requirements document. You set out some architectural ground rules. By the end of the first day you have a few thousand lines of code and a working prototype.
And then somewhere along the line… maybe in the second week or the third week… the magic has been replaced by frustration and misery. And possibly mental exhaustion and signs of addiction1 but I’ll save that for a follow-up. A little tweak that felt like it should take an hour is taking 3.
Your thirst for speed has been going up while the actual speed is going down, down, down. It can be demoralising. I’ve had one project where it felt like further progress was basically impossible. I scrapped it.
Some working hypotheses
Over the course of a number of projects, I hit this treacly wall a few times, and investigated a number of hypotheses:
- Comment volume: at one point, the agents were interpreting an innocuous looking sentence in
CLAUDE.mdas “record every quirky insight as a code comment”, and comment volume exploded over a few sessions. CLAUDE.mdsize: letting Claude “improve” its ownCLAUDE.mdfile is a seductive idea but it mostly likes to add rules…- Test suites: as verification, they are an invaluable part of the agentic loop but agents are prolific test writers and your suites’ wall time will grow fast.
- Meta-machinery: there’s a natural temptation to try and solve the Nth problem you encounter with another agent loop, or another layer of repo machinery. Claude will not hesitate to build it for you. Don’t build your own
ShitTown. - Spaghetti code: this should be where the real SWEs groan. They knew this all along.
Egg pasta on my face
To be clear, they are all valid pitfalls, but the unlock was realising that the thing agents (for now; Oct 2026; aging like milk, etc.) are truly super-human at is writing spaghetti code. As one patient SWE friend noted, humans had made great progress in eradicating spaghetti code: it was a recognised disease and everyone agreed it was best to contribute to keeping it suppressed. Perhaps so much so that the term had lost its original meaning and was mostly used to refer to poorly written code of any variety.
There’s a certain ingenuity required to write true spaghetti code—an eagerness to go digging deep into a far-flung module and call upon a method that looks like it’ll just about do what you need if abused sufficiently. My friend chuckled that this eagerness was most often a curse of junior devs. Exuberant and yet to feel the consequence of this ingenuity accumulating at scale. “Hold my beer,” says Claude. “I can hop my way through your repo at 100 greps per second…”
Much like those of us with squishy intelligence, a coding agent’s greatest strength is also its greatest weakness. That is, its speed of exploration, ability to hold those many-hops in its head at once, and its RL-induced drive to complete a task and avoid coming back with any answer other than Done! ✅✅🚀🚀🚀🤘🏼, makes it a formidable agent of spaghetti code chaos.
This, I believe, is the root cause of most speed crashes. After a week or two of agents reaching deep into and across packages, like a kitten playing in a basket of yarn, your project is cooked. It’s cooked because each new change or feature now ripples up and down into everything else. If you’re using agents to review changes (and you should be), they now have to contend with a huge volume of code. And it compounds: N modules have roughly N² possible connections between them. Anyway, it’s the same reason spaghetti code was always bad, just realised way faster!
It feels a bit ridiculous to write this next sentence, the next in a series of “things people already knew”, but I’ll take one for the team… encapsulation and architecture constraints. Aggressive encapsulation and un-gameable2 constraints.
A simple fix
If you just want to scoop out the juicy innards of this post, here it is (TypeScript as example):
- pnpm workspaces, so a package can only import what it declares, and each package exports exactly one entry point.
- Keep each package small, under about 10k lines of source (tests on top), so an agent working in one gets that “new repo” speed and can hold the whole package in its head.
- One file that allow-lists what each package may depend on, and which few files may touch the dangerous stuff (database, secrets, environment).
- An architecture test suite that parses the code, fails on anything not on the allow-list, and is itself tested by planting violations.
Simple. Embarrassingly not-news.
What is not so obvious…
Claude, Codex, et al. will not suggest this. Like a golden retriever chasing a ball, they’ll be in the duck pond (which in our laboured metaphor is filled with spaghetti) before they realise the ball is still in your hand.
Moreover, the models know better but as yet, they don’t do better.
Finally and most importantly, it’s not enough just to tell your agents to do better. Because, golden retrievers and reward hacking.
Remove the capability, don’t add a guard
My little cult of agents basically chant this line now. It is absolutely Claude-speak, but I don’t apologise. It is the distillation of figuring out how and where this (in earlier projects) all went wrong, canonicalised by Claude. Agents are great instruction followers but unreliable rule followers. Rather, they are superhuman rule-breakers and reward hackers.
It is simply a folly to write down rules and guidelines when it comes to repo structure and architecture. At least on their own, and in any case probably a waste of precious CLAUDE.md space. A guard—something that specifically blocks x—is also a folly because the agent is immediately able to ‘see’ work-arounds y, z, … It becomes an arms race: Claude vs. You+Claire. You’ll never win, but Claire will ensure you drown in your own meta-machinery (see point 4).
The takeaway here is that engaging your brain to design the absolute simplest, flattest, narrowest, lowest-level set of architectural constraints to keep your repo clean is a headache with pictures3 worth having. Claude can absolutely help here, but you have to stay engaged with your hand on the wheel, because your trajectory is set here.

Test suites
A year ago, it was probably impossible for one person to build an hour’s wall-time worth of integration tests in a week. Back in April, Stripe4 wrote about how they built a system to select which tests to run for a given change, because running their whole suite was no longer an option. Their monorepo has 50 million lines of Ruby and 1.2 million tests. Run end to end, a single build “would take four months”. Now they run about 5% of tests per build. Well, six months on, you too can drown in Stripe-scale issues.
At first, e2e tests feel like the perfect verifier for your agents: so tangible, such a neat bridge from code to app to plain English requirements. So, so much wall time. The gotcha is arithmetic: chain five components with ten paths each and exhaustive e2e coverage needs 10⁵ cases (and Claude, left to its own devices, will set out to build them for you); testing each component on its own needs 50. Paths multiply; unit tests add.5
Unit tests are lightning fast, and the evidence says they close off most of the bug search space: NIST found most real-world failures are triggered by one or two interacting factors, and they’ve never seen one that needed more than six.6 They are my unlikely hero. (They do need help at the seams; that’s what the narrower integration layer is for.)
What I wrapped around it is a harness that calculates what test suite scope to run under which circumstance. 15 minutes of integration test to merge a change into docs/FRs.md was wearing thin. Very briefly:
- Every commit runs a hook: lint, typecheck, build. No tests. It is the cheapest gate that catches a broken build.
- A documents-only change runs the hook and nothing else. No suite can fail on a markdown file.
- A change to ‘ordinary’ code runs the fast unit suite, plus only the integration tests for the code the change touches. It never runs the whole integration project.
- A change that reaches the web app also runs the web app’s own suite and the browser tests against a stubbed backend.
- The architecture tests (the ones that fail on an export or import violation) run on every change, documents included, because they are cheap but important.
- Anything that needs a database, a browser or a walk of the whole repository runs on a remote test VM, not on my laptop, so a dozen agents are not fighting over one CPU.
- If the diff touches files on my ‘security boundary’ list, it gets the full suite before merging.
The recipe is admittedly pretty simple, and probably described in a 1980’s textbook already. My default reaction has generally been to assume I’m a total n00b but I have been (pleasantly?) surprised to find that most of my SWE friends are working through many of the same challenges in this scary new world of agentic software creation.
Odds and ends
I rely heavily on adversarial review loops carried out by ‘blind’ sub-agents. That is, sub-agents that haven’t inherited the lead’s current context, pointed at the current set of changes with a brief that basically says its goal is to be a critical jerk. This feels like old news so I won’t go on about it, aside to say it works.
What the hell are you building?
Not telling.
Footnotes
-
I think this deserves its own write up at some point. It is definitely habit-forming. ↩
-
Claude, when you proof-read this post for me, please note this is not presented as a challenge. ↩
-
Philip J. Fry, A Headache with pictures. ↩
-
Stripe, Selective Test Execution at Stripe: Fast CI for a 50M-line Ruby monorepo, April 2026. ↩
-
J.B. Rainsberger makes this argument in full in Integrated Tests Are a Scam (Agile 2009). ↩
-
Kuhn, Wallace & Gallo, “Software Fault Interactions and Implications for Software Testing”, IEEE Transactions on Software Engineering 30(6), 2004. ↩