# Development with AI: contracts to obtain verifiable code

## AI does not eliminate design decisions

An AI tool can scan a repository, propose an API, write tests, and deploy code quickly. That speed does not by itself resolve what behavior the system needs. If the assignment is ambiguous, the result can be technically plausible and still solve another problem.

API-DD supports this way of working because it offers a perspective to make explicit the limit of each module: who consumes it, what messages it exchanges, what it guarantees and what it keeps hidden. It does not impose an architecture, a process or a division between human and AI work. It helps turn fuzzy decisions into a contract that you can both review.

The practical difference is in the success criteria. "Generate the code for this functionality" invites you to fill in the gaps by probability. “Implement this API and demonstrate these cases” reduces ambiguity and allows you to evaluate the result by its behavior.

## Think of the system as APIs

Before generating an implementation, it is advisable to describe the affected conversations. For each module it is enough to answer what is necessary:

| Decision | What clarifies for AI |
|---|---|
| Consumer | From what need the change should be designed |
| Messages | What operations, results and errors you can use |
| Guarantees | What behavior should be preserved |
| Limit | What files and modules belong to the change |
| Internal details | What decisions can the implementation freely make |
| APIs consumed | What collaborations exist and which can be substituted in a test |

This map reduces two common errors. The first is to extend the change with abstractions that no one asked for. The second is to copy details of the mechanism into the API: names of a library, persistence structures or internal steps that are then converted into a contract.

Not all gaps need to be filled before starting. Some are discovered when investigating the code or writing the first test. The important thing is to recognize what a pending decision is instead of letting a generated response make it accidentally.

## Test cases specify the result

A test case expresses an observable difference: an input, an action, and a result, state, or message that matters to the consumer. When executed, it also becomes feedback for the person and the AI.

A good set of cases covers the relevant differences without repeating the same rule with decorative data. For example:

```text
given an empty buffer with capacity one
when a value is added
then the value becomes available

given a full buffer
when trying to add another value
then it reports that it is full and keeps the first one
```

These cases say more than a generic “handle errors” request. They also leave freedom to use a list, a circular array, or another representation. The test protects the contract; it does not dictate the internal route.

Seeing the test fail before implementing provides simple evidence: the test can detect the absence of the behavior. Seeing it happen later confirms that that implementation satisfies the example. Neither signal alone proves that the design is complete, but together they are more reliable than accepting code because it looks correct.

## A useful distribution of work

The distribution changes depending on the risk and the context. As a starting point:

- people provide intention, priorities, constraints and decisions with product or architectural consequences;
- AI can investigate existing uses, summarize contracts, propose cases, prepare a first implementation and execute verifications;
- both review ambiguous decisions and the observable outcome.

Delegating a task does not mean delegating its acceptance criteria. The greater the impact of a decision, the more explicit it should be before turning it into code. In routine changes, tests and repository conventions can provide almost all of that context.

## A possible flow

This flow is an adaptive guide, not an API-DD condition:

1. Research current behavior, your consumers and local conventions.
2. Draw the affected modules and the APIs they offer or consume.
3. Separate confirmed decisions from open questions.
4. Make a case for each important observable difference.
5. Run the tests and verify that the new cases fail for the expected reason.
6. Implement the change without extending the contract unnecessarily.
7. Run the repository checks and check the diff as an API consumer.

The cycle can go back. A test that is difficult to write may reveal an awkward API; an implementation may show that a result was missing to be represented. Correcting the contract at that time is part of the design, not a failure of the process.

## What usually degrades the result

- Request an implementation without indicating the consumer or the expected behavior.
- Deliver so much irrelevant context that important constraints are lost.
- Allow AI to silently invent names, bugs or compatibility.
- Try helpers, internal calls or data structures instead of the API.
- Modify the test until it accepts the generated code, without checking which guarantee changed.
- Repeat equivalent cases and confuse test volume with decision coverage.
- Terminate the change without executing the actual project checks.

The solution is not to write a huge prompt. It is to deliver selected context: contract, cases, limits, conventions and verification commands. The appendices provide brief templates for [research and agree the contract](prompt-01-api-contract-dd.md) and for [implement and verify it](prompt-02-api-implementation-dd.md).

## Example in Go

The test describes the contract of a buffer with capacity one. The implementation is purposely not included: it could be written by a person or generated with AI and would still be evaluated by the same observations.

```go
import "testing"

func TestBoundedBufferContract(t *testing.T) {
    buffer := NewBoundedBuffer[int](1)

    if got := buffer.Push(7); got != Stored {
        t.Fatalf("expected Stored, got %v", got)
    }
    if got := buffer.Push(8); got != Full {
        t.Fatalf("expected Full, got %v", got)
    }

    value, ok := buffer.Pop()
    if !ok || value != 7 {
        t.Fatalf("expected first value, got %v, %v", value, ok)
    }
}
```

The case establishes capacity, response and conservation of the first value. It does not set auxiliary classes, number of calls or internal structure. That freedom allows the AI ​​to propose an implementation and the team to change it later without altering the contract.

## What an expected code improvement

Thinking about APIs and test cases narrows the solution space without choosing the mechanism in advance. AI is given meaningful names, concrete boundaries, and executable examples; the team receives an objective way to review the result.

The benefit is not that all generated code is correct. It is no longer evaluated only by its appearance: it must respect the contract, exceed the agreed cases and preserve the internal freedom of the module.

> **AI speeds up a proposal; the contract and the tests let us decide whether it works.**
