DEV Community

Cover image for The conversation around software development is regressing with AI
Armando Picón
Armando Picón

Posted on

The conversation around software development is regressing with AI

One of the advantages — or possible disadvantages — of keeping up with the latest advances in technology is that, at some point, a revolution can become so constantly present on social media that you inevitably end up saturated with every post, video, or reel talking about it.

I remember that this was one of the reasons why I initially refused to adopt AI in my workflows.

However, time passed and, inevitably, I started using some tools for things as simple as iterating over ideas and thoughts that tend to come back to me while I study and learn new things.

That helped me lower some of the barriers and mental blocks I had imposed on myself as a result of the information overload I experienced at the beginning.

On top of that, I have also had the chance to filter the kind of content I consume much more carefully.

For now, I follow a fairly limited number of creators because I know they actually put into practice the things they talk about. So I get the feeling that the amount of "bullshit" is considerably lower compared to other, more sensationalist creators.

And it was precisely through that selection of content that I started noticing something I find quite interesting.

Are we reinventing the wheel?

Those who follow me across my different social networks know that I mainly work on mobile application development for Android and that, more recently, I have also become involved in multiplatform development thanks to Kotlin Multiplatform.

However, while exploring the use of AI tools in software development more broadly, I have started to notice a kind of regression in the conversation around our profession.

Not necessarily in the tools.

Not necessarily in the capabilities we now have available.

But in the way we talk about them.

I keep coming across more and more content — including content from engineers working directly at companies building these kinds of tools — where it feels like we are rediscovering practices that have been part of software engineering for decades.

Now we constantly talk about guardrails to prevent an agent from introducing incorrect code.

But we have been using compilers, type systems, linters, static analysis, tests, and pipelines for years precisely to establish boundaries around which changes we consider acceptable.

We can also design increasingly sophisticated workflows to improve the context a model receives, organize instructions, split work across agents, or help the model understand our codebase better.

All of that has value.

But if, after building that workflow, we still do not have a solid CI system capable of automatically verifying what the model has just produced, then we are still placing a large part of our trust in the assumption that the model understood our instructions correctly.

And when we start talking about agent verification, the mechanism underneath often looks surprisingly familiar:

the agent generates code, we run a series of checks, we observe what failed, and then we iterate again.

The automation may be new.

The speed may be new.

The agent is definitely new.

But the principle behind the verification is not necessarily new.

And I think this is where it is worth stopping for a moment.

Probabilism vs. determinism

For the average user — and probably also for developers who have not spent much time studying what is happening behind these tools — AI can look like a black box.

You give it an instruction and, almost magically, it starts generating a series of results that will probably satisfy what you are looking for.

But if we look a little deeper, we find something much more interesting.

Behind many of these tools there is an entire set of components that, working together, give us capabilities such as reasoning, memory, access to information, interaction with local or remote tools, execution of actions, and more.

At the center of much of this machinery are language models.

And behind these models are mathematical systems that, simplifying things quite a bit, estimate a probability distribution over what the next token in a sequence should be.

That detail matters.

We are working with probabilistic systems.

That means we should not assume that the exact same input will necessarily produce the exact same output.

On the other end, we have deterministic systems.

In other words, systems where, given the same conditions and the same input data, we expect to obtain the same result.

A simple arithmetic operation is probably the most obvious example.

2 + 2 will still be 4 no matter how many times we run the operation.

And this is exactly where a paradox appears that I find particularly interesting:

The more probabilistic the way we generate code becomes, the more we need deterministic mechanisms to verify whether that code is acceptable.

Back to software development

A few lines above, I mentioned that I still see and hear content creators related to software development talking about how much AI is changing our profession.

And yes, many things are changing.

The speed at which we can produce code is changing.

The number of alternatives we can explore is changing.

The way we interact with our tools is changing.

Even the role developers play in some parts of the process is changing.

But if we deliberately abstract away many of those differences, there is one fundamental change that is much easier to observe:

We can delegate code generation.

Before, that code was written primarily by a human.

Today, an increasingly large portion of it can be generated by an AI model.

Of course, someone will say that I am oversimplifying things.

And yes.

I am.

Deliberately.

Because that simplification allows us to observe something important: changing who — or what — generates the code does not eliminate the mechanisms we used to verify it.

Quite the opposite.

For decades, we have been building fairly deterministic mechanisms that help us reduce uncertainty and verify certain properties of the software we produce.

To name a few:

Compilation and type systems

Compiler checks, type systems, and null-safety help us detect type errors, invalid contracts, and nullability issues before running the program.

Static Analysis

Android Lint, Detekt, SwiftLint, or Ktlint help us detect certain known bugs, bad practices, and code smells through predefined rules.

Formatting

Ktlint, Spotless, SwiftFormat, or Dart Format help us establish consistent and verifiable formatting rules.

Unit Testing

JUnit, XCTest, Kotlin Test, or Dart Test help us verify units of behavior in isolation.

Integration Testing

It allows us to verify how different components work together: repositories, databases, network layers, services, persistence, and so on.

UI Testing

