Interface-Driven Development
How I think about building software with agents
The premise
When I begin designing a system, I usually begin with some type of premise.
Maybe the premise is that an agent should be able to perform a particular task. Maybe a package should expose a reusable capability. Maybe a workflow that currently works for one company should be general enough to work for many companies and builders.
But the premise is only the beginning, because then I start questioning it.
What has to be true for this to work? What does the agent actually need to know? Which decisions belong inside the system, and which decisions should be exposed to the caller? What happens when the implementation changes? What happens when another team uses it? What happens when the original engineer is no longer there to explain it?
Eventually those questions lead me toward an interface.
I use the word interface broadly. It might be an Effect service, a function, a schema, a command, a package entry point, a typed error, or a small set of operations through which someone can understand and use a system.
The specific form matters less than what the interface accomplishes.
A good interface carries the model of the system. It explains what can be done, constrains what cannot be done, and gives the engineer or agent enough information to take the next valid step without understanding the entire implementation behind it.
That is what I mean by Interface-Driven Development.
The idea is not that we should define an interface at the beginning and then blindly implement it. The interface itself is part of the reasoning process. I question it, prototype alternatives, observe where each one fails, and keep changing it until the shape of the system begins to make sense.
The interface is not simply an output of the design.
It is where I discover the design.
The interface
The document is not the interface
An AI coding agent usually enters a repository through text.
So our natural response has been to give it more text: instruction files, architecture summaries, conventions, examples, lists of exceptions, and explanations of how the repository is supposed to work.
Those documents can help. I use them, and I do not think they should disappear.
But think about what we are asking the agent to do.
We are asking it to read a rule in one document, remember that rule while inspecting source files, recognize the exact moment when the rule applies, and then translate the rule into the correct action. Meanwhile, search results, tool output, implementation details, and unrelated instructions are all competing for the same context.
Humans have the same problem, although experienced engineers often hide it well. They fill gaps using institutional knowledge, previous conversations, intuition, and memory. An agent is less capable of silently recovering all of that missing context.
That makes agents useful as a test of the system itself.
If an agent has to read a long document and remember a paragraph from twenty minutes ago before it can safely call a function, then the system may have documentation, but the interface is not carrying enough of the model.
The alternative is not to eliminate documentation. The alternative is to move the enforceable parts of the explanation into the place where the work happens.
A specification can explain why publishing requires link validation.
The interface can make publishing depend on link validation.
The document states the reasoning. The interface carries the constraint.
Mature systems need both.
What I mean by an interface
When people hear Interface-Driven Development, they may think about API-first development, user interfaces, or defining TypeScript interfaces before implementing a class.
Those ideas overlap with what I am describing, but I mean something wider.
An interface is the boundary through which an engineer or agent forms a model of a capability and acts on it.
Suppose an agent needs to publish an article. The implementation may involve file loading, Markdown parsing, schema validation, reference checking, rendering, persistence, and deployment.
The agent should not necessarily have to understand that implementation graph.
It needs an interface that answers a smaller set of questions:
- What can I do?
- What input is required?
- What constraints apply?
- What happened?
- What can I do next if it fails?
In Effect, that might look something like this:
import { Context, Effect, Schema } from "effect"
const PublishInput = Schema.Struct({
slug: Schema.NonEmptyString,
})
class DraftMissing extends Schema.TaggedErrorClass<DraftMissing>()(
"DraftMissing",
{ slug: Schema.String },
) {}
class ReferenceBroken extends Schema.TaggedErrorClass<ReferenceBroken>()(
"ReferenceBroken",
{ href: Schema.String },
) {}
interface PublishingService {
readonly inspect: Effect.Effect<ReadonlyArray<string>>
readonly publish: (
input: typeof PublishInput.Type,
) => Effect.Effect<
PublishedArticle,
DraftMissing | ReferenceBroken
>
}
class Publishing extends Context.Tag("Publishing")<
Publishing,
PublishingService
>() {}Notice what the caller does not need to know.
It does not need to know which Markdown parser is used, where drafts are stored, how reference validation is implemented, or how the final page is rendered.
That information still exists, but it remains behind the boundary until a task actually requires it.
The interface exposes the stable capability while hiding decisions that the caller should not need to carry.
That is a deep interface: a relatively small boundary that contains substantial behavior and substantial reasoning.6, 8
The interface is a prototype of the model
Sometimes I know an interface is wrong before I can explain exactly why it is wrong.
The function might technically work, but the name causes people to think about it incorrectly. The input might be valid, but it exposes a decision that belongs inside the implementation. The service may contain operations that do not belong to the same concept.
At that point I trust the feeling, but I do not treat the feeling as proof.
I build another version.
I change the name. I move an operation. I combine two functions. I split one function apart. I make the input narrower. I expose a typed failure instead of a generic exception. I try using the interface from the perspective of an engineer who did not design it.
Now that agents can write and navigate software, I can also give the interface to an agent and see what it does.
Does it find the correct operation?
Does it understand the input?
Does it bypass the intended path?
Does it open ten implementation files because the interface does not explain enough?
Does an error reduce uncertainty, or does it force the agent into another broad search?
These are not only agent questions. They are design questions.
The agent simply makes the answers easier to observe.
This is why prototyping is so important to my process. Reasoning can only simulate failures that already exist somewhere inside the model I have built. A prototype creates contact with failures outside that model.
The interface may not compose the way I expected. The abstraction may be too specific. The supposedly generic operation may depend on an assumption that only holds inside the current company. Two concepts that looked related in theory may change for completely different reasons in practice.
The prototype introduces information that thought alone could not produce.
Then I update the model.
Agents expose how much the system expects us to remember
A great deal of software works because the engineers around it remember things the software does not express.
They remember that one function should never be called before another. They remember which database state is technically valid but operationally dangerous. They remember that a generic-looking service only works for one tenant type. They remember why an oddly named field cannot be renamed.
That can work while the team is small and the original engineers are still present.
But now add more engineers.
Then add more teams.
Then add agents that did not participate in the original design discussions.
The system begins to scale across more than traffic and data. It scales across people, context, ownership, coordination, and memory.
This is where interface design becomes architectural.
A good interface reduces how much of the system any one actor has to hold in its head. It preserves important distinctions. It makes the supported path easier than the unsupported path. It communicates enough of the model that a new engineer or agent can act without reconstructing the entire history of the codebase.
The goal is not to make the system simple by hiding everything.
The goal is to decide which complexity belongs behind the boundary and which information must cross it.
Hide too little and the caller has to understand the implementation.
Hide too much and the caller cannot reason about consequences.
The interface has to expose the right model.
The interface walk
I think of an agent working through a repository as performing an interface walk.
It begins with a task and then moves through a sequence of increasingly constrained decisions.
1. Locate
The agent finds the package, service, command, or module that owns the capability.
Names and package boundaries matter here. If five packages appear to own the same operation, the agent begins with ambiguity.
2. Inspect
The agent looks at the operations exposed by that boundary.
It should see capabilities, not an inventory of implementation details.
3. Constrain
The agent decodes the input schema, types, preconditions, and allowed states.
The interface should eliminate invalid interpretations before execution.
4. Execute
One operation performs the work.
The supported path should not require the agent to manually reproduce the implementation steps hidden behind the interface.
5. Recover
If the operation fails, the result should explain the bounded next actions.
A generic error such as Invalid input expands the search space. A tagged error containing the invalid field, expected state, and allowed alternatives reduces it.
For example:
type PublishError =
| {
readonly _tag: "DraftMissing"
readonly slug: string
}
| {
readonly _tag: "SchemaInvalid"
readonly issues: ReadonlyArray<Issue>
}
| {
readonly _tag: "ReferenceBroken"
readonly href: string
}Each step in the walk should reduce uncertainty.
That is the real standard I care about. Not whether the repository contains a large amount of information, but whether the next decision reveals the information relevant to that decision.
The method
Start from decisions, not files
Repositories are usually organized around implementation structure.
There are folders for services, schemas, repositories, handlers, utilities, components, and infrastructure. Those structures can be reasonable for maintaining the code, but they do not necessarily describe the work someone is trying to perform.
An agent does not usually begin with the goal of editing a file.
It begins with a decision or task:
- Publish an article.
- Create an agent.
- Resume a failed run.
- Validate a configuration.
- Add a tool.
- Change an authorization rule.
So when I design an interface, I begin by asking what decisions the worker has to make.
Which decisions should remain with the caller?
Which decisions can the system make safely?
Which decisions require domain context?
Which ones are implementation details that should not escape the boundary?
The interface should be shaped around those decisions rather than mirroring the directory tree beneath it.
A directory tree is a map of the implementation.
An interface should be a map of valid action.
Put the truth at the boundary
When a rule can be enforced, I would rather encode it than depend on someone remembering it.
Identifiers belong in schemas and types.
State transitions belong in operations that make invalid transitions impossible or explicit.
Authorization belongs in actual authorization boundaries.
Failure categories belong in typed results.
Important workflows belong in commands or services that preserve the required sequence.
This does not mean that every idea must become a type. Some reasoning is contextual. Some decisions depend on business judgment. Some intent cannot be reduced to an executable rule.
That information belongs in specifications, architecture records, and explanations.
But once the system knows enough to enforce something, I want that truth represented at the interface.
Otherwise every engineer and every agent has to recreate the rule independently.
Make the correct path easier than circumvention
When capable engineers repeatedly bypass a step, I take that as feedback about the system.
Maybe the step is too manual. Maybe it requires too much context. Maybe it does not explain why it exists. Maybe the incentives reward skipping it.
Agents will expose the same problem even more directly. They tend to follow the path that is most visible and executable.
So if the safe path requires reading three documents and calling five functions in the correct sequence, while the unsafe path requires importing one internal module, the design is effectively encouraging circumvention.
The answer cannot only be another instruction saying not to bypass the process.
The supported interface has to become the easier path.
A canonical operation should validate, execute, and report. If a step is necessary and repeatable, the system should perform it or require it structurally.
The interface should turn the process into the default behavior.
Test the interface, not only the implementation
An interface can compile, have complete unit coverage, and still be difficult to use.
So I believe we should test the walk itself.
Give a fresh agent a task and the repository root. Do not preload the architectural explanation. Observe what happens.
Which files does it open?
How many alternative paths does it inspect?
Does it find the intended boundary?
How many invalid operations does it attempt?
Does it understand the failures?
Does it complete the task correctly?
Then compare that against another version of the interface or against a document-led baseline.
The point is not merely to reduce tokens or file reads. An agent that opens fewer files but makes a shallow, incorrect change is not an improvement.
Semantic correctness comes first.
But after correctness, the path matters. A system that allows a worker to reach the correct result through a smaller, clearer decision space is easier to maintain and easier to scale across engineers and agents.
I would record at least:
- task success
- tests passed
- files inspected
- invalid attempts
- recovery attempts
- tokens consumed
- elapsed time
- whether the intended interface was used
The goal is not to prove that every repository needs a formal agent interface.
The goal is to make interface quality observable.
A practical method
This is roughly how I approach Interface-Driven Development.
Begin with a premise. State what you believe the system should allow. Do not treat it as true yet.
Question the premise. Ask what assumptions it depends on, who the users are, what changes at different forms of scale, and where the design might fail.
Decompose the system. Find the concepts, decisions, boundaries, state transitions, and failure categories. Do not begin by copying the existing folder structure.
Prototype competing interfaces. Try different names, input shapes, service boundaries, commands, and error models. Use the alternatives. Give them to engineers. Give them to agents. See where each one creates confusion or circumvention.
Let failure update the model. When a prototype fails, do not immediately patch the symptom. Ask what the failure says about the abstraction. Was the boundary wrong? Was the operation too generic? Did the caller need information the interface concealed? Did the interface expose a decision the implementation should own?
Encode the stable truth. Once the model begins to settle, move it into types, schemas, operations, tests, and failures. The system should remember the parts that should no longer depend on human memory.
Run the design forward. Ask what happens with more engineers, fewer engineers, more teams, more agents, more data, greater complexity, and different ownership. Scale is any dimension along which the assumptions can stop holding.
Test the interface walk. Give the system to someone who did not design it and see whether the interface teaches the correct next move.
Convergent evidence, not proof
I want to be careful about the evidence behind this approach.
No study directly validates Interface-Driven Development with agents as the methodology I am describing here.
What exists is a collection of findings that point in the same direction.
Research on long-context language models shows that the location of relevant information can affect whether that information is used reliably.1 Repository-level coding systems often improve through iterative retrieval rather than loading everything at once.4 Constrained operations make available actions explicit.3 Research on API usability shows that interface choices measurably affect developer performance.10
Together, these findings support a hypothesis:
Agents should perform better when the important decisions in a repository are exposed through small, discoverable, constrained, and testable interfaces.
That is not a verdict.
It is something we should test.
| Source | Observed result | Design implication |
|---|---|---|
| Lost in the Middle1 | Relevant information may be used less reliably when buried within large contexts. | Do not depend on a single rule being remembered from a large instruction file. |
| RepoCoder4 | Iterative retrieval and generation improved repository-level completion in its evaluation. | Make the next relevant boundary easy to locate. |
| Agentless3 | A comparatively narrow localization, repair, and validation pipeline was competitive on SWE-bench. | Prefer bounded stages over unconstrained repository wandering. |
| SWE-bench11 | Real issue resolution requires reasoning across repository-level context. | Evaluate interfaces using real changes rather than isolated code generation. |
| Myers and Stylos10 | API design choices affect programmer usability and performance. | Treat agent-facing interfaces as usability design. |
Context-engineering guidance also emphasizes compact, high-signal context and just-in-time retrieval.2 Function calling provides machine-readable operation schemas, although a schema alone does not guarantee good tool selection or correct execution.5
The interface still has to be designed well.
Where this breaks
Interfaces can fail in several ways.
An interface can hide information required for an unusual task.
A schema can freeze an abstraction before the domain is understood.
A generic facade can become disconnected from the implementation beneath it.
A package can expose so many operations that the interface becomes another context problem.
Metrics such as fewer files opened can encourage shallow fixes.
An agent can still choose the wrong interface.
And an interface that appears safe is not a replacement for a real security boundary.
These failures matter because the methodology should not become another rule that survives after it stops helping.
Preserve an escape hatch to the implementation.
Keep interfaces close to the capabilities they expose.
Version them when their contracts change.
Test uncommon paths.
Avoid creating a universal interface that tries to represent every possible task.
Do not hide decisions the caller genuinely needs to make.
Most importantly, let the complexity earn its place.
A small exploratory repository may need nothing more than clear source code and a concise README. A stable system with recurring operations, multiple teams, and many agents may justify a much stronger interface layer.
The method should respond to the system rather than becoming ceremony imposed on it.
The interface is where the system explains itself
Interface-Driven Development is ultimately how I think through software.
I start with a premise and question it. I decompose the system until I can see the decisions and boundaries. I prototype interfaces because the prototypes reveal failures my reasoning could not predict. I compare alternatives until one begins to carry the model clearly.
Then I move that model into the system.
The types carry constraints. The schemas carry valid input. The operations carry the supported workflow. The failures carry the next move. The tests carry the promise.
Agents make this process more important because they cannot depend on all of the context, intuition, and organizational memory that experienced engineers quietly use.
But they also make the process easier to evaluate.
Give an agent a task and see whether the system teaches it how to act.
If it cannot find the capability, the ownership may be unclear.
If it repeatedly takes the wrong path, the incentives may be wrong.
If it needs the entire implementation graph to use one operation, the boundary may not be deep enough.
If it can complete the task safely through a small interface, then some important part of the model has successfully moved out of our heads and into the software.
That is the kind of system I want to build.
Not one that expects people and agents to remember everything, but one whose interfaces help them reason about what comes next.
References
- Liu et al. (2023). “Lost in the Middle: How Language Models Use Long Contexts.”
- Anthropic (2025). “Effective context engineering for AI agents.”
- Xia et al. (2024). “Agentless: Demystifying LLM-based Software Engineering Agents.”
- Zhang et al. (2023). “RepoCoder: Repository-Level Code Completion Through Iterative Retrieval and Generation.”
- OpenAI. “Function calling.”
- Parnas (1972). “On the Criteria To Be Used in Decomposing Systems into Modules.”
- Liskov & Zilles (1974). “Programming with Abstract Data Types.”
- Ousterhout (2018). A Philosophy of Software Design.
- Sweller (2010). “Element Interactivity and Intrinsic, Extraneous, and Germane Cognitive Load.”
- Myers & Stylos (2016). “Improving API Usability.”
- Jimenez et al. (2023). “SWE-bench: Can Language Models Resolve Real-World GitHub Issues?”