Agentic Coding and Software Architecture from First Principles
Why good architecture can reduce token consumption, improve the code agents produce, and make human engineering judgment more valuable.
One of my projects became a counterexample to a seductive idea: that software engineering could be replaced by a sufficiently persistent conversation with an AI agent.
The code was almost entirely AI-generated. Human intervention concentrated on prompts rather than code-level review or architectural decisions. There was no adequately maintained AGENTS.md establishing a reliable map of the system, its boundaries, and its development rules. When the agent produced an unsatisfactory result, the response was usually another instruction, another correction, another attempt.
The repository grew, but its internal structure did not become correspondingly clearer. Eventually, agents consumed substantial amounts of tokens trying to reconstruct how the system worked. They struggled to extract consistent information from code that previous agent sessions had generated. The project became internally confused, expensive to change, and ultimately abandoned.
My diagnosis is not that AI cannot write software. It is that we had delegated implementation without establishing a reliable mechanism for preserving architectural coherence.
We were optimizing the conversation while neglecting the system the conversation was about.
In other production projects, I took a different approach: explicit business boundaries, consistent implementation patterns, typed contracts, and executable checks. Domain-driven design was one useful example—not the objective itself.
These experiences led me to a broader argument:
Good architecture can make an AI agent cheaper and more reliable by reducing how much it must discover, how many decisions it must improvise, and how long its mistakes remain undetected.
To explain why, we need to start below frameworks, prompts, and design patterns.
A note on evidence: the project accounts are anonymized engineering observations, not controlled experiments. The examples below are simplified. Research findings are cited separately; none establishes a universal token-saving percentage for my projects or for DDD.
1. What are the first principles of agentic coding?
First-principles reasoning does not mean attaching equations to familiar advice. It means identifying the essential properties of a problem, then deriving design choices from those properties.
“Use DDD” is not a first principle. Neither is “write a comprehensive agent instruction file.” Both are possible interventions whose value depends on what they accomplish.
For an agent modifying an existing software system, I start with five basic premises.
The objective is a valid system change, not more code
A request to add subscription cancellation is not fulfilled merely because a function named cancelSubscription exists. The system must acquire the requested behavior without violating the constraints that still apply.
We can express the objective as:
$$
S_1 = T(S_0, A), \qquad G(S_1) \land I(S_1)
$$
Here, $S_0$ is the starting system, $A$ is a sequence of actions, and $S_1$ is the resulting system. $G$ represents the requested behavior; $I$ represents the invariants and compatibility requirements we intend to preserve.
The relevant system includes more than source files. It may include database schemas, dependency versions, API contracts, migration procedures, and deployed behavior. Some constraints also apply during the transition: a correct final schema does not excuse a rollout that temporarily breaks live clients.
This immediately changes the optimization target. We are not maximizing code output. We are seeking an acceptable change at an acceptable total cost.
The agent acts on observations, not omniscience
An agent does not begin with a complete, current model of a repository. It reads files, searches symbols, inspects contracts, executes commands, and interprets feedback.
Its decisions therefore depend on a working representation of the system, not the system in its entirety:
$$
a_t = \pi(G, \widehat{S}_t)
$$
The notation is a conceptual model, not a claim about the model’s internal implementation. $\widehat{S}_t$ represents the task-relevant understanding assembled from available evidence.
When that understanding omits a payment callback, a transaction boundary, or an old compatibility requirement, a locally plausible edit can be globally wrong.
The agent does not need to understand everything. It needs enough accurate information to justify this particular change—and a way to recognize when that information is insufficient.
Acquiring and processing information has a cost
Every search result, file excerpt, instruction, and execution log competes for attention and consumes some combination of tokens, time, and tool execution.
A simple accounting model for a run with $m$ model invocations is:
$$
T_{\text{run}} = \sum_{k=1}^{m}\left(T_{\text{input},k}+T_{\text{output},k}\right)
$$
Repeated context can be counted again across invocations. Actual monetary cost also depends on caching and pricing; elapsed time includes tool execution and waiting. These quantities should not be conflated.
There is also a distinction between fitting information into a context window and using it effectively. Lost in the Middle demonstrated position-sensitive performance on long-context question answering and retrieval: relevant information in the middle could be used less effectively than information near the ends. That result concerns the models and tasks studied, not a permanent limit on every future model. It nevertheless cautions against treating context capacity as equivalent to effective comprehension.[1]
The engineering question becomes: what information must the agent process, and what can the system safely let it ignore?
Implementation requires choosing among alternatives
A feature can often be implemented in several places and through several abstractions. The agent must decide which choices fit the repository.
Should a payment callback update a subscription directly? Invoke an application service? Publish an event? Schedule a job? Which component owns idempotency?
Without explicit boundaries, those questions must be answered repeatedly during implementation. Architecture can settle recurring decisions in advance, leaving the agent to solve the genuinely new part of the task.
Reducing the available choices does not mathematically guarantee correctness. An inappropriate architecture can exclude the right solution. The useful objective is narrower: eliminate unnecessary decisions and make unsuitable choices detectable without preventing legitimate change.
A generated patch is a hypothesis, not evidence of success
Confidence in a response is not evidence that a migration is safe or a concurrency invariant holds. The patch needs evaluation against something beyond its own explanation.
This leads to an iterative structure:
Observe
↗ ↘
Interpret feedback Form a working model
↑ ↓
Execute checks Choose an action
↖ ↙
Apply the change
ReAct explored the value of interleaving reasoning and actions that obtain information from an external environment. It offers a useful foundation for this interaction pattern, although it does not establish that any particular software architecture is superior.[2]
Feedback is still incomplete. A passing test suite establishes neither universal correctness nor the correctness of the requirements. A test written from the same misunderstanding as the implementation can confirm the wrong behavior.
The loop therefore needs meaningful oracles: reviewed acceptance criteria, independent regression tests, contracts, and appropriate operational evidence. It also needs stopping rules and escalation, not the assumption that another iteration always improves the result.
2. Architecture is context engineering at repository scale
These premises suggest three places to intervene: improve the agent’s observations, reduce unnecessary implementation choices, and strengthen the evidence used to accept changes.
Software architecture can affect all three.
This is not an entirely new purpose for architecture. Parnas’s classic paper on modular decomposition argued that the criteria used to divide a system matter to its comprehensibility and flexibility. Information hiding is about preventing a consumer from having to depend on internal design decisions—not merely dividing a large file into smaller files.[3]
The agentic interpretation follows naturally: a boundary that lets a human understand one operation without reconstructing the whole system may also reduce the context an agent needs for that operation.
Consider the difference between discovering how subscription activation works by tracing scattered updates, and finding a documented operation that explicitly states its preconditions, effects, and failure modes. The second representation concentrates the information needed for a decision.
That is what I mean by context compression. It is not shorter variable names, fewer comments, or a statistical claim about Shannon entropy. It is preserving the semantics relevant to a task while making unrelated implementation details unnecessary to inspect.
The compression fails when the abstraction lies. A repository method called save is not useful compression if callers must inspect its implementation to learn whether it opens a transaction, emits an event, or invalidates a cache. Those effects must either belong elsewhere or be part of an explicit contract.
This gives architecture a practical test:
Can a reader make a correct local change from the public contract and nearby evidence, or must they repeatedly reconstruct distant implementation details?
Agentless provides adjacent empirical evidence for the value of deliberate localization. Its approach separates locating relevant code from generating and validating repairs rather than leaving every step to unrestricted exploration. Its benchmark results demonstrate that a simpler, structured workflow can be competitive and economical. They do not directly measure the effect of repository architecture.[4]
My inference is that code organization and retrieval workflow should reinforce each other. A navigation strategy works best when the repository contains meaningful places to navigate to.
3. What changed in my production projects
From technical categories to business ownership
In one of my production projects, the important shift was toward business-oriented ownership. Billing, subscriptions, product definitions, and notifications became explicit areas of responsibility rather than concepts reconstructed from cross-cutting technical folders.
Within a business area, the structure distinguished domain behavior, application orchestration, infrastructure adapters, and inbound interfaces. A simplified representation is:
subscriptions/
├── domain/ # Business definitions and invariants
├── application/ # Use cases and orchestration
├── infrastructure/ # Persistence and external adapters
└── interfaces/ # HTTP handlers and other entry points
The folder names were not the achievement. The achievement was deciding where behavior belonged and making that decision consistent.
For an activation rule, the agent could start with the subscription model and its tests. For a database mapping change, it could start with the persistence adapter. For an API representation change, it could start with the interface contract. Cross-cutting changes still required broader analysis, but a local task no longer needed to begin as an unrestricted architectural investigation.
DDD was useful here because its bounded contexts establish where a model applies, while its tactical patterns provide ways to express identity, behavior, and consistency. These are modeling tools, not a mandatory directory template.[5]
I sometimes describe the evolution as “MVC to DDD,” but that phrase needs qualification. MVC and DDD are not mutually exclusive alternatives: MVC concerns presentation organization and can sit on top of a domain model. What changed in my project was a controller-and-service-centered organization with diffuse business ownership, not the mere existence of controllers.[6]
From local organization to explicit package boundaries
In another production project, I extended the same principle across a monorepo. Deployable applications consumed reusable business capabilities; the business core did not depend on an API process or worker entry point.
Repository contracts expressed the capabilities needed by the business layer. Persistence implementations supplied them. Public package entry points distinguished supported integration surfaces from internal files. Cross-context collaboration used deliberately narrow contracts rather than convenient imports into another area’s internals.
This addresses a different source of wasted reasoning. An agent should not have to rediscover the permitted dependency direction every time it adds a feature.
It also changes what “consistent output” means. New code has an established place, an established dependency pattern, and an established way to report failures. There is less need to invent a new helper, manager, or service convention for each task.
My practical experience favored this explicit structure. But the defensible claim is about the mechanism and the workflow, not an invented numerical improvement. I did not run a controlled experiment proving that changing the architecture alone reduced token consumption by a specific percentage.
4. Better models reduce guesswork; executable rules reject mistakes
The value of architecture becomes clearer when it changes what information the code actually expresses.
A state machine is more informative than a status field
Consider a subscription lifecycle. A string field tells the agent almost nothing about which states exist, which transitions are legal, or whether quota exhaustion is recoverable.
An explicit transition model makes some of that knowledge local:
// Illustrative lifecycle rules, not a complete subscription implementation.
type Status =
| "queued"
| "active"
| "exhausted"
| "expired"
| "cancelled";
const allowedTransitions: Readonly<Record<Status, readonly Status[]>> = {
queued: ["active", "cancelled"],
active: ["exhausted", "expired", "cancelled"],
exhausted: ["queued", "expired", "cancelled"],
expired: [],
cancelled: [],
};
function canTransition(from: Status, to: Status): boolean {
return allowedTransitions[from].includes(to);
}
An agent no longer needs to infer these particular rules by comparing a scheduler, an API handler, and a payment callback.
But the table alone is not enforcement. All relevant writes must pass through the appropriate transition operation, and that operation must check its additional business preconditions. Authorization, quota calculations, and concurrent activation are not solved by an enum.
This distinction matters throughout agentic coding: a rule being visible is different from a rule being enforced.
An explicit commercial model is more informative than conventions
In another part of a production system, separating a product definition from its purchasable pricing options made the business meaning clearer. An order referred to a specific pricing identity rather than reconstructing a purchase from an assortment of period fields.
Treating that purchased pricing definition as immutable also made an important distinction explicit: changing what is sold next must not silently rewrite what was sold previously.
The agent can still implement this incorrectly. The benefit is that the relevant concepts now exist as named objects and contracts instead of assumptions scattered across handlers.
Neatness and correctness need different mechanisms
Consistent structure reduces arbitrary variation. It tells the agent where a use case belongs, how dependencies are supplied, and how failures should be represented. This is the “more orderly code” benefit.
Correctness requires more. Import checks can reject forbidden dependencies. Schema validation can reject malformed inputs. Domain tests can reject illegal transitions. Integration tests can expose transaction errors. Operational evidence can reveal behavior that the pre-deployment checks did not cover.
These checks are complementary, not a universal ladder in which runtime validation is always stronger than static analysis. Each detects a different class of mistake.
SWE-agent offers a closely related result at the tool-interface level. Its authors showed that changing the agent-computer interface—including navigation, editing, and feedback—could improve performance without changing the underlying model weights. That supports the broader proposition that engineering the environment matters; applying the proposition to business architecture is an extension of the idea, not a result established by that paper.[7]
The objective is not to hope that a stronger model will always remember the conventions. It is to make correct conventions easy to follow and violations difficult to overlook.
5. Why prompt engineering alone was not enough
The failed project is useful because the mistake was not simply “we forgot a document.” The deeper mistake was treating successive prompts as the principal mechanism for preserving system knowledge.
A correction such as “do not update subscription state directly” can help the current session. Unless the repository gains an authoritative operation, a test, or an enforceable boundary, the same ambiguity remains for the next session.
The recurring failure pattern can be described as:

Adding prompt detail may interrupt that cycle. It does not necessarily remove its cause.
This is not an argument against prompts. Requirements, examples, and architectural explanations are often best expressed in language. A prompt can also instruct an agent to improve the architecture. The distinction is between an instruction that produces a durable improvement and one that merely compensates for the same defect again.
A complete agent guide is not an exhaustive agent guide
The inadequate AGENTS.md in the failed project mattered because there was no reliable, maintained account of the essential rules. That does not imply that the solution was a much longer file.
The empirical evidence is mixed. Lulla and colleagues studied 124 pull requests across ten repositories and reported 28.64% lower median runtime and 16.58% lower median output-token use with AGENTS.md. Crucially, their study did not establish full semantic correctness, and the output-token reduction should not be represented as a uniform reduction in total tokens.[8]
Gloaguen and colleagues found that context files increased the number of steps agents took. Their detailed conclusions distinguish a marginal negative success effect for generated files from a marginal gain for developer-written files, while highlighting the extra exploration and testing induced by instructions.[9]
Different tasks, agents, and measurements prevent these findings from collapsing into a simple “context files work” or “context files do not work” verdict.
My design rule is to document the information the agent cannot safely infer: authoritative entry points, non-obvious invariants, exact verification commands, compatibility obligations, and forbidden shortcuts. Put detailed subsystem knowledge near that subsystem rather than repeating the entire repository in every session.
Then encode enforceable rules in the build, types, tests, and permissions where appropriate.
The instruction file should explain the system’s rules. It should not be the only place those rules exist.
6. Architectural refactoring enlarges the feedback loop
The ordinary coding loop asks a local question:

That loop can improve a patch while leaving the repository progressively harder to work in. Each task may finish, yet the next task may require more exploration and more exceptions.
This is where I think architectural refactoring belongs inside a larger agentic workflow.
Suppose an agent repeatedly misses a lifecycle rule. The immediate response is to repair the patch. The larger response is to ask why the rule was easy to miss. Was it hidden in a scheduled job? Duplicated across entry points? Missing from the service contract? Documented in a file that ordinary navigation would never reach?
The answer may justify changing the environment rather than merely changing the next prompt:

An inner loop improves the implementation. An outer loop improves the conditions under which future implementations are produced.
A controller-centered system reorganized around coherent business ownership is one possible outer-loop intervention. So is replacing a misleading abstraction, adding a missing integration test, shortening noisy tool output, or correcting an outdated agent guide. DDD is not a prerequisite.
The point is to expand the question from “did this patch pass?” to “why is this class of task repeatedly expensive or error-prone?”
Refactoring is not automatically progress
A refactor should not be accepted because the resulting diagram looks more sophisticated. It needs a specific hypothesis: perhaps one lifecycle operation will replace duplicated state changes, or a public contract will remove the need for cross-package implementation imports.
For a production system, I would make such changes incrementally, preserving behavior with characterization and integration tests, checking migration compatibility, and retaining a rollback path. The agent can perform much of the implementation, but the acceptance criteria must not become easier merely because they obstruct the refactor.
There is also a boundary between engineering checks and security controls. An import rule can reject an architectural violation in CI; it does not make an agent with unrestricted shell access incapable of bypassing the rule. Tool permissions, protected checks, and review authority belong outside the patch being evaluated when that independence matters.
The outer loop should make the environment more trustworthy—not give the agent permission to redefine success until its own changes pass.
7. Optimize the cost of a correct change, not the fewest tokens
A badly structured repository can waste tokens through repeated discovery. An overengineered repository can waste them through unnecessary indirection.
Both fail the same test: they require too much incidental knowledge for the task at hand.
A small CRUD application may need only a clear module structure and strong tests. A domain-heavy subscription system may benefit from explicit lifecycle modeling. Turning every operation into a procession of factories, ports, events, and services is not a first-principles conclusion. It is another convention that needs justification.
The sensible objective is the lowest expected total cost of an accepted, sufficiently verified change. That includes model usage, tool runtime, human review, rework, and the risks the change introduces. An extra integration test may increase today’s token use while reducing the expected cost of a production failure.
To evaluate the token-saving claim properly, I would compare equivalent tasks on controlled repository snapshots, hold the agent configuration and resource budget fixed, and repeat runs. Comparing unrelated projects would confound architecture with language, task complexity, repository size, and tooling.
The useful measurements are not just a token total:
| Dimension | What to record |
|---|---|
| Context acquisition | Input tokens, files inspected, search calls, repeated reads |
| Execution cost | Output tokens, elapsed time, tool execution, caching behavior |
| Accepted quality | Independent tests, contract checks, architectural violations |
| Rework | Repair iterations, review corrections, regressions, failed attempts |
Failed attempts must remain in the accounting. Otherwise, a configuration that produces one cheap success and many discarded failures can appear efficient.
Architectural investment also has to pay for itself over time. Its implementation, migration, and maintenance costs should be compared with the expected savings across future tasks—not only the first demonstration after the refactor.
That is a testable engineering argument. “DDD saves 70% of tokens” without a controlled measurement is not.
8. Why human coding ability still matters
The conclusion is not that agents should write less code. It is that delegating code production does not eliminate the need to understand the code being produced.
In the failed project, removing manual implementation was treated as though it also removed the need for sustained architectural intervention. Those are different decisions.
Someone still has to determine whether a business distinction is real, whether a transaction boundary is adequate, whether an abstraction hides a necessary guarantee, and whether a passing test verifies the intended requirement. An agent can propose answers and investigate alternatives. Those answers still need justified acceptance.
Recent research on prompting-based DDD illustrates both the opportunity and the limitation. Eisenreich, Jusic, and Wagner found useful early-stage modeling artifacts in an industrial case study, but errors accumulated into later aggregate and technical-architecture outputs. The authors positioned the framework as support for expert collaboration rather than a replacement for it. This is evidence from a particular study, not proof of a permanent limit on AI architecture capabilities.[10]
The distinction I care about is therefore not “humans design, machines type” as an immutable division of labor. It is delegation without abandonment of judgment.
Nor is architectural judgment separate from coding competence. A diagram does not reveal whether cancellation is safe under retries. A service boundary does not prove that its transaction is correct. Engineers need enough implementation-level understanding to trace behavior, challenge generated abstractions, debug failures, and recognize when the model has solved the wrong problem elegantly.
As implementation becomes cheaper, these decisions can gain leverage. A good decision about ownership or invariants can improve many future changes. A bad decision can be reproduced just as efficiently.
That does not make architecture synonymous with adding layers. Sometimes the most important architectural intervention is deleting an abstraction, merging boundaries that never should have been separated, or choosing not to distribute a system.
The first-principles argument is ultimately straightforward. An agent must construct an adequate understanding from costly observations, choose a valid change, and obtain credible evidence that the change works. Architecture can make each of those obligations easier—or harder.
DDD is one way to improve that environment. Prompt engineering is another tool within it. Neither substitutes for understanding the system.
AI makes producing code cheaper. Good engineering makes producing the right change cheaper.
The valuable coding skill is not the ability to enter the most characters. It is the ability to understand behavior, establish boundaries, express constraints, and judge evidence.
The better agents become at writing code, the more leverage there is in deciding how that code should be organized—and how we will know it is right.
References
Nelson F. Liu et al. (2024). “Lost in the Middle: How Language Models Use Long Contexts.” Transactions of the Association for Computational Linguistics, 12, 157–173. Paper. ↩︎
Shunyu Yao et al. (2023). “ReAct: Synergizing Reasoning and Acting in Language Models.” ICLR 2023. Paper. ↩︎
David L. Parnas (1972). “On the Criteria To Be Used in Decomposing Systems into Modules.” Communications of the ACM, 15(12), 1053–1058. DOI. ↩︎
Chunqiu Steven Xia, Yinlin Deng, Soren Dunn, and Lingming Zhang (2024). “Agentless: Demystifying LLM-based Software Engineering Agents.” arXiv:2407.01489. Paper. ↩︎
Eric Evans (2015). Domain-Driven Design Reference: Definitions and Pattern Summaries. Domain Language. Reference. ↩︎
John Yang et al. (2024). “SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering.” NeurIPS 2024. Paper. ↩︎
Jai Lal Lulla et al. (2026). “On the Impact of AGENTS.md Files on the Efficiency of AI Coding Agents.” arXiv:2601.20404, version 2. Paper. ↩︎
Thibaud Gloaguen et al. (2026). “Evaluating AGENTS.md: Are Repository-Level Context Files Helpful for Coding Agents?” arXiv:2602.11988. Paper. ↩︎
Tobias Eisenreich, Husein Jusic, and Stefan Wagner (2026). “Automating Domain-Driven Design: Experience with a Prompting Framework.” arXiv:2603.26244. Paper. ↩︎