Compose UI Test, Espresso, XCUITest, or Flutter Integration Test help us verify certain behaviors through the user interface.

Code Coverage

JaCoCo, Kover, or Xcode Coverage help us measure what portion of the code was executed during our tests.

Coverage does not prove quality by itself. Having 100% coverage does not necessarily mean having good tests, much less having a bug-free product. But it is still a measurable signal that can be part of a broader verification system.

Git Hooks

Pre-commit and pre-push hooks allow us to run certain checks before our changes leave our machine.

And we could keep going.

CI pipelines.

Contract testing.

Mutation testing.

Property-based testing.

Dependency checks.

Security checks.

Reproducible builds.

There are many more.

And some of them will probably make much more sense than others depending on the product, platform, or team we are working with.

The important thing is not to use all of them.

The important thing is to understand the principle behind them.

None of these practices guarantees, on its own, that our software is correct.

That absolute guarantee has practically never existed.

What they do is something far more useful:

They systematically reduce our uncertainty.

AI does not eliminate these practices

It makes them more important.

And this is precisely where I feel part of the current conversation is looking too much in one direction.

We can spend hours improving our prompts.

We can discuss how to build better context files.

We can experiment with different ways to organize instructions.

We can create agents, sub-agents, tools, MCPs, and increasingly complex workflows.

In fact, we should learn how to work with all of that properly.

But if our strategy ultimately depends exclusively on the model "understanding" what we want and producing correct code, then we are still playing with prompts.

We are still trying to increase the probability of getting a good answer.

And that is not necessarily bad.

The problem starts when we confuse improving that probability with verifying that the result is correct.

They are different problems.

Probabilistic generation, deterministic verification

I think we can use a fairly simple framework here to think about how to incorporate AI into our development processes.

It is not trying to invent anything new.

In fact, that is exactly the point.

1. Generate

We allow the model to do the things these tools can be extremely useful for:

exploring alternatives, writing code, performing refactors, proposing implementations, navigating our codebase, or even operating directly on our repository.

At this stage, we accept the probabilistic nature of the process.

We are not trying to eliminate it.

The model proposes.

2. Constrain

Then we make the generated code face contracts defined by us.

The compiler.

The type system.

Null-safety.

Formatting.

Linting.

Static analysis.

Architectural rules.

API contracts.

Whenever a constraint can be turned into an executable rule, we gain the ability to stop depending on the model "remembering" it.

The system simply enforces it.

3. Verify

Then we verify behavior.

Unit tests.

Integration tests.

UI tests.

Contract tests.

Property-based tests.

Whatever mechanisms make sense for the problem we are solving.

At this point, we stop asking whether the code "looks correct."

We start asking whether it satisfies concrete properties we can verify.

4. Decide

Finally, we establish the conditions under which we accept that change.

Build.

Tests.

Quality gates.

Security checks.

Code review.

Human approval when appropriate.

Only after going through those verifications do we allow the code to move forward.

Generate.

Constrain.

Verify.

Decide.

In reality, we are not doing something radically different from what we were already doing.

We are changing the entity that generates part of the code and dramatically increasing the speed at which it can do so.

And precisely because of that, we need to strengthen the rest of the system.

So what is a guardrail, really?

This is where the conversation becomes particularly interesting.

A guardrail does not have to be a particularly clever instruction inside a prompt.

It can simply be a rule the model cannot ignore.

The model can forget that we have a particular formatting convention.

Ktlint does not.

The model can incorrectly infer a type contract.

The compiler does not negotiate with it.

The model can convince us, with a perfectly reasonable explanation, that its implementation works.

A failing test has a very different opinion.

Of course, we can now use agents to execute all of these mechanisms, interpret the results, and even automatically fix whatever failed.

That does represent a new and extremely interesting capability.

But we are building that capability on top of principles we already knew.

We should not confuse a new layer of automation with the emergence of a new discipline.

It is still software engineering.

Stop just playing with prompts

I have nothing against prompts.

Nor against context engineering, agents, or increasingly sophisticated workflows.

Quite the opposite.

They are tools I am progressively incorporating into my own work, and I find them extremely interesting.

But I think there is an important difference between improving the probability that a model will produce correct code and building a system capable of rejecting it when it does not.

The first strategy tries to get a better answer.

The second reduces our dependence on the answer being correct in the first place.

And if I can choose where to place my trust, I would rather place it in what I can verify.

Maybe that is one of the most interesting opportunities AI is giving us.

If we increasingly need to spend less time manually writing every single line of code, then we can invest part of that time in building better contracts, better tests, better pipelines, and better verification systems.

Not to stop AI from working.

But precisely so that we can trust more of what it produces.

AI is dramatically changing who can generate code, how much code we can generate, and how quickly we can do it.

But it has not eliminated the engineering we need to decide which code deserves to reach production.

And maybe part of the conversation around software development is regressing precisely because we are too fascinated by this new way of generating code and are forgetting everything we had already learned about how to verify it.

The more probabilistic software generation becomes, the more deterministic the process that decides whether we accept the result should be.

Top comments (0)