Maciej Soliński
Back to the blog

AI-Generated Code: What to Check Before Accepting It

A practical review workflow for AI-generated Java code: requirements, correctness, boundaries, dependencies and maintainability.

AI-Generated Code: What to Check Before Accepting It

An AI assistant can produce a convincing implementation in seconds. It may include descriptive method names, comments and tests. None of those things, on its own, proves that the code solves the right problem.

My preferred approach is to treat the output as a proposed change. It needs a clear requirement, a reviewable diff and evidence that it behaves correctly. The responsibility for accepting it remains with the developer.

Make the task small enough to verify

“Improve this service” leaves too much room for interpretation. A more useful request identifies the current problem and the limits of the change:

Fix the duplicate notification sent after an order is confirmed.
Keep the public API and database schema unchanged.
First identify the path that sends the notification twice.
Add a regression test, then propose the smallest fix.
Explain what the test proves and what remains unverified.

This is a workflow suggestion, not a guarantee about an assistant’s behaviour. Its advantage is that the result can be assessed against a concrete outcome. A broad rewrite is harder to review because the fix gets mixed with unrelated design choices.

Use only the repository material and data that the organisation allows the assistant to access. Synthetic examples are often enough to explain a failure without copying production records or credentials.

Check the requirement separately from the implementation

Before reading the generated code in detail, write down a few expected outcomes. For an order endpoint, these might include a valid request, an unknown order, a request for another tenant’s order and a repeated command.

This separates the source of truth from the proposed solution. If both the implementation and its tests are generated from the same mistaken assumption, the tests may pass while the requirement is still violated.

For a monetary calculation, determine the expected value independently. For a query, inspect whether it selects the intended records. For a retry, ask what happens if the first request succeeded but its response was lost.

Read across boundaries

The most expensive mistakes are not always inside the method under review. A local change can affect authorisation, transaction boundaries or the API consumed by another system.

Consider this illustrative fragment:

return repository.findById(orderId)
        .orElseThrow(() -> new OrderNotFoundException(orderId));

It looks straightforward. In a multi-tenant application, it is safe only if access control is enforced elsewhere in a reliable way. The fragment itself does not establish that the current caller may read this order. Passing authentication is not the same as authorisation for a particular record.

The OWASP authorisation guidance is a useful reference for access checks. The review should confirm how those checks apply to this application, rather than assuming that a generated controller includes them.

Verify the APIs and dependencies

Code can be conceptually reasonable and still use a method that is unavailable in the project’s version. Check imports, dependency versions and the relevant official documentation.

Avoid adding a library just to make a small generated snippet compile. First ask whether the existing stack already provides the needed capability. Each new dependency adds update work and another compatibility boundary.

For a Spring change, inspect how the code is invoked. An annotation can look correct while being bypassed by a call that never crosses the expected proxy. For concurrent code, identify the actual executor, timeout and resource limits. Comments such as “thread-safe” are claims to verify, not evidence.

Ask for failure cases, then challenge them

A useful follow-up request is to list the assumptions and propose inputs that would break the solution. It can expose questions that were missed during the first pass, but it remains another generated answer. Important cases still need independent checking.

Test boundaries rather than only examples that mirror the code: an exact limit, an empty collection, a timeout, a duplicate request, a failed commit or access across tenants. Not every change needs all of these tests. Choose the ones related to the operation being changed.

Use a short acceptance checklist

Before accepting the patch, I would check:

  • The change implements the agreed behaviour without unrelated edits.
  • A clean build succeeds with the project’s actual JDK and dependencies.
  • Relevant tests exercise both the normal path and the important failure case.
  • Access control, transactions and resource limits still make sense.
  • The code can be explained without relying on the assistant’s summary.

Record what was actually run. “Tests added” is different from “tests passed”, and a passing unit test is different from an integration test against the real database engine.

AI can reduce the time needed to draft an implementation or explore alternatives. It does not remove the need to understand the result. The practical goal is to spend less time typing routine code while keeping enough attention for the decisions that determine correctness.