Skip to content

AI doesn't make Domain-Driven Design less important. It makes it essential.

Fabio Fognani

Over the past year, I’ve noticed a recurring pattern while using AI-assisted coding tools.

The code generated by AI is generally correct, sometimes impressively so. I can read it, understand it, modify it. Yet a few days later, I often struggle to recall how the system, or a part of it, is actually organized.

At first, I assumed this was simply the price of delegation: if I don’t spend the mental effort required to design and implement a solution myself, of course I won’t remember it as well.

After observing this phenomenon repeatedly, I’m convinced that’s largely true — but I also started to suspect something deeper is happening.

I believe AI-assisted development exposes a fundamental difference between how humans and large language models manage complexity.

The difference between “understanding code” and “owning a mental model”

Section titled “The difference between “understanding code” and “owning a mental model””

Software developers rarely think just in terms of lines of code.

Instead, we build mental models.

We organize systems into concepts, boundaries, abstractions, and relationships. We compress complexity into a manageable set of ideas.

When I think about an e-commerce system, I don’t keep hundreds of implementation details in my head. I think about a few core concepts:

  • Order
  • Payment
  • Inventory
  • Customer

Those concepts become anchors around which everything else is organized.

Research on software comprehension consistently highlights the importance of mental models in understanding and maintaining software systems. Developers do not merely write code; they construct internal representations of how the system works.

When I build a feature myself, I am simultaneously building that mental model.

When AI generates most of the implementation, I may understand the code locally without ever constructing the corresponding global model.

The result is a strange situation:

I understand the code while looking at it, but I don’t really own the architecture.

Human cognition operates under severe working-memory constraints.

To cope with those limitations, we constantly compress information.

We create:

  • abstractions
  • categories
  • patterns
  • hierarchies

A human architect might think:

Domain
├── User
├── Order
└── Payment

and only expand those concepts when necessary.

LLMs operate differently.

They can process enormous amounts of context simultaneously. They do not need to compress information into a small set of stable abstractions to the same extent humans do.

As a result, AI-generated solutions often contain a lot of very local material:

  • types and interfaces scoped to a single file or flow
  • mapping and transformation functions that only one call site needs
  • parsers, validators, and small algorithms tuned to one payload shape
  • intermediate data structures that exist only to bridge one step to the next

None of this is wrong in isolation. Taken one at a time, many of these are perfectly reasonable local optimizations.

The problem is cognitive rather than technical. There are often too many of them. They pile up inside the same files, enriching each module with concepts that only make sense in that immediate context. And they tend to be hyperspecialized: instead of folding a case into a pattern you could recognize elsewhere, the model optimizes the single path in front of it — producing slight variants of the same idea rather than one shared abstraction.

Every additional concept competes for space inside a human brain. And the codebase grows…

The hidden cost of AI-generated architecture

Section titled “The hidden cost of AI-generated architecture”

One of the most common patterns I encounter is the proliferation of structures that are technically valid but semantically unnecessary.

Suppose you already have an Article entity and an ArticleRepository that loads it for the application layer. Showing an article summary on a web page could stay inside that model:

async function getArticleSummary(articleId: string) {
const article = await articleRepository.getById(articleId);
return article.toSummary();
}

The repository returns a domain object you already know. The use case returns a subset of its data — nothing new to learn.

The AI-generated version often looks reasonable, and may even be leaner over the wire, fetching only the fields the handler needs. But it could introduce a parallel path and LOTS of new code, like in the following example:

interface ArticleSummaryRow {
id: string;
title: string;
status: string;
}
function isRecord(data: unknown): data is Record<string, unknown> {
return typeof data === "object" && data !== null;
}
function parseArticleSummaryRow(entry: unknown): ArticleSummaryRow {
if (!isRecord(entry)) {
throw new Error("Invalid entry payload");
}
if (!isRecord(entry.sys)) {
throw new Error("Expected sys object");
}
const sys = entry.sys;
if (typeof sys.id !== "string") {
throw new Error("Expected sys.id to be a string");
}
if (!isRecord(entry.fields)) {
throw new Error("Expected fields object");
}
const fields = entry.fields;
if (typeof fields.title !== "string") {
throw new Error("Expected fields.title to be a string");
}
if (typeof fields.status !== "string") {
throw new Error("Expected fields.status to be a string");
}
return {
id: sys.id,
title: fields.title,
status: fields.status,
};
}
async function getArticleSummary(articleId: string): Promise<ArticleSummaryRow> {
const params = new URLSearchParams({
select: "sys.id,fields.title,fields.status",
locale: "en-US",
});
const response = await fetch(`https://cms.example.com/v1/entries/${articleId}?${params}`, {
headers: { Authorization: `Bearer ${process.env.CMS_TOKEN}` },
});
const data: unknown = await response.json();
return parseArticleSummaryRow(data);
}

Did I just lose your attention?

Same screen. Same three fields. Yet now you also own ArticleSummaryRow, an isRecord helper that looks reusable, a parseArticleSummaryRow function that walks sys/fields with typeof checks, and a bespoke CMS fetch with hand-picked field selection — all of it beside Article and ArticleRepository, which were already there.

And then you notice the custom fetch has no retry logic, no DataLoader, none of the resilience or batching the repository may already have been giving you for free.

Worse, ArticleSummaryRow has started to drift from Article. The next change — a renamed status, a truncated title, a business rule about what “published” means — will either have to be reconciled back into Article, or, more likely, the LLM will patch the handler in place: duplicate the logic that already belongs in the entity, because that parallel type is now the path of least resistance.

So, eventually, the architecture stops reflecting the domain and starts reflecting implementation details.

Navigating the codebase becomes increasingly difficult because the concepts themselves are not meaningful.

The challenge is no longer:

Is this code correct?

The challenge becomes:

Which of these concepts actually deserve to exist?

Why boundaries matter more in the age of AI

Section titled “Why boundaries matter more in the age of AI”

This is where Domain-Driven Design (DDD) becomes particularly relevant.

Domain-Driven Design sounds heavier than it is. At its core, it means building software around a shared understanding of the business domain: the concepts that matter, the language used to describe them, and the boundaries that separate different responsibilities. The code should reflect that model, rather than forcing developers to think in terms of implementation details.

(Sorry, backend friends — deliberate oversimplification by a front-end guy.)

Traditionally, DDD was often justified as a way to manage complexity in large systems. Today, I think it serves an additional purpose, even in medium-sized projects.

It protects the developer’s mental model from fragmenting under wild code generation.

A strong domain model provides:

  • stable concepts
  • clear boundaries
  • shared vocabulary
  • cognitive compression

The goal is not merely architectural elegance.

The goal is preserving a representation of the system that a human can actually keep in their head.

Without strong boundaries, AI tends to introduce semantically similar but distinct structures. Over time, the architecture drifts away from the domain and toward an accumulation of implementation-specific concepts.

The code remains correct.

The system becomes harder to think about.

Before AI-assisted coding became common, many projects followed an implicit sequence:

Mental model → Code

Today, I believe the sequence should look more like:

Mental model
Boundaries (orthogonal parts, each with a clear concern)
Vocabulary (shared names everyone know what they refer to)
Code

The bottleneck is no longer writing code: AI can generate it almost instantly. The hardest part is maintaining a coherent mental model of the system.

That means investing more effort upfront in:

  • domain exploration
  • ubiquitous language
  • context maps
  • sequence diagrams
  • event flows
  • architectural boundaries

Paradoxically, the easier code becomes to produce, the more important conceptual modeling becomes.

AI does not reduce the value of Domain-Driven Design. It increases it.

When code generation becomes nearly free, the scarce resource is no longer implementation effort. The scarce resource becomes human understanding and memory.

Domain models, bounded contexts, and shared vocabulary are not merely architectural sophistications.

They are mechanisms for cognitive compression.

Their purpose is to ensure that the software remains aligned with concepts that humans can understand, remember, and reason about — even when an AI is generating most of the code.

And there is another implication. The faster code gets written, the more projects need a structure that is not custom, but as uniform as possible. Let’s say “standard”, at least relative to the other projects in the same team or company. What used to take a year can now ship in a few weeks. If you cannot sense how an app is organized before you even open the codebase, the sheer volume (and the speed at which large chunks of it were written) will overwhelm you.

There is a useful historical analogy here: the industrial revolution did not simply make craftsmen faster. It fundamentally changed the constraints under which products were built.

A skilled craftsman could keep the entire product in mind. Parts could be unique. Processes could be adapted on the fly. Knowledge was often implicit and lived inside the heads of individuals.

Assembly lines changed that. Once production accelerated dramatically, standardization became essential. Components needed clear interfaces. Processes needed to be repeatable. Workers needed shared conventions. The system could no longer depend on every individual carrying the whole product in their head.

AI-assisted development may be pushing software in a similar direction.

As code generation becomes dramatically faster, architectural originality becomes less valuable than architectural predictability. Teams need stronger boundaries, more consistent vocabulary, and more recognizable patterns — not because AI requires them, but because humans do.

The faster code is produced, the more important it becomes that developers can immediately understand where they are, what a concept means, and which part of the system owns a particular responsibility.

In that sense, AI is increasing, not reducing, the need for standardization. As code becomes cheaper to generate, consistency, boundaries, and shared vocabulary become more valuable than ever.

And while the future may belong to AI-assisted development, the responsibility for creating, naming, and preserving those concepts remains fundamentally human.