AI good practices are actually good engineering

You can’t ignore all the noise on the internet about using AI for coding. You won’t be able to form an opinion based only on what you read: the topic is very polarising. It is full of posts from people claiming incredible success, and just as many complaining that it’s utterly useless and that they spent more time fixing the slop it produced than writing it correctly in the first place.

For this reason, I ventured into the AI coding world with the hypothesis that it is not something magical that works out of the box, but something that requires specific workflows and practices to be effective.

After plenty of time working alongside an LLM and experimenting with different approaches, I had the most obvious of epiphanies: AI good practices are just software engineering good practices.

When I talk about software engineering, I mean the approach that includes domain and data definition, software design, documentation, testing strategy, and coding workflow.

Domain definition and data model

The domain definition and data model are things I would not let the AI figure out in isolation. First of all, they are both aspects that can’t be easily rectified later, unlike the code itself.

They also require experience, judgement, and deep business-domain context. All things that are hard to fully encode in a document for the AI to digest. Often, the AI can figure out a sensible solution for a very common case, but this will likely not fit very well. For this reason, even if it works from time to time, I would not rely on it.

Technology

Another area I would leave to humans is technology choice. Technology decisions must take into consideration many factors that, ironically, have very little to do with the technology itself: licensing, staff skills, cost to run, and cost to maintain, for example.

When adopting a technology, it is also very effective not to give the AI too much room for creativity. “Use Node.js” is not enough (which libraries? where will the service run? etc.). It is much better to provide templates so that the final result is closer to what the engineer intended.

Software design

Another part where engineering practices help AI be effective is software design.
The human engineer is best suited to define program boundaries, interfaces, and how the pieces fit together. Again, this is a matter of experience and context.

Once the units are well defined, the chances of the AI generating duplication are reduced. This applies to any type of code, including frontend components. If the AI can rely on a neat set of UI elements, it can output a clear and coherent frontend structure instead of something entirely custom and unmaintainable.

Documentation

Documentation is the part where I think every change should start. I learned (the hard way!) that writing documentation often helps uncover logical problems and refine ideas. Just to be clear, when I say documentation I include project READMEs and docstring. Not something abstract and located far from the project. Sparring with AI here can be helpful to speed up the process, but ultimately the engineer needs to ensure that things are explained with a sufficient level of detail and that edge cases and exceptions are taken into consideration.

Other guardrails

Having solid guardrails can significantly speed up the review process and ensure that the AI works in the way we expect. Documentation helps, but linting rules are even better - this is another area I would leave to humans.

I also believe strictly typed languages are much more promising. TypeScript is better than plain JavaScript, and I suspect languages like Rust may be even better (although I am still experimenting with this).

Unit testing and TDD

Once documentation is in a good state and interfaces and boundaries are defined, this is when I let the AI try writing tests. I do this because writing exhaustive tests is long and boring, but also because incorrect tests usually reveal missing details in the documentation.

These issues need to be fixed iteratively.

In any case, as an engineer you must thoroughly review the tests. If they are incorrect, everything that follows will fail for sure.

In recent years I have seen many colleagues become increasingly dismissive of unit testing and the the good old testing pyramid. I predict AI will change that attitude. I can also say that I rarely used TDD in the past, preferring to write tests while writing the code rather than before.

The introduction of AI in my workflow has settled this definitively. TDD it is!

Commits and review

When it comes to coding, once the necessary guardrails are in place, you can let the AI fill in the blanks. Since you will still need to review the output, it is important to break changes into small pieces, ensuring that each step leads to a working state.

The coding phase must always include testing, depending on the strategy chosen by the engineer. I am not referring to unit tests here, but to integration or end-to-end tests.

Simply specifying “and add test coverage” does not work well with AI. In my experience, it often results in poorly conceived tests that follow anti patterns and add little value. It is far better to provide examples for the AI to follow.

Local code review should be treated as a pull request from an external contributor. As a (human) reviewer, you evaluate the approach, verify that relevant tests are included, and ensure documentation is updated.

You should not need to manually test the code. If you catch yourself doing so, it is a good signal that an automated test is missing.

What about brown field projects?

I suggest using a similar workflow for brown field projects:

  • documentation
  • tests
  • changes

I suspect that one reason people believe AI does not work well for brown field projects is that these systems often lack documentation and tests. Some knowledge may have been lost over time, making reviews awkward and risky.

The bad news is that AI is unlikely to perform well unless, with patience, we spend time reconstructing documentation and tests. Once again, this is a pattern that existed long before AI entered our workflows.

Exceptions

That said, software engineering should also consider context. For example, is the project just a prototype or an experiment that will be rewritten?

In those cases, different criteria apply, and we can give the AI a longer leash.

A note on spec-driven development

If you follow the latest AI trends, you may notice that my workflow is iterative and primarily human-driven.

I tend to agree with Kent Beck here: spec-driven development does not currently align well with my way of working—though I am open to changing my mind.

Conclusion

In my experience, good engineering is the main factor behind success. Conversely, a lack of engineering may produce results faster initially, but in the long run it leads to over-complicated solutions, technical debt, low team velocity, and ultimately unmaintainable codebases.

It is good engineering that makes coding successful. AI is just a boost that helps reach the final outcome faster.

Written by

Maurizio Lupo
Maurizio Lupo Senior Engineering Manager