Clean Architecture for AI Systems
AI providers change faster than almost anything else in your system: a new model ships, a price changes, a compliance rule forces you off the cloud. If your use-case logic calls a vendor SDK directly, every one of those events is a code rewrite and a round of regression risk. Clean Architecture solves this by organizing code into concentric layers with dependencies pointing inward, so the volatile parts live at the edge and the valuable parts stay still.

The four layers
In a .NET solution, Clean Architecture maps to four projects:
- Domain β the enterprise concepts and rules. For LyraLearn:
Lesson,KnowledgeChunk,TutorAnswer,Citation. No framework references, no SDKs β just C# that models the business. This is the innermost layer and depends on nothing. - Application β the use-cases and the interfaces they depend on. Here lives
AnswerStudentQuestion, plus contracts likeIEmbeddingProvider,IChatProvider, andIVectorStore. Application depends only on Domain. - Infrastructure β the implementations of those interfaces:
SqlServerVectorStore,LocalEmbeddingProvider,AzureOpenAiChatProvider, EF Core, HTTP clients. Infrastructure depends on Application (to implement its interfaces), never the reverse. - Presentation β the ASP.NET Core web app and API. It depends on Application to invoke use-cases and on Infrastructure only at startup, for wiring.
The rule is simple: dependencies point inward. Domain and Application know nothing about SQL Server, Azure, or any vendor. They speak only in interfaces.
Why AI providers live only in Infrastructure
This is the payoff. An AI provider is an implementation detail, so it belongs in Infrastructure behind an Application interface:
// Application β stable, no SDKs
public interface IChatProvider {
Task<string> CompleteAsync(Prompt prompt, CancellationToken ct);
}
// Infrastructure β swappable, vendor-specific
public sealed class AzureOpenAiChatProvider : IChatProvider { /* ... */ }
public sealed class LocalLlamaChatProvider : IChatProvider { /* ... */ }
The use-case AnswerStudentQuestion depends only on IChatProvider. Swapping Azure OpenAI for
a fully local model β or vice versa β is a DI registration change in one composition-root
file:
services.AddScoped<IChatProvider, LocalLlamaChatProvider>(); // was AzureOpenAiChatProvider
Model selection becomes configuration, not a refactor. The use-cases don't change, and β critically β neither do their tests.
Testability falls out for free
Because use-cases depend on interfaces, your unit tests inject a FakeChatProvider that returns
canned text and a FakeVectorStore that returns fixed chunks. You can test the AI Tutor's
grounding and validation logic deterministically, offline, with no API key and no cost β
the model is mocked at the seam. The expensive, non-deterministic provider is exercised only in
a small set of integration tests at the Infrastructure boundary. That separation is what lets a
team change models on a Friday and still ship with confidence.