Maintainable Systems with AI: Tactical Speed, Strategic Design

AI is here to stay, but AI-generated code, even from the latest models, still lacks good design practices and code cleanliness. Commonly known as "AI Slop": code that eventually reaches a breaking point where adding one new feature breaks three others.

This raises the question: How can I use AI to write good code? somewhere in the middle of the traditional craft of software engineering and Vibe coding[1], being able to build robust, maintainable systems while still leveraging AI agents and benefiting from the enormous productivity they provide.

Why purely AI-generated code isn't maintainable

You may have submitted a PR before with 20+ files generated entirely by AI, leaving it do all of the development till the app "just" works without any kind of iteration on the implementation from a design perspective, with no thought put into how this system may evolve in the future and how to extend it.

The feature might work just fine, but is it maintainable? you will only know once it's too late.

What is AI slop

If we want to define it in software engineering context it refers to low-quality, buggy, or insecure code generated rapidly by AI, often lacking human craftsmanship, context, and optimization, leading to technical debt.

In other terms, bad code but generated by AI, where the complexity of adding new features increases as the codebase grow.

Even the newest LLM models with the most fancy AI agent will still generate bad code if not thoughtfully guided by a good developer. And I don't believe this will change with whatever new LLM model that will come out. It's a limitation of LLMs, though the ecosystem around LLMs is improving everyday and the utilization of LLMs keeps getting higher (and more expensive), but in general, LLMs won't be able to develop a maintainable system alone.

So AI isn't the future?

No, that would be a ridiculous thing to say, and I think a lot of fully AI-generated apps will still get shipped and used, not all codebases needs to be maintainable with clean code, but any system that is expected to be there for years and used by many users, with updates always being shipped, this type of system must be maintainable or it will reach a dead end at some point.

Now we have the question, How can we use AI to build maintainable systems?

AI and Maintainable Systems

Even if you tell an AI: "You're a senior software engineer" and all the other fancy AI rules, it still won't generate senior engineer code, while these prompts do help, AI continues to move tactically. AI agents do not "think" the way humans do. They process a prompt, identify requirements, and iterate until they find the "solution". This process rarely considers how a feature might evolve or how new features may depend on it.

In short: AI thinks tactically, while (good) developers think strategically.

Strategic and Tactical Programming

This terminology was used by John Ousterhout in A Philosophy of Software Design: "Tactical vs Strategic programming"[2]:

Most programmers approach software development with a mindset I call tactical programming. In the tactical approach, your main focus is to get something working, such as a new feature or a bug fix. At first glance this seems totally reasonable: what could be more important than writing code that works? However, tactical programming makes it nearly impossible to produce a good system design. The problem with tactical programming is that it is short-sighted. If you’re programming tactically, you’re trying to finish a task as quickly as possible.

Strategic programming requires an investment mindset. Rather than taking the fastest path to finish your current project, you must invest time to improve the design of the system. These investments will slow you down a bit in the short term, but they will speed you up in the long term

...and the recommendation is to do tactical programming most of the time and sometimes do strategic one, this will keep development fast, while still investing in the system and keep it maintainable, which will payoff on the long run.

The most effective approach is one where every engineer makes continuous small investments in good design.

A similar take on strategic vs tactical programming is the edited version of Facebook Slogan: Move Fast without Breaking Things[3],

We can apply those ideas on development with AI agents too, although the consequence of not doing strategic development here is more amplified, as it's tempting to let AI develop everything, And because of how AI slop is contagious (AI agents like to follow existing patterns), a codebase can become a complete mess in a few months.

But that can also mean being strategic in the right places will pay off quickly because, again, AI agents like to follow existing patterns.

We want to identify when we should slow down and be strategic to ensure the reusability, extensibility of core parts in the system, and when we shouldn't care much about the design decisions and not fuss much on parts AI gets wrong (so we can still move fast).

Knowing when you should take control (be strategic) will become an intuition: is this task worth careful design or can AI handle it?

Below are several areas where thinking strategically pays off on the long run:

Architecture and Layering

When the codebase haven't established a clear way of doing things yet, AI doesn't have patterns to follow and will try to use code conventions and folder structures in the simplest way possible, which most likely won't scale up later, and it will become harder and harder to refactor moving forward, so it's a good idea to take time setting up conventions early on, also try to use AI-friendly conventions, like colocation[4] which will help AI read the needed context easier.

Shared Utilities & Components

I noticed a pattern with AI that it doesn't try to extract out repeated logic alone, or at the very least it's not very good at it because most of the time the reusable parts are not very clear, so it falls on the developer to always keep an eye out for any piece of code that would make sense to extract out.

APIs and Contracts

APIs are hard to change once they're used, be it the props of a component, the shape of requests and responses, or even function parameters, AI tends to define those based on the current needs without considering contract stability, say you prompted AI to extract a component out, more probably than not, it will expose more props than needed, which will make this component very inflexible for updates in the future because of how shallow it is, instead components/modules should be deep[5], this is usually an obvious design and it's implemented naturally by any good developer, but not with AI from my observations.

... and more

The listed above are just examples and what you should think about strategically will vary widely depending on the domain and tech stack, but you can see the pattern, what you need to focus on should be anything that: 1. keeps codebase maintainable, easy to update. 2. helps AI to do more work without needing you to intervene repeatedly.

Conclusion

The technologies around AI are evolving everyday and there is still significant room for improvements in the ecosystem, however I believe LLMs themselves have reached their peak, and that is to say LLMs likely won't be able to produce better code than what they do today with whatever model that may show up.

Then how do we build maintainable systems with AI? I don't believe it can be done with completely autonomous AI agents. Because AI writes code tactically, it lacks the foresight for long-term codebase health. Maintainable systems with AI are possible if it's guided by a developer, who thinks strategically, someone who designs deep, robust modules that can allow AI to write better code without sacrificing productivity.

Reference

  1. Vibe Coding Definition (Tweet)
  2. A Philosophy of Software Design by John Ousterhout (Book) or you can read a blog version: Strategic vs Tactical Programming
  3. Move Fast without Breaking Things (Article)
  4. Colocation(Article)
  5. Modules Should Be Deep (Article)