# Clean Code Architecture C#: Complete Guide and Interview Questions 2026 > Master Clean Code and Clean Architecture in C# with SOLID principles, practical patterns, and interview questions. Learn layered architecture, dependency injection, and testable code design for .NET 10. - Published: 2026-08-24 - Updated: 2026-08-24 - Author: Anthony Fillion-Maillet - Tags: C#, Clean Architecture, Clean Code, SOLID, .NET, Design Patterns - Reading time: 12 min --- Clean Code Architecture in C# combines Robert C. Martin's Clean Code principles with the Clean Architecture pattern to produce maintainable, testable, and scalable .NET applications. Technical interviews increasingly focus on these concepts because they reveal how a candidate thinks about software design beyond just making code work. > **Interview Insight** > > When asked about Clean Architecture, interviewers expect candidates to explain the dependency rule: source code dependencies point inward, toward higher-level policies. The Domain layer knows nothing about Infrastructure, not the other way around. ## SOLID Principles as the Foundation of Clean C# Code SOLID principles form the backbone of Clean Code in C#. The [official Microsoft documentation on .NET fundamentals](https://learn.microsoft.com/en-us/dotnet/fundamentals/) recommends these patterns for enterprise applications. Each principle addresses a specific maintenance problem. **Single Responsibility Principle (SRP)**: A class has one reason to change. The `OrderService` below handles only order processing, delegating persistence and notifications to separate components. ```csharp // OrderService.cs public class OrderService { private readonly IOrderRepository _orderRepository; private readonly INotificationService _notificationService; public OrderService( IOrderRepository orderRepository, INotificationService notificationService) { _orderRepository = orderRepository; _notificationService = notificationService; } public async Task CreateOrderAsync(CreateOrderRequest request) { // Validates and creates the domain entity var order = Order.Create(request.CustomerId, request.Items); // Persists through the repository abstraction await _orderRepository.AddAsync(order); // Notifies through a separate service await _notificationService.SendOrderConfirmationAsync(order); return order; } } ``` **Open/Closed Principle (OCP)**: Classes remain open for extension but closed for modification. New payment methods require new classes, not changes to existing ones. ```csharp // IPaymentProcessor.cs public interface IPaymentProcessor { string PaymentMethod { get; } Task ProcessAsync(Payment payment); } // StripePaymentProcessor.cs public class StripePaymentProcessor : IPaymentProcessor { public string PaymentMethod => "Stripe"; public async Task ProcessAsync(Payment payment) { // Stripe-specific implementation var charge = await _stripeClient.CreateChargeAsync(payment.Amount); return new PaymentResult(charge.Id, charge.Status == "succeeded"); } } // PaymentService.cs public class PaymentService { private readonly IEnumerable _processors; public PaymentService(IEnumerable processors) { _processors = processors; } public async Task ProcessPaymentAsync( Payment payment, string method) { var processor = _processors .FirstOrDefault(p => p.PaymentMethod == method) ?? throw new NotSupportedException($"Payment method {method} not supported"); return await processor.ProcessAsync(payment); } } ``` Adding PayPal support means adding a `PayPalPaymentProcessor` class. The `PaymentService` remains unchanged. ## The Four Layers of Clean Architecture in .NET Clean Architecture organizes code into concentric layers. The [Clean Architecture repository by Jason Taylor](https://github.com/jasontaylordev/CleanArchitecture) provides a widely-adopted .NET template. Each layer has explicit responsibilities and dependencies flow inward. | Layer | Responsibility | Dependencies | |-------|---------------|-------------| | Domain | Entities, value objects, domain events | None | | Application | Use cases, DTOs, interfaces | Domain | | Infrastructure | Database, external APIs, file system | Application, Domain | | Presentation | Controllers, views, API endpoints | Application | ```csharp // Domain/Entities/Customer.cs public class Customer { public Guid Id { get; private set; } public string Email { get; private set; } public CustomerStatus Status { get; private set; } private Customer() { } // EF Core constructor public static Customer Create(string email) { if (string.IsNullOrWhiteSpace(email)) throw new DomainException("Email cannot be empty"); return new Customer { Id = Guid.NewGuid(), Email = email.ToLowerInvariant(), Status = CustomerStatus.Active }; } public void Deactivate() { if (Status == CustomerStatus.Inactive) throw new DomainException("Customer already inactive"); Status = CustomerStatus.Inactive; } } ``` The Domain entity encapsulates business rules. It validates its own invariants and exposes behavior through methods, not setters. > **Why Private Setters?** > > Private setters prevent external code from putting entities into invalid states. The `Deactivate()` method enforces the rule that inactive customers cannot be deactivated again. This pattern appears frequently in Domain-Driven Design and Clean Architecture implementations. ## Dependency Injection Patterns for Clean Architecture Dependency Injection (DI) enables the dependency inversion required by Clean Architecture. .NET 10 includes a built-in DI container that supports constructor injection, scoped lifetimes, and keyed services introduced in .NET 8. ```csharp // Program.cs (Minimal API) var builder = WebApplication.CreateBuilder(args); // Domain services - typically transient builder.Services.AddTransient(); // Application services - scoped per request builder.Services.AddScoped(); // Infrastructure - scoped to share DbContext builder.Services.AddScoped(); builder.Services.AddScoped(); // External clients - singleton with HttpClient pooling builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri("https://api.stripe.com/v1/"); }); // Keyed services for multiple implementations (.NET 8+) builder.Services.AddKeyedScoped("email"); builder.Services.AddKeyedScoped("sms"); var app = builder.Build(); ``` The Application layer defines interfaces. The Infrastructure layer implements them. The Presentation layer (or composition root) wires everything together. ## Repository Pattern with Entity Framework Core 9 The Repository pattern abstracts data access behind domain-centric interfaces. EF Core 9, shipping with .NET 10, provides the underlying ORM. For advanced EF Core patterns, see the [SharpSkill EF Core performance guide](/blog/dotnet/ef-core-performance-best-practices). ```csharp // Application/Interfaces/IOrderRepository.cs public interface IOrderRepository { Task GetByIdAsync(Guid id, CancellationToken ct = default); Task> GetByCustomerAsync(Guid customerId, CancellationToken ct = default); Task AddAsync(Order order, CancellationToken ct = default); void Update(Order order); } // Infrastructure/Repositories/OrderRepository.cs public class OrderRepository : IOrderRepository { private readonly ApplicationDbContext _context; public OrderRepository(ApplicationDbContext context) { _context = context; } public async Task GetByIdAsync(Guid id, CancellationToken ct = default) { return await _context.Orders .Include(o => o.Items) .FirstOrDefaultAsync(o => o.Id == id, ct); } public async Task> GetByCustomerAsync( Guid customerId, CancellationToken ct = default) { return await _context.Orders .Where(o => o.CustomerId == customerId) .OrderByDescending(o => o.CreatedAt) .ToListAsync(ct); } public async Task AddAsync(Order order, CancellationToken ct = default) { await _context.Orders.AddAsync(order, ct); } public void Update(Order order) { _context.Orders.Update(order); } } ``` The repository returns domain entities, not DTOs. Mapping to DTOs happens in the Application layer, keeping the Domain pure. ## Unit Testing Clean Architecture Components Clean Architecture makes testing straightforward because dependencies are injected through interfaces. [xUnit](https://xunit.net/) and [NSubstitute](https://nsubstitute.github.io/) provide the testing framework and mocking library for .NET. ```csharp // OrderServiceTests.cs public class OrderServiceTests { private readonly IOrderRepository _orderRepository; private readonly INotificationService _notificationService; private readonly OrderService _sut; public OrderServiceTests() { _orderRepository = Substitute.For(); _notificationService = Substitute.For(); _sut = new OrderService(_orderRepository, _notificationService); } [Fact] public async Task CreateOrderAsync_ValidRequest_PersistsAndNotifies() { // Arrange var request = new CreateOrderRequest( CustomerId: Guid.NewGuid(), Items: new[] { new OrderItemDto("SKU-001", 2, 29.99m) }); // Act var order = await _sut.CreateOrderAsync(request); // Assert await _orderRepository.Received(1).AddAsync(Arg.Any()); await _notificationService.Received(1) .SendOrderConfirmationAsync(Arg.Is(o => o.Id == order.Id)); } [Fact] public async Task CreateOrderAsync_EmptyItems_ThrowsDomainException() { // Arrange var request = new CreateOrderRequest( CustomerId: Guid.NewGuid(), Items: Array.Empty()); // Act & Assert await Assert.ThrowsAsync( () => _sut.CreateOrderAsync(request)); } } ``` Tests verify behavior, not implementation. The `_sut` (system under test) interacts with mocks. Assertions check that the correct methods were called with the correct arguments. ## Interview Questions on Clean Architecture C# Technical interviews probe understanding of Clean Architecture at multiple levels. These questions appear in senior .NET interviews. For more interview preparation, see the [SharpSkill Clean Architecture interview module](/technologies/dotnet/interview-questions/clean-architecture). > **Common Interview Trap** > > Candidates often confuse Clean Architecture with N-tier architecture. The key difference: in Clean Architecture, dependencies point inward toward the Domain. In N-tier, each layer depends on the one below it, making the Domain dependent on Infrastructure. **Q: How does Clean Architecture differ from traditional layered architecture?** Traditional layered architecture has each layer depending on the one below: Presentation depends on Business Logic, which depends on Data Access. Clean Architecture inverts this: the Domain has no dependencies, the Application depends on Domain, and Infrastructure depends on both. This inversion means the database technology can change without touching business rules. **Q: When would you choose not to use Clean Architecture?** Clean Architecture adds indirection. For CRUD-heavy applications without complex business rules, the overhead exceeds the benefit. A simple API that proxies database tables directly does not need four layers. The value emerges when business logic complexity justifies the separation. **Q: How do you handle cross-cutting concerns like logging and caching?** Two patterns work well: decorator pattern and middleware. A caching decorator wraps the repository interface, implementing the same interface while adding cache logic. Logging typically uses middleware or a DI interceptor that wraps service calls without polluting business logic. ```csharp // CachingOrderRepository.cs (Decorator pattern) public class CachingOrderRepository : IOrderRepository { private readonly IOrderRepository _inner; private readonly IDistributedCache _cache; public CachingOrderRepository( IOrderRepository inner, IDistributedCache cache) { _inner = inner; _cache = cache; } public async Task GetByIdAsync(Guid id, CancellationToken ct = default) { var cacheKey = $"order:{id}"; var cached = await _cache.GetStringAsync(cacheKey, ct); if (cached is not null) return JsonSerializer.Deserialize(cached); var order = await _inner.GetByIdAsync(id, ct); if (order is not null) { await _cache.SetStringAsync( cacheKey, JsonSerializer.Serialize(order), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }, ct); } return order; } // Other methods delegate to _inner } ``` **Q: How do you structure validation in Clean Architecture?** Validation happens at two levels. Domain validation (invariants) belongs in entities: an Order cannot have zero items. Application validation (input validation) belongs in command handlers or validators: the request must include a valid customer ID. FluentValidation integrates well for input validation, while domain validation remains in entity constructors and methods. ## Code Organization and Naming Conventions Project structure communicates architecture. The standard Clean Architecture template organizes projects by layer. ``` src/ ├── MyApp.Domain/ │ ├── Entities/ │ ├── ValueObjects/ │ ├── Events/ │ └── Exceptions/ ├── MyApp.Application/ │ ├── Common/ │ │ ├── Interfaces/ │ │ └── Behaviors/ │ ├── Orders/ │ │ ├── Commands/ │ │ ├── Queries/ │ │ └── EventHandlers/ │ └── Customers/ ├── MyApp.Infrastructure/ │ ├── Persistence/ │ ├── Services/ │ └── Configuration/ └── MyApp.WebApi/ ├── Controllers/ ├── Filters/ └── Middleware/ ``` Vertical slices (Orders, Customers) group related use cases. This organization scales better than horizontal slices (Commands, Queries) as the application grows. ## Performance Considerations in Clean Architecture Abstraction has a cost. Every interface call adds indirection. These practices minimize overhead while preserving testability. **Use records for DTOs**: Records generate efficient `Equals` and `GetHashCode` implementations. They are immutable by default, preventing accidental mutation. ```csharp // Application/Orders/Queries/OrderDto.cs public record OrderDto( Guid Id, Guid CustomerId, IReadOnlyList Items, decimal Total, DateTime CreatedAt); public record OrderItemDto( string Sku, int Quantity, decimal UnitPrice); ``` **Avoid over-abstraction**: Not every class needs an interface. Abstract external dependencies (database, HTTP clients, file system). Internal domain services that have one implementation rarely need interfaces. **Profile before optimizing**: The overhead of Clean Architecture layers is typically negligible compared to I/O operations. A database query taking 50ms dwarfs the microseconds spent in method dispatch. ## Applying Clean Code Principles to C# Methods Clean Code focuses on readability at the method and class level. These practices apply regardless of architectural pattern. **Methods do one thing**: A method named `ProcessOrderAndSendEmail` violates SRP. Split it into `ProcessOrder` and `SendOrderConfirmation`. **Meaningful names**: `CalculateOrderTotal` communicates intent. `DoCalculation` does not. Variable names follow the same rule: `customerOrders` over `list`. **Small methods**: If a method exceeds 20 lines, it likely does too much. Extract helper methods with descriptive names. ```csharp // Before: Long method doing multiple things public decimal CalculateInvoiceTotal(Invoice invoice) { decimal subtotal = 0; foreach (var item in invoice.Items) { subtotal += item.Quantity * item.UnitPrice; } decimal discount = 0; if (invoice.Customer.IsPreferred) { discount = subtotal * 0.1m; } decimal tax = (subtotal - discount) * 0.2m; return subtotal - discount + tax; } // After: Small methods with single responsibilities public decimal CalculateInvoiceTotal(Invoice invoice) { var subtotal = CalculateSubtotal(invoice.Items); var discount = CalculateDiscount(subtotal, invoice.Customer); var tax = CalculateTax(subtotal - discount); return subtotal - discount + tax; } private decimal CalculateSubtotal(IEnumerable items) => items.Sum(item => item.Quantity * item.UnitPrice); private decimal CalculateDiscount(decimal subtotal, Customer customer) => customer.IsPreferred ? subtotal * 0.1m : 0; private decimal CalculateTax(decimal taxableAmount) => taxableAmount * 0.2m; ``` The refactored version reads like a summary of the business logic. Each helper method can be tested independently. ## Key Takeaways for Clean Code Architecture in C# - The dependency rule: code dependencies point inward, from Infrastructure toward Domain, never outward - SOLID principles guide class design: single responsibility, open for extension, Liskov substitution, interface segregation, dependency inversion - Four layers separate concerns: Domain (entities), Application (use cases), Infrastructure (external systems), Presentation (API/UI) - Dependency injection wires layers together at the composition root, typically in `Program.cs` - Repository pattern abstracts data access behind domain-centric interfaces that return entities, not DTOs - Unit tests verify behavior by mocking interfaces, validating that the correct methods receive the correct arguments - Clean Code at the method level: small methods, meaningful names, single responsibilities - Performance cost of abstraction is typically negligible compared to I/O operations; profile before optimizing - Interview questions probe understanding of the dependency rule, trade-offs, and practical patterns like decorators for cross-cutting concerns --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/dotnet/clean-code-architecture-csharp-guide-interview-questions-2026