Logo
Artificial Intelligence

Clean Architecture with FastAPI in the AI Era: Designing Codebases Humans and Agents Can Safely Change

AI can generate a working FastAPI application in minutes. But can it safely maintain that application as requirements change?

Sergio Infante
Sergio Infante
Software Engineer
November 11, 202613 min read
Share
Clean Architecture with FastAPI in the AI Era: Designing Codebases Humans and Agents Can Safely Change

Writing Code Is No Longer the Hardest Part

Imagine starting a new backend project today.

You open your favorite AI coding assistant and write:

Build a REST API using FastAPI, PostgreSQL, SQLAlchemy, and Pydantic. The application should manage customer orders, support basic CRUD operations, and include unit tests.

A few minutes later, you have a working application.

The agent has created your database models, request schemas, API endpoints, and perhaps even a Docker configuration.

You run the tests. Everything passes.

The application works.

But does it have a good architecture?

That's a completely different question.

Generating code and designing software are not the same activity.

An AI agent can produce a technically correct implementation while introducing unnecessary coupling, mixing business rules with HTTP handlers, or making decisions that become increasingly expensive to change.

These problems rarely appear during the first implementation. They emerge when requirements evolve.

Consider what happens when you ask the agent to:

  • Replace PostgreSQL with DynamoDB.
  • Introduce a new business rule for high-value orders.
  • Expose the same functionality through a background worker.
  • Add an external payment provider.
  • Refactor the persistence layer without changing business behavior.

If your codebase has clear architectural boundaries, these changes can remain relatively isolated. Without those boundaries, even a small feature might require modifications across unrelated parts of the application.

When AI agents make those changes, the consequences can become harder to review. This is where Clean Architecture with FastAPI becomes particularly interesting.

Clean Architecture is no longer just about making code easier for humans to maintain. It can also help create an environment where AI agents make safer, more predictable changes. Let's explore how.

The Problem with AI-Generated FastAPI Applications

First, we need to acknowledge something important.

AI coding assistants are genuinely useful.

They can generate boilerplate, implement endpoints, create database queries, write tests, and accelerate development considerably.

The problem is not that AI writes bad code by definition.

The problem is that working code is not necessarily maintainable code.

Consider a simplified FastAPI endpoint:

All-in-one order endpoint (before refactoring): https://gist.github.com/sinfante-waverley/3022a4c7ccb924475e19a2f1e5829fe6
All-in-one order endpoint (before refactoring): https://gist.github.com/sinfante-waverley/3022a4c7ccb924475e19a2f1e5829fe6

This implementation isn't inherently unusual. For a small prototype, it might be perfectly reasonable.

But look carefully at the responsibilities of this endpoint.

It handles:

  1. HTTP input and errors.
  2. Business validation.
  3. Business decisions.
  4. Database persistence.
  5. Transaction management

Five different responsibilities live inside a single function.

Now imagine that a new requirement arrives:

Orders over $1,000 require manual review, except when the customer belongs to our trusted enterprise program.

Where should the agent implement this rule?

Inside the endpoint?

Inside the SQLAlchemy model?

Inside a helper function?

What happens if we later introduce a background worker that creates orders without making HTTP requests?

We now risk duplicating business logic or forcing non-HTTP components to depend on our API layer.

The issue is not AI itself.

The issue is that the codebase does not clearly communicate where different kinds of decisions belong.

Clean Architecture: The Dependency Rule Still Matters

Clean Architecture is often misunderstood as a specific directory structure.

It is not.

Creating folders named domain, application, and infrastructure does not automatically produce a clean architecture.

The important principle is the direction of dependencies. Business rules should not depend on infrastructure details. Your business logic should not need to know whether you are using:

  • FastAPI or another HTTP framework.
  • PostgreSQL or DynamoDB.
  • SQLAlchemy or another persistence library.
  • An HTTP endpoint or a message consumer.

These technologies are implementation details surrounding the business rules.

For our example, we will use four logical areas:

The domain sits at the center.

The application layer depends on the domain. Infrastructure and interface adapters depend on the abstractions they need, while the application's composition root (the single place where concrete implementations are wired together, such as main.py) connects concrete implementations.

This distinction is particularly valuable when using coding agents.

A clearly defined boundary tells the agent where new functionality belongs and which dependencies it can use.

It also gives us something we can verify automatically.

Building a FastAPI Application Around the Domain

Let's refactor our order example.

We will use the following structure:

Figure 1. A minimal Clean Architecture FastAPI project structure
Figure 1. A minimal Clean Architecture FastAPI project structure

This is intentionally minimal.

We do not need dozens of interfaces, abstract factories, or complicated inheritance hierarchies.

Our goal is to separate responsibilities, not maximize abstraction.

Step 1: Model business behavior in the domain

Our order entity should express business rules without knowing anything about FastAPI or SQLAlchemy.

app/domain/order.py: https://gist.github.com/sinfante-waverley/df8ff3dccbe8bbf98a113ccb3c6e619f
app/domain/order.py: https://gist.github.com/sinfante-waverley/df8ff3dccbe8bbf98a113ccb3c6e619f

