πŸ—οΈ Clean Architecture in .NET 9 – The Foundation of Scalable Web Apps

πŸ—οΈ 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

  1. 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
  2. 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
  3. 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
  4. 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


πŸ€” 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 ✨


Read more

✨ Hidden Gems: 2 Powerful but Less Used EF Core LINQ Methods (2025 Update)

✨ Hidden Gems: 2 Powerful but Less Used EF Core LINQ Methods (2025 Update)

Go beyond the basics β€” Master these underrated EF Core features to write high-performance, production-grade applications! πŸš€ 1️⃣ ExecuteUpdateAsync() β€” Bulk Update Without Loading Entities πŸ› οΈ Introduced in EF Core 7 β€” Perform direct SQL UPDATE operations without fetching entities into memory. πŸ”Ή Usage: await dbContext.Users .Where(u => u.LastLogin < DateTime.UtcNow.AddYears(

By Bharath Kumar J C