Some Architectures Work Better with LLMs Than Others
The Hypothesis#
I have been working with LLMs to generate code for long enough now that I am starting to notice patterns in what works and what breaks. Not all architectures seem equally friendly to AI-assisted development. This is worth examining carefully, and I do not think it is obvious which patterns will age well in a world where much of our code is LLM-generated.
The architectural challenges remain the same regardless of who or what writes the code. Complexity still accumulates. Coupling still creates fragility. Cohesion still matters. The type system still catches certain classes of errors. What I am observing is that different architectural patterns may fare differently depending on how LLMs tend to approach these challenges. The problems are not different. How LLMs resolve them might be.
My hypothesis is this. Architectural patterns that balance code locality, coupling, and cohesion differently may produce different outcomes with LLM-assisted development than they do with traditional development practices. This is not a settled conclusion. It is an observation that keeps showing up in my work, and I think it deserves serious consideration before we commit to architectures that might make LLM-generated code harder to maintain safely.
Why Shared Abstractions Become Landmines#
The pattern I see most often is when you ask an LLM to modify a feature in a codebase that uses shared domain objects. The LLM generates code based on the context you provide. It sees the domain object you are working with, it understands the immediate feature you are building, and it has a propensity to rewrite that domain object to solve the problem in front of you.
What the LLM typically cannot see is the full dependency graph of where else that domain object is used. The change looks correct in isolation. When you run your tests, you discover that features you were not intending to touch are now broken.
This is a coupling problem. Both humans and LLMs struggle with the same underlying issue when modifying shared abstractions. The challenge is understanding the full impact of changes across a dependency graph.
The difference seems to be one of degree. Humans can deliberately search the codebase for usages and trace through implications. LLMs tend to optimize for the immediate context, rewriting code to satisfy the request in front of them without systematically tracing through all call sites or considering what other features depend on the invariants that existed before the change. The abstraction that was meant to enforce consistency across features becomes a point of fragility.
Onion architecture provides a concrete example of where this pattern shows up. The pattern places domain entities at the center of the system, shared across multiple use cases. This is intentional. The domain model is meant to represent the core business logic, reused by different application layers that need to interact with that logic.
When you ask an LLM to modify a feature that uses one of these domain entities, the AI may rewrite the entity to serve the feature you are working on. The changes can ripple through other use cases that share that same entity, often in ways that are not immediately visible until runtime or integration tests catch the break.
Now contrast this with vertical slice architecture. Each feature encapsulates its own data structures, its own logic, and its own representations. When you ask an LLM to modify a feature in a vertical slice system, it rewrites code that is scoped to that feature.
The duplication that we have traditionally been taught to avoid becomes an asset. Changes stay contained. If the LLM generates bad code, the blast radius is limited to the feature you were working on. You are not discovering unintended breakage in parts of the system you thought were isolated.
This connects directly to locality of behavior. The more context needed to understand a piece of code exists in close proximity to that code, the better an LLM can reason about it. Shared abstractions spread context across the codebase. Vertical slices concentrate it. The LLM does not have to reconstruct a mental model of the entire system to make a safe change. It only needs to understand the feature it is modifying.
The Economic Question#
Constantine’s equation tells us that the cost of software is dominated not by the initial build but by the cost of changes over time.
If that holds, then the question we need to answer about architecture is not which pattern produces the cleanest initial implementation. It is which pattern minimizes the total cost of changes when those changes are increasingly being made by LLMs.
The argument for shared abstractions goes like this. If you duplicate logic across features and that logic needs to change, you now have to change it in multiple places. If you forget one, you have introduced inconsistency. Shared abstractions prevent this by ensuring that a change in one place propagates everywhere. The compiler enforces correctness. This reasoning applies whether the changes are made by humans or LLMs.
The counter-argument centers on blast radius. If your abstractions are shared and something modifies one to solve an immediate problem, you may have broken multiple features without realizing it. The cost of that unintended breakage, the debugging time, the time spent writing tests to prevent regressions, the time spent coordinating fixes across different parts of the system, that cost compounds.
The vertical slice approach accepts duplication as the cost of isolation. When a change is needed, you make it in multiple places, but each change is deliberate and scoped. This reduces the risk of hidden coupling.
There is an honest acknowledgment required here. You do give something up with this approach. You lose some of the compiler safety that comes from shared types. You accept that bugs can exist independently across features rather than being caught systematically. You forgo some of the coordination benefits that shared abstractions provide when multiple teams need to interact with the same core logic.
This is not a new tension. The trade-off between duplication and abstraction has been with us since before DRY and Single Responsibility were articulated. The question I am exploring is whether the emergence of LLMs as primary code generators shifts the balance.
The calculation depends on your specific context. If your system is highly stable, if changes are infrequent, if coordination across features is a primary concern, shared abstractions may still be the right choice. But if you are iterating rapidly, if LLMs are generating much of your code, and if unintended breakage from shared object modifications is a recurring problem, architectures that embrace locality may look more attractive. This is an economic decision about where you want to spend your time debugging versus where you want to accept duplication.
The hidden cost of agentic development is that the code arrives faster but the understanding required to integrate it safely does not. Architectures that contain the blast radius of changes reduce the surface area where that understanding is required. That containment has economic value when changes are cheap to generate but expensive to validate.
What This Means#
I am not arguing that everyone should immediately rewrite their systems to vertical slice architecture or abandon shared abstractions entirely. That would be dogmatic, and architecture is a team decision that depends on your constraints, your team structure, and the nature of the system you are building. What I am suggesting is that the calculation around which architectural patterns best serve your needs may be shifting as LLMs become more central to how we generate code.
The principles that govern good architecture have not changed. Coupling and cohesion still matter. Locality of behavior still matters. The cost of changes over time still dominates. What may be changing is which patterns best satisfy those principles when a significant portion of your code is being written by systems that reason about local context but may struggle with global dependencies.
If you are actively using LLMs to generate code, it is worth paying attention to where the breakage happens. If you are repeatedly discovering that LLM-generated changes to shared abstractions are breaking unrelated features, that is signal. If you find yourself spending significant time validating that changes to core domain objects have not introduced regressions elsewhere, that is cost. The question to ask is whether a different architectural pattern would reduce that cost by making changes more naturally isolated.
This is an emerging pattern, not settled science. I expect the tooling around LLM-assisted development will improve. Better static analysis, better integration with type systems, better understanding of cross-cutting concerns. But until those tools exist and are mature, the architecture that makes it easiest for an LLM to modify code safely without breaking unintended things may not be the architecture we would have chosen five years ago.
The fundamentals still matter. But the application of those fundamentals in a world where AI compresses iterations but not understanding may look different than we are used to. That difference is worth taking seriously.