The Real Declaration Is Tests and Acceptance Criteria

Published: 2026-11-02 5,078 words 16 min read

Recently I have been relearning Java in English. While typing through the code on classes and objects in the official tutorial, I also sorted out the difference between imperative and declarative programming.

Imperative vs. Declarative: Separating “What You Want” from “How to Get It”

Java is a classic imperative language. Suppose you write a basic Animal class so an animal can eat and drink, with cats eating cat food and dogs eating dog food. The code has to spell out every step line by line:

Java
UTF-8|15 Lines|
public class Animal {
    private String type;

    public void eat() {
        if ("cat".equals(type)) {
            System.out.println("Eat cat food");
        } else if ("dog".equals(type)) {
            System.out.println("Eat dog food");
        }
    }

    public void drink() {
        System.out.println("Drink water");
    }
}

First check whether the animal is a cat or a dog; if it is a cat, feed it cat food, if it is a dog, feed it dog food, and call drink() for water. The programmer writes every step of how the work gets done. If a branch is missing or the order is wrong and the program misbehaves, the fault naturally lies with the programmer.

Terraform’s HCL and the workflow DSL exported by Dify are declarative. To get a job done in Terraform, such as creating a server, the configuration looks like this:

hcl
UTF-8|4 Lines|
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
}

Nothing here says which API to call first, how many seconds to wait before retrying on failure, or how to verify that the machine has booted. I only write what I want and leave how to get it to the platform. A Dify DSL file works the same way: it only declares which nodes exist in the workflow, how they connect, and which model they use, while Dify’s engine handles scheduling and execution.

As long as what you want is stated clearly, keeping the execution stable is the platform’s job. Separating the goal from the implementation is a form of decoupling.

Why Coding with an LLM Is Not the Same as Declarative Programming

On the surface, coding with an LLM also looks declarative. You type a prompt into a chat box asking the model to “write an Animal class with eat and drink methods, where cats eat cat food and dogs eat dog food.” You do not type the conditional branches yourself, and you seem to be stating only what you want, much like making a wish.

Yet there is a critical difference between the two.

Traditional declarative systems can decouple humans from implementation details because the underlying execution is deterministic. Give the same Terraform configuration or the same Dify DSL to the same version of the platform, and the parsed steps and resulting state are fixed. You can afford to write only what you want because the platform turns the “how to get it” part into a deterministic pipeline.

Make the same wish to an LLM, however, and you get a different result every time. On one run it writes the cat food and dog food branches properly; on the next it misses a null check; on the one after that it changes the method signature. When the underlying execution is nondeterministic, merely writing down a wish does not automatically produce a stable outcome.

A Better Analogy Is Kubernetes

If a single generation is not deterministic, can coding with AI still become declarative? A better analogy here is Kubernetes.

In Kubernetes, the physical environment is full of uncertainty too: machines crash, containers die, and networks jitter. Yet users still only need to declare in YAML, “I want 3 replicas.” Kubernetes makes this work not because containers never fail once the YAML is submitted, but because a controller runs continuously in the background:

Text
UTF-8|7 Lines|
Declare desired state (3 replicas)
  ↓
Controller checks state (only 2 running)
  ↓
Spot diff and retry (start 1 more replica)
  ↓
Loop until reality matches desired state

You declare the desired state, and the controller keeps checking the current state and retrying until reality catches up. Even when a single execution is unstable, as long as this control loop keeps running, the final state converges.

Applied to Agents: Declaration, Controller, and IDE

Map this structure onto today’s Agent programming, and each role falls cleanly into place:

Kubernetes ConceptCounterpart in Agent ProgrammingRole in the Loop
Desired StateTests, type checks, acceptance criteriaDefine what “done right” means so the machine can verify it repeatedly
ControllerClaude Code, CodexInspect errors, edit code, and retry until convergence
Control Panel / ConsoleCollaboration tools like RaftAn IDE where humans assign tasks, monitor multiple Agents, and review results
Operation Without a ControllerWriting a prompt with no checksRun once and hope for the best; pure wish-making

Here, the real “declaration” is not the prompt that kicks off a task, but the tests and acceptance criteria. The prompt is more like the initial trigger sent into the system; only tests, type checks, and explicit acceptance conditions can serve as a desired state that the machine can verify again and again.

Coding Agents like Claude Code and Codex play the role of that retrying controller. They write a version of the code, run the tests, read the errors, and go back to revise until all tests turn green and the page meets requirements. A single model output can still go wrong, but the control loop pulls unstable generation back toward deterministic acceptance criteria.

When development shifts to “declaring acceptance criteria + letting the controller converge,” the IDE people use every day changes with it. Tools like Raft no longer design their interface around typing every line of code by hand. Instead, they place channels (Chat), tasks (Tasks), and files (Files) in a single workspace and connect different Agents such as Claude Code and Codex to collaborate:

The Raft workspace interface with Chat, Tasks, and Files tabs, where humans and Agents collaborate around tasks in the same channel
Raft puts channels, tasks, and files in one workspace so different Coding Agents can claim and advance tasks. Screenshot from the official Raft website

What humans do inside it is no longer typing out every if-else branch for the program, but defining task goals, checking which Agent is stuck, and reviewing the final deliverables. In that sense, it is an IDE built for Agents.

Conversely, if you only write a prompt with neither tests nor checks to compare reality against the desired state, Claude Code or Codex will stop after a single pass. No one knows whether the result is actually right, and no mechanism forces it to converge. That is not declarative programming; that is just making a wish.

Sources