Notice what is missing. There is no FastAPI import. There is no database session. There is no SQLAlchemy model. There is no HTTP exception.

The order entity knows only about the business rule.

And because it uses ordinary Python objects, we can test its behavior without starting an application or connecting to a database.

tests/domain (order entity tests): https://gist.github.com/sinfante-waverley/7a8aa8054f6e8cfadf97ef1138288ce8
tests/domain (order entity tests): https://gist.github.com/sinfante-waverley/7a8aa8054f6e8cfadf97ef1138288ce8

These tests are fast and deterministic.

They do not require dependency injection, HTTP clients, databases, or mock infrastructure.

This is valuable for human developers, but also for AI agents.

When an agent changes a business rule, it can run a focused test suite and receive immediate feedback without provisioning the entire application.

Step 2: Define a persistence contract

Our application needs somewhere to store orders.

But it should not know how those orders are stored. We can express the requirement using Python's Protocol.

app/application/ports.py: https://gist.github.com/sinfante-waverley/e9cd79d1ef8da73b4cbd74e6cbee14ec
app/application/ports.py: https://gist.github.com/sinfante-waverley/e9cd79d1ef8da73b4cbd74e6cbee14ec

This is a small but important abstraction.

It says the application needs a repository that can save orders.

It does not require PostgreSQL, SQLAlchemy, DynamoDB, or any other implementation.

We are separating a business capability from the technology that provides it.

Step 3: Implement the application use case

Next, we create the use case responsible for creating orders.

app/application/create_order.py: https://gist.github.com/sinfante-waverley/6e1fb72018ba40947ed4b1fa70f83a7f
app/application/create_order.py: https://gist.github.com/sinfante-waverley/6e1fb72018ba40947ed4b1fa70f83a7f

The use case orchestrates business behavior.

It creates a domain object and asks the repository to persist it.

Notice that the use case does not import SQLAlchemy or FastAPI.

We could invoke it from an HTTP endpoint, a background worker, or a command-line interface.

The business behavior would remain the same.

Step 4: Implement the infrastructure adapter

Now we can introduce SQLAlchemy.

This example assumes an existing mapped OrderModel and a configured database session.

app/infrastructure/repositories/sqlalchemy_orders.py: https://gist.github.com/sinfante-waverley/b10e9924ddf58eefe9c7bc52e4e0ab95
app/infrastructure/repositories/sqlalchemy_orders.py: https://gist.github.com/sinfante-waverley/b10e9924ddf58eefe9c7bc52e4e0ab95

This is where persistence details belong.

If we later switch to DynamoDB, we can write another adapter that satisfies the same repository contract.

Of course, replacing a database in a real production system involves much more than implementing a new repository. Transactions, query capabilities, consistency models, migrations, and operational behavior also matter.

Clean Architecture does not eliminate those concerns.

It helps prevent them from spreading into business logic.

Step 5: Connect everything through FastAPI

FastAPI's dependency injection system provides a convenient way to connect our application components.

app/interfaces/api/orders.py: https://gist.github.com/sinfante-waverley/3815797894ea9040e2d9eebb8b343dc7
app/interfaces/api/orders.py: https://gist.github.com/sinfante-waverley/3815797894ea9040e2d9eebb8b343dc7

The endpoint now has a much clearer responsibility.

It translates an HTTP request into a use-case invocation and converts the result into an HTTP response.

Business logic stays in the domain.

Persistence stays in infrastructure.

The application use case coordinates both through a contract.

We have not reduced the total number of lines of code.

That was never the goal.

We have reduced the number of architectural responsibilities that can accidentally become entangled.

Why This Matters More When AI Agents Write Code

So far, nothing we have discussed is particularly new.

Clean Architecture existed long before AI coding assistants became popular.

What has changed is the way software gets modified.

Traditionally, developers needed to understand at least part of a codebase before making changes.

Today, an engineer can delegate an implementation task to an agent and review the resulting diff.

That makes it possible to produce more changes with less manual code writing.

But it also creates an important question:

How does the agent know which architectural decisions it is allowed to make?

Consider this instruction:

Add support for sending a notification when an order requires manual review.

An agent working in an unstructured codebase might import an email SDK directly into the order model.

The implementation could work.

The tests might pass.

But now the domain depends on an external communication service.

Our business entity now has to manage infrastructure behavior.

A better implementation would introduce a notification boundary and coordinate notification delivery from the application layer.

The agent shouldn't have to invent that architectural decision every time it receives a new task.

We can communicate those expectations explicitly.

AGENTS.md: Making Architectural Intent Visible to AI

Tools such as OpenAI Codex support repository-level guidance through AGENTS.md. Claude Code supports project instructions through CLAUDE.md, and can also read AGENTS.md.

These files provide persistent project context, including conventions, workflows, and architectural expectations.

For our FastAPI project, we might create the following file:

AGENTS.md: https://gist.github.com/sinfante-waverley/10cc8cd93045bdfdbc1dca8ed1573b33

This does not guarantee that an agent will follow every rule.

Instructions are guidance, not enforcement.

