ποΈ Clean Architecture in .NET 9 β The Foundation of Scalable Web Apps

Welcome to Week 1 of our blog series: Mastering .NET Web Application Architecture. In this post, weβll lay the groundwork by understanding what Clean Architecture is and why it's essential for building maintainable, testable, and scalable apps.
π₯ Have you ever worked on a .NET project where everything felt like spaghetti?
- Services directly calling repositories...
- Business logic sprinkled inside controllers...
- Changes in one layer causing ripple effects everywhere...
You start off fast, but after a few sprints, the code becomes fragile, tightly coupled, and nearly impossible to scale or test.
Thatβs where Clean Architecture comes in.
It gives your application a solid structure, promotes separation of concerns, and ensures your business logic remains insulated β no matter what changes around it.
In this post, letβs break down what Clean Architecture in .NET 9 looks like, and why it should be the foundation for your next scalable web app.
π§ What is Clean Architecture?
Clean Architecture is a layered approach that separates concerns across different parts of your application. It allows you to evolve independently, test components in isolation, and swap out infrastructure with minimal effort.
ποΈ Core Layers of Clean Architecture
π§± The idea: Dependencies flow inward, and the inner layers have no idea about the outer ones.
π Dependency Rule
One of the key principles of Clean Architecture is that dependencies should point inward. That is, the inner layers should not depend on the outer layers. This ensures that the core business logic remains unaffected by changes in external systems.βMicrosoft Learn+2Microsoft for Developers+2C# Corner+2
ποΈ The Four Layers of Clean Architecture
- Domain Layer: This is the core of your application, containing business logic and entities. It's independent of any external systems or frameworks.βC# Corner
- Application Layer: This layer orchestrates the business logic, handling use cases and application-specific rules. It defines interfaces that are implemented in the outer layers.βMedium+12Anton Dev Tips+12Milan JovanoviΔ+12
- Infrastructure Layer: Here, you'll find implementations for data access, external services, and other technical details. This layer depends on the Application Layer but not vice versa.βC# Corner+2Anton Dev Tips+2Positiwise+2
- Presentation Layer: This is the user interface of your application, such as a web API or UI. It interacts with the Application Layer to fulfill user requests.βMicrosoft for Developers+4Medium+4Medium+4

π Sample .NET 9 Project Structure

π§© Code Examples by Layer
πΉ Product.cs
(Domain Layer)
namespace Domain.Entities
{
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
}
πΉ IProductService.cs
(Application Layer)
namespace Application.Interfaces
{
public interface IProductService
{
Task<IEnumerable<Product>> GetAllAsync();
Task<Product> GetByIdAsync(Guid id);
}
}
πΉ ProductService.cs
(Application Layer)
using Application.Interfaces;
using Domain.Entities;
namespace Application.Services
{
public class ProductService : IProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository;
}
public async Task<IEnumerable<Product>> GetAllAsync() =>
await _repository.GetAllAsync();
public async Task<Product> GetByIdAsync(Guid id) =>
await _repository.GetByIdAsync(id);
}
}
πΉ ProductRepository.cs
(Infrastructure Layer)
using Domain.Entities;
using Microsoft.EntityFrameworkCore;
public class ProductRepository : IProductRepository
{
private readonly AppDbContext _context;
public ProductRepository(AppDbContext context)
{
_context = context;
}
public async Task<IEnumerable<Product>> GetAllAsync() =>
await _context.Products.ToListAsync();
public async Task<Product> GetByIdAsync(Guid id) =>
await _context.Products.FindAsync(id);
}
πΉ ProductsController.cs
(Presentation Layer)
using Microsoft.AspNetCore.Mvc;
using Application.Interfaces;
using Domain.Entities;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
var products = await _productService.GetAllAsync();
return Ok(products);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(Guid id)
{
var product = await _productService.GetByIdAsync(id);
return product != null ? Ok(product) : NotFound();
}
}
π Frontend Integration Examples
β Angular Product Service
@Injectable()
export class ProductService {
private baseUrl = 'https://localhost:5001/api/products';
constructor(private http: HttpClient) {}
getAll(): Observable<Product[]> {
return this.http.get<Product[]>(this.baseUrl);
}
}
β Angular Component Example
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of products">
<h3>{{ product.name }}</h3>
<p>βΉ{{ product.price }}</p>
</div>
`
})
export class ProductListComponent implements OnInit {
products: Product[] = [];
constructor(private productService: ProductService) {}
ngOnInit() {
this.productService.getAll().subscribe(data => this.products = data);
}
}
β React ProductList Component
import { useEffect, useState } from 'react';
import axios from 'axios';
export default function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
axios.get('https://localhost:5001/api/products')
.then(response => setProducts(response.data));
}, []);
return (
<div>
{products.map(p => (
<div key={p.id}>
<h3>{p.name}</h3>
<p>βΉ{p.price}</p>
</div>
))}
</div>
);
}
π οΈ Benefits of Clean Architecture
- Maintainability: With clear separation of concerns, it's easier to locate and fix issues.βC# Corner+3Medium+3Medium+3
- Testability: Business logic can be tested in isolation without relying on external systems.βMedium+1Microsoft Learn+1
- Scalability: Adding new features or making changes becomes more straightforward.β
- Flexibility: You can swap out external components (like databases or UI frameworks) with minimal impact on the core logic.βAnton Dev Tips
π Further Reading
- Microsoft's Guide on Clean Architecture
- Clean Architecture in .NET Core: A Complete Guide
- Implementing Clean Architecture in ASP.NET Core Web API
π€ When Clean Architecture Might Be Overkill
Clean Architecture adds overhead, so for:
- Small apps, POCs, or internal tools with short lifespans
- Solo dev quick builds with minimal logic
...you might be better off with a leaner structure. Use what suits the project scale.
β Key Takeaways
- Clean Architecture separates concerns and ensures testability.
- Dependencies point inward β your business logic stays safe.
- It's the foundation for scalable, maintainable applications.
- Works great with frontend frameworks like Angular/React.
π Wrapping Up
Thatβs a wrap for todayβs post on Clean Architecture in .NET 9!
Weβve laid the foundation by exploring its layered structure, seeing how each piece fits, and integrating it with frontend frameworks like Angular or React.
But this is just the beginning...
π Up next: weβll dive deeper into Layered Architecture β breaking down responsibilities, DTO mappings, and how to avoid messy project structures with smart design patterns.
ποΈ Stay tuned for Wednesday's post:
βEntity Layer, Domain Layer, Application Layer β Who Does What?β
π¬ Donβt forget to subscribe to get updates directly in your inbox.
Thanks for reading,
β Bharath β¨