Programming the Non-Deterministic: The New Paradigm of Software Engineering
From automated code generation to building systems capable of producing, evaluating, correcting, and guaranteeing software autonomously.
English translation of an article originally written in Spanish. Leer el original en español.
In this article
- Introduction
- The problem of working with non-deterministic systems
- Programming systems that program
- Deterministic architecture around non-deterministic generation
- Validation can no longer rely on tests alone
- Context and constraints become part of the architecture
- Concurrency and consistency at enterprise scale
- The ability to self-evaluate and improve
- The new responsibility of developers and architects
- How do we measure that it actually works?
- The future is not a developer who programs less
- Conclusion: programming the non-deterministic
Introduction
We are not simply talking about automating programming. We are talking about a shift in the engineering paradigm we use to build software.
My hypothesis is that, over the coming decades, a large part of the work of developers and software architects will consist of designing systems capable of controlling, evaluating, and guaranteeing results produced by other non-deterministic systems: artificial intelligence models.
Large-scale code generation is already a reality. Models like Claude, together with tools like Claude Code, are showing that it is possible to delegate increasingly complex development processes, from targeted changes to implementations that involve multiple components of a system.
The problem is no longer just getting a model to generate working code.
The real challenge is getting a system to generate massive amounts of code, make technical decisions, modify existing architectures, and evolve applications without compromising their integrity.
And to get there, we need more than better models.
We need a new way of doing engineering.
1. The problem of working with non-deterministic systems
Language models do not work like a traditional compiler.
Given the same context, they can produce different implementations, choose different strategies, modify components in unexpected ways, or find solutions that look correct but do not fully meet the original requirements.
Even when we get satisfactory results, we cannot assume that the next run will produce exactly the same outcome.
This characteristic has important consequences when we talk about enterprise development.
In a small project, a developer can manually review an implementation, identify problems, and fix them. But once we start generating code at scale, with multiple agents working in parallel on different modules, that supervision model stops being viable.
We cannot depend on a developer reviewing every function, every change, and every decision a model makes.
Nor can we rely solely on the model itself to evaluate the quality of what it just produced.
We need to change the approach.
Instead of trying to eliminate the uncertainty of models, we have to design systems capable of working with it.
2. Programming systems that program
Imagine an engineering platform built on models like Claude.
It receives a functional requirement and has to turn it into a real implementation inside an enterprise system.
The process could be fully automated:
- Analysis: interprets the requirement, consults the documentation, and builds the necessary context.
- Planning: determines which components need to change, identifies dependencies, and proposes an implementation strategy.
- Generation: produces the code, tests, documentation, and changes required.
- Validation: runs functional tests, static analysis, security checks, and architectural verifications.
- Evaluation: compares the results against the original requirements and contracts.
- Correction: identifies problems, adjusts the implementation, and runs the validations again.
- Integration: prepares the changes to be incorporated into the system once the established conditions are met.
- Self-evaluation and improvement: analyzes the results, identifies error patterns, and proposes improvements for future runs.
The interesting part is that we do not need to program each of the solutions this system produces.
We need to program the system that determines how they are produced, under which conditions, which decisions the models can make, and how we verify that the results are acceptable.
This is a fundamental difference.
We are not just automating the writing of code. We are building software production systems.
3. Deterministic architecture around non-deterministic generation
This is probably the most important technical challenge.
If the component responsible for producing code is non-deterministic, the mechanisms responsible for controlling its results cannot depend on that component always behaving the same way.
We need to clearly separate two responsibilities:
Generating solutions
- Probabilistic and non-deterministic
- The model is free to find a solution
- Always within the established limits
Validating and authorizing results
- Explicit, verifiable conditions, reproducible whenever possible
- The system decides whether the solution can move forward
- Does not depend on the model behaving the same way
This does not mean every evaluation can be reduced to a boolean condition. Some properties, such as semantic quality or user experience, require more complex evaluations.
But it does mean we must set clear limits on what a model can decide and which conditions an implementation must meet without exception.
For example, we can establish that:
- An agent can modify certain modules, but not others.
- An implementation must respect existing API contracts.
- No change can introduce unauthorized dependencies.
- Changes must pass functional and integration tests.
- Critical vulnerabilities automatically block integration.
- Changes to sensitive components require additional approval.
- Every change must be traceable to its origin, context, and validations.
The model is free to find a solution within the established limits.
The system is responsible for determining whether that solution can move forward.
The control loop
An architecture of this kind could be represented as follows:
Input → Context → Generation → Evaluation → Validation → Acceptance → Deployment
Walk through the loop stage by stage, including what happens when an implementation fails.
- Input: a functional requirement arrives.
- Context: the necessary context is built: documentation, contracts, current decisions.
- Generation: the model produces a solution within the established limits.
- Evaluation: the results are compared against the original requirements and contracts.
- Validation: tests, static analysis, security, and architectural checks. If it fails, the system identifies what failed and generates again with that context, and the new implementation goes through every check once more.
- Acceptance: the change moves forward only if it meets the conditions; the correction loop has limits and completion criteria.
- Deployment: the change is incorporated into the system, traceable to its origin, context, and validations.
But there is a fundamental part: the loop does not end when an implementation fails.
The result of every evaluation must become useful information for the next attempt.
If a model produces an implementation that does not pass the tests, the system must be able to identify what failed, provide the corresponding context, and request a new solution.
There is one condition, though: fixing a problem does not mean automatically accepting the result.
Every new implementation must go through the corresponding checks again. A fix can resolve one defect and introduce others.
That is why the generation and correction loop needs limits, completion criteria, and mechanisms to prevent infinite iterations or progressive degradation of the solution.
4. Validation can no longer rely on tests alone
I think one of the mistakes we can make when building these systems is assuming it is enough to ask a model to generate code along with its tests.
That guarantees nothing on its own.
A model can generate an implementation and tests that share the same conceptual error. It can produce tests that validate the behavior it implemented, but not the behavior the business actually needed.
That is why we need independence between generation and evaluation.
Validation should combine different mechanisms:
Deterministic tests: unit, integration, contract, and regression tests that verify specific behaviors.
Static analysis: tools that detect typing, quality, dependency, vulnerability, and convention problems.
Architectural validation: checks that confirm changes respect the system's boundaries and structural decisions.
Semantic evaluation: mechanisms that check whether the implementation actually satisfies the original requirements, including scenarios that cannot easily be expressed through conventional tests.
Behavioral testing: running real or simulated scenarios to detect unexpected behavior.
Independent evaluation: when other models are used to review results, their assessments must be checked against objective criteria and must not become the sole authority for acceptance.
Turn on the mechanisms the article describes and see which generation risks become covered. None of them is enough on its own.
Validation mechanisms
Risks of non-deterministic generation
- Regressions in known behavior
- Vulnerabilities
- Unauthorized dependencies
- Structural boundary violations
- Broken contracts between components
- Fails the original requirement
- Unexpected behavior
- Code and tests sharing the same conceptual error
Combining these mechanisms makes it possible to build evidence-based confidence, instead of depending on how much we trust a particular model.
And this is especially important for enterprise software, where an error can have operational, financial, legal, or security consequences.
5. Context and constraints become part of the architecture
Another aspect I consider fundamental is context.
A model can be extraordinarily capable, but if it receives incomplete, contradictory, or outdated information, it can produce a technically valid implementation that turns out to be wrong for our system.
That is why context should not simply be a set of documents we hand to the agent.
It must be part of the engineering architecture.
We need to build mechanisms capable of providing up-to-date information about:
- The structure and responsibilities of components.
- Contracts between services.
- Data models and relationships.
- Dependencies and versions.
- Current architectural decisions.
- Security rules.
- Functional and non-functional requirements.
- Change history and known issues.
We also need to control how that context evolves.
If a model works with an older version of a contract, or with an architectural decision that is no longer in force, it can generate changes that are incompatible with the current system.
This leads to another important concept: context must be versionable, traceable, and verifiable.
We cannot expect models to correctly understand a complex system if we ourselves are not able to represent its knowledge in a structured way.
6. Concurrency and consistency at enterprise scale
There is an additional problem that appears when we stop working with one agent and start working with dozens or hundreds of simultaneous generation processes.
Concurrency.
Imagine several agents working on the same repository:
- One modifies the authentication system.
- Another implements a new payments module.
- Another modifies the persistence layer.
- Another updates the APIs.
- Another generates integration tests.
Each one can produce a correct implementation on its own.
But that does not mean all the implementations are compatible with each other.
Dependency conflicts, incompatible changes, contract inconsistencies, and regressions can appear that only show up once the components are integrated.
That is why the software factory needs coordination and isolation mechanisms.
Some of the capabilities required will be:
- Isolated work environments.
- Version control and concurrent changes.
- Management of dependencies between tasks.
- Contract validation between components.
- Incremental integration.
- Regression testing across the whole system.
- Rollback strategies.
- Versioning of the models, prompts, tools, and contexts used.
The unit of work can no longer simply be a task assigned to an agent.
It has to be a controlled change within a much larger system.
7. The ability to self-evaluate and improve
There is a possibility I find especially interesting: building systems that not only generate and fix code, but can also improve their own generation processes.
Imagine a platform that records that a certain type of implementation produces recurring errors.
It can identify patterns, analyze the causes, detect problems in the context provided, review the instructions used, or propose changes to the validation mechanisms.
It can even experiment with different generation strategies and compare their results.
But there is an important distinction here.
A system that proposes improvements to its own operation should not automatically be authorized to modify its control mechanisms.
Otherwise, we could end up with a system that learns to evade its own constraints instead of improving its quality.
That is why improvements must be evaluated against independent criteria, compared with previous versions, and go through acceptance mechanisms before being incorporated into the production process.
The goal is to build a continuous improvement loop that learns from its results without losing the guarantees that justify its autonomy.
8. The new responsibility of developers and architects
All of this changes the nature of our professional responsibility.
If we are building systems that generate software, our job is not just to verify that a particular implementation is correct.
We have to guarantee that the process producing thousands of implementations is reliable.
That means making decisions about:
Architecture: defining the limits within which models and agents can operate.
Context engineering: making sure they have correct, sufficient, and up-to-date information.
Control design: establishing independent mechanisms to verify results.
Security: defining permissions, isolation, access policies, and limits on autonomy.
Observability: being able to reconstruct what the system did, why it did it, with what information, and what the results were.
Evaluation: measuring the real quality of implementations and the effectiveness of the control mechanisms.
Governance: establishing which decisions can be automated and which require human accountability.
There is something I consider fundamental: we cannot delegate responsibility for software quality simply because the code was produced by an artificial intelligence.
The system can generate, evaluate, correct, and deploy.
But we are the ones who must design the conditions under which it can do so.
9. How do we measure that it actually works?
If we want to take this model into enterprise environments, we need to stop measuring success by the amount of code generated or the speed of the agents.
The metric that matters is the reliability of the whole process.
Some metrics I consider fundamental:
| Metric | What we want to know |
|---|---|
| First-pass acceptance rate | What percentage of changes pass validation on the first attempt |
| Defect escape rate | How many defects reach production |
| Regression rate | How many changes introduce regressions |
| Human intervention rate | How many changes need human intervention |
| False acceptance rate | How many incorrect changes are accepted |
| Recovery time | How long the system takes to recover from a failure |
| Cost per accepted change | How much it costs to produce a change that actually meets the requirements |
There is one metric I find especially important: the false negative rate of our validation mechanisms, that is, the errors the system lets through.
We can have a factory that generates thousands of changes and correctly rejects most of the defective ones. But if some incorrect changes reach production undetected, we still have a problem.
That is why reliability cannot be measured only by the number of successful implementations.
We must also measure our ability to detect incorrect implementations.
And, fundamentally, we must establish what level of risk is acceptable depending on the type of system and the potential impact of a failure.
10. The future is not a developer who programs less. It is a developer who designs software production systems
My view is that this paradigm will define a large part of our profession over the coming decades.
We will increasingly work with models capable of generating code, architectures, documentation, tests, and even infrastructure proposals.
Our specialization will be increasingly tied to understanding those models, knowing their capabilities and limitations, and building systems that let us take advantage of them without losing control over the results.
This does not mean we will stop needing deep software engineering knowledge. We will need it to design the mechanisms that verify what these systems produce.
Nor does it mean that everything can be automated, or that we can mathematically guarantee there will never be errors.
It means we must stop thinking of code generation as an isolated activity and start designing complete processes for producing, validating, and evolving software.
The real innovation will not lie only in getting models that can write more code or solve more complex problems.
It will lie in our ability to build systems that can use those models to produce reliable software, continuously, and at a scale that would be hard to manage manually today.
Conclusion: programming the non-deterministic
I believe we are facing a new engineering paradigm.
We are moving to building deterministic systems of control, evaluation, and assurance around inherently non-deterministic generation processes.
And this opens a new specialization for developers and architects: designing the mechanisms that allow artificial intelligence systems to produce software autonomously, safely, and reliably.
The challenge is no longer writing code, defining every architectural decision, or directly managing every infrastructure component.
The challenge is how we build quality control over code we did not write, architectures we did not define, and infrastructure we do not own.
And, above all, how we guarantee that all of it works.
That is the engineering problem I believe lies ahead of us.
And, in my opinion, it is where software development is heading.