Logo
Back to blog
Jul 25, 2026 · 9 min read

Architecture

Software architecture in the AI age

How decades-old concepts, like DDD and tests as documentation, can give you back control over products built with AI agents.

By Vinícius Amélio

Cover for Software architecture in the AI age

In the age of LLMs, code production and digital product development have become so widespread that anyone with a bit of time and some familiarity with technology can build applications of different sizes and complexities. What used to take a few days, weeks, or even months can now be built in a few minutes or hours, depending on how clear and specific you can be when giving context to your agents. This can be a good thing, as more and more MVPs, prototypes, and hand-offs happen in record time. Had an idea? Fire up your Claude Code, Codex, Copilot, or (insert your favorite coding agent here) and watch your ideas come to life in real time, validate them with users in record time, and voilà: you're ready to make money, hit your KPIs, reach your app's MAU target... but what comes next?

As time passes, that code initially built with little to no oversight starts showing the same symptoms as an MVP built a decade ago at a startup that, in the best case, did really well and frantically expanded every feature its users needed. These symptoms, once commonly felt (and suffered) by human developers, are now felt by both humans and agents alike. You — Dev/PM/Product Designer/(insert your fancy English job title here) — start noticing that any relatively small change to the project, one that already requires moving between files to understand what's going on, fills your agent's context window at an accelerated pace. And worse: before you even touch a single line of code, context compaction is already kicking in almost automatically. You try to go back over all the generated code, understanding it file by file (which you lied to yourself about reviewing before every commit), and finally realize the mistake was delegating architectural and software design decisions to the agent. But who can blame you? The product demanded a tight deadline, you delivered within that deadline, but now it's time to clean house.

The cognitive debt in teams

Reading everything I said in the introduction might give the impression that I'm suggesting using AI in projects, particularly corporate ones, is a mistake — and that's not the case, since I even have friends who use it (obviously a joke). The whole point is the "how" it's done.

See, it used to be very common for teams to carry technical debt as new features and directions were developed for their products. Technical debt is nothing more than the well-known scenario of "We didn't do it the ideal way for reason X, but one day we'll fix it." Today we're facing a different kind of debt, cognitive debt, which is far more damaging to your software's health. Cognitive debt is like the technical debt we used to have, but with one big difference: "My agent generated this code, one day I'll understand it better before I ship it." There you go — with that, you've implanted the recipe for chaos into your codebase, and, beyond that, into your own understanding of technology and ownership.

How can concepts more than two decades old give you back control over your product?

Domain-Driven Design (DDD) was first introduced in a book titled "Domain-Driven Design: Tackling Complexity in the Heart of Software," written by Eric Evans in 2003. In summary — a very condensed summary, extracting only what's necessary for my argument — it defines a few core concepts such as:

  • Ubiquitous language: a common vocabulary shared by both developers and the business team.
  • Bounded contexts: well-defined boundaries around domain models to manage complexity.
  • Entities and value objects: fundamental building blocks used to classify and model objects based on identity and mutability.

I know a lot of developers turn up their nose the moment they hear things like DDD, Clean Code, and Clean Architecture. I'd say this has become even more common nowadays with LLMs, with comments like "That's over-engineering," "Too much complexity for too little result," among other complaints. I find many of them fair, up to a point, depending on the size of the project, the technologies used, the size of the team, the team's seniority, and a few other factors that aren't relevant to this topic.

Even so, I want to make it clear that the idea isn't to ask your AI to go apply DDD, creating contexts and aggregates, applying CQRS to your endpoints, or anything like that. The idea is to structure your applications in such a way that both you and your team, as well as the agents, can read the codebase in a simple way, without flooding the context window and without creating cognitive overload for anyone trying to read the code on their own.

See, you might already apply some of these concepts in your day-to-day: you might already use the terms your product/business team uses to name the components in your codebase, making it easier to understand when someone asks you something in the daily standup; you might already use modules or something like vertical slices, where each feature has its own folder within the project that scopes the topics covered within that virtual product boundary; you might already have structures that represent business operations and actors.

An example of all these concepts I mentioned was the design of Physikos, my strength training management app. Let's imagine it has a few different contexts, such as: workout, analytics, dashboard, and settings.

Each of these modules has a responsibility tied directly to the application and its business representation: workout handles everything related to managing training sessions, analytics handles everything aimed at providing comparisons of that training data in chart form, dashboard is responsible for showing training metrics and body measurements. Each of these modules has its own features, with its own repositories, services, events, components, and tests. Each of these modules is a separate package in my codebase, such that for every change I need to make, all I have to do is add that module's folder to my prompt's context, and that's it: the agent knows that, most likely, the entire piece of code it needs to understand to make a change related to what I'm asking is contained in that folder; my team and I know that if I need to touch something in the workout log, I need to change something in that folder; and finally, I know that the tests for whatever I'm about to change are also in that part of the project.

In practice, the folder structure looks roughly like this, with each context isolated in its own module and each feature carrying its own layers:

src/
├── workout/
│   ├── features/
│   │   ├── log-session/
│   │   │   ├── data/
│   │   │   │   └── workout-repository.ts
│   │   │   ├── domain/
│   │   │   │   └── workout-session.ts
│   │   │   ├── services/
│   │   │   │   └── log-session-service.ts
│   │   │   ├── events/
│   │   │   │   └── session-logged-event.ts
│   │   │   ├── components/
│   │   │   │   └── log-session-form.tsx
│   │   │   └── log-session.test.ts
│   │   └── edit-plan/
│   │       └── ... (same layers: data, domain, services, events, components, tests)
│   └── index.ts
├── analytics/
│   └── features/
│       └── ... (same layer pattern)
├── dashboard/
│   └── features/
│       └── ... (same layer pattern)
└── settings/
    └── features/
        └── ... (same layer pattern)

Notice there's no generic utils or services folder shared across modules holding business logic: each context is self-contained, and whatever is truly cross-cutting (formatting, HTTP clients, etc.) lives isolated in a separate shared package, with no business rules of its own.

This way, the agent doesn't need to comb through the entire project, needlessly filling up its context window; my team doesn't need to read through a tangle of files to understand what's about to change; I save tokens, time, and my sanity.

Tests as documentation

Just as it's become ridiculously easy to generate production code these days, we also have that same ease in generating tests, whatever kind they may be: unit, integration, component, e2e, golden, behavioral, (insert your favorite type of test here).

All of them can be generated by AI just the same, and they're a good way to add guardrails to your agents while also ensuring quality and documentation. See, in a very long coding session, agents tend to get lazy: when they get "stuck" on some specification that wasn't very well covered in your spec document or prompt, they'll often find a sneaky way to simply skip that step because they've been stuck too long trying to get something to work the wrong way. When you specify that certain tests must be written, and they're part of your DoD (Definition of Done), your AI tends to have an easier time solving more complex problems because it knows what the expected final result is. But more than that, this definition of "done" avoids that annoying repetitive cycle where you read the final output and see that your agent marked the task as finished when it doesn't actually meet everything that was needed.

Building a new feature? Add standardized behavior tests that every feature should have: empty state, error state, loading state, success state, variants. If it's server-side, you can spin up a container with the database used in the project, or build an in-memory/SQLite adapter that validates DB communication errors, errors coming from third-party APIs, invalid input, timeouts, fallback activation, circuit breaking, DLQ population, cache access rules, etc.

Fixing a bug? Use TDD — ask the agent to start the fix with tests. It'll start by writing the correct behavior and only then adapt the code exactly to that scenario, without hallucinating hypotheses that don't match the bug itself.

In the end, you'll have, at the very least, a cataloged source of how your software works and what's expected of it.