However, they make the intended architecture visible to the tools contributing to the repository.

The agent no longer needs to infer every convention from existing code.

And when instructions are paired with automated validation, the result becomes much more powerful.

Architecture Tests: Don't Trust the Prompt Alone

An AGENTS.md file can tell an agent not to import SQLAlchemy into the domain.

But what happens if the agent ignores that instruction? Or if a human developer makes the same mistake? We should detect the violation automatically.

A practical option for Python projects is to add dependency-boundary checks to CI.

For example, the following test uses Python's ast module to detect forbidden imports.

tests/architecture (forbidden-import check): https://gist.github.com/sinfante-waverley/aaf512e72055ad9390ee79ba296fc4f7
tests/architecture (forbidden-import check): https://gist.github.com/sinfante-waverley/aaf512e72055ad9390ee79ba296fc4f7

This is intentionally a small example, not a complete import-analysis solution.

It does not handle every possible relative import, dynamic import, or alias resolution scenario.

For larger projects, a dedicated dependency-contract tool such as Import Linter provides a more comprehensive approach.

The underlying principle is what matters:

Tools should verify architectural decisions, not just document them in prompts.

Now, if an agent introduces a forbidden dependency, CI can fail.

The agent receives concrete feedback.

The engineer receives a clear architectural signal.

And the repository has an enforceable rule independent of whichever model generated the code.

Giving Agents a Feedback Loop

A useful coding agent needs more than instructions. It needs a way to determine whether its changes are acceptable.

For our project, that feedback loop could consist of:

Figure 2. A feedback loop for AI coding agents: automated checks first, human review last
Figure 2. A feedback loop for AI coding agents: automated checks first, human review last

Each mechanism checks a different property.

Unit tests verify behavior.

Linters catch selected code-quality issues.

Type checkers identify inconsistencies in interfaces and values.

Architecture tests verify dependency boundaries.

Human review evaluates whether the implementation actually matches the business intent and whether its design trade-offs make sense.

None of these mechanisms is sufficient on its own.

Together, they give both developers and coding agents a much stronger basis for making changes safely.

This shift matters for engineering practice.

Instead of relying exclusively on instructions like "write clean code," we define properties we can check.

Clean Architecture Does Not Mean More Abstraction Everywhere

There is a danger in this discussion.

We could start believing that every FastAPI endpoint requires a repository interface, a use-case class, multiple adapters, and separate request and response objects.

That is not the argument.

Clean Architecture should help manage complexity, not manufacture it.

A simple internal CRUD application might not benefit from introducing several layers for every database operation.

A prototype intended to validate a business idea might reasonably use a much simpler structure.

And even in a mature system, we should avoid creating abstractions without a meaningful reason.

A useful question is:

Which parts of this application are likely to change independently, and which business rules must remain stable when they do?

For example, isolating payment calculations from a payment-provider SDK is usually worthwhile.

Creating an elaborate abstraction around a single trivial database lookup might not be.

AI can generate boilerplate quickly, but that does not make unnecessary abstractions free.

Every additional interface, layer, and convention becomes something that humans and agents must understand and maintain.

Good architecture requires judgment.

The Engineer's Role Is Changing, Not Disappearing

AI coding agents can make development substantially faster.

But faster implementation does not automatically produce better systems.

An agent can generate a repository adapter.

It can extract a service.

It can write tests.

It can refactor a module.

It cannot independently guarantee that the resulting architecture reflects your product's long-term needs and constraints.

The engineering team still owns that responsibility.

Engineers increasingly need to define:

  • Where business rules belong.
  • Which dependencies are acceptable.
  • What should remain stable as infrastructure changes.
  • How correctness is verified.
  • Which architectural trade-offs are justified.
  • What level of complexity the product actually requires.

The job is not simply to tell an agent what code to write.

It is to create the conditions under which code can be changed responsibly.

Clean Architecture is one tool for doing that. (We look at how these roles are shifting across the whole delivery lifecycle in our white paper Human vs. AI: Redefining Roles in the Software Development Lifecycle.)

Conclusion: AI Writes Code. Engineers Design Boundaries

Clean Architecture has always been about separating business rules from implementation details.

It allows systems to evolve without making every change affect every component.

In the AI era, that separation offers another practical advantage.

Clear architectural boundaries make a codebase easier for coding agents to navigate, understand, and modify.

Repository instructions communicate intent.

Automated architecture checks enforce constraints.

Tests provide feedback.

Human engineers retain responsibility for the decisions those mechanisms cannot make.

The goal is not to build a complicated architecture that looks impressive.

The goal is to build software whose important boundaries remain understandable and verifiable, regardless of whether the next change comes from a human developer or an AI agent.

AI makes writing code cheaper. That makes architectural judgment more valuable, not less.

About the author & stay in touch
Sergio Infante
Sergio Infante
Software Engineer

Software engineer with Nearly 20 years building systems, leading teams, and teaching what I learn.

Stay up to date

Subscribe to Waverley's newsletter and stay up to date with the latest articles.

Let's Work Together

Ready to build something great?

Let's talk about your project.

Contact Us