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.

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.
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 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.
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly INotificationService _notificationService;
public OrderService(
IOrderRepository orderRepository,
INotificationService notificationService)
{
_orderRepository = orderRepository;
_notificationService = notificationService;
}
public async Task<Order> 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.
public interface IPaymentProcessor
{
string PaymentMethod { get; }
Task<PaymentResult> ProcessAsync(Payment payment);
}
// StripePaymentProcessor.cs
public class StripePaymentProcessor : IPaymentProcessor
{
public string PaymentMethod => "Stripe";
public async Task<PaymentResult> 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<IPaymentProcessor> _processors;
public PaymentService(IEnumerable<IPaymentProcessor> processors)
{
_processors = processors;
}
public async Task<PaymentResult> 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 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 |
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.
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.
var builder = WebApplication.CreateBuilder(args);
// Domain services - typically transient
builder.Services.AddTransient<IOrderValidator, OrderValidator>();
// Application services - scoped per request
builder.Services.AddScoped<IOrderService, OrderService>();
// Infrastructure - scoped to share DbContext
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
// External clients - singleton with HttpClient pooling
builder.Services.AddHttpClient<IPaymentGateway, StripeGateway>(client =>
{
client.BaseAddress = new Uri("https://api.stripe.com/v1/");
});
// Keyed services for multiple implementations (.NET 8+)
builder.Services.AddKeyedScoped<INotificationService, EmailNotificationService>("email");
builder.Services.AddKeyedScoped<INotificationService, SmsNotificationService>("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.
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<IReadOnlyList<Order>> 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<Order?> GetByIdAsync(Guid id, CancellationToken ct = default)
{
return await _context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id, ct);
}
public async Task<IReadOnlyList<Order>> 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.
Ready to ace your .NET interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Unit Testing Clean Architecture Components
Clean Architecture makes testing straightforward because dependencies are injected through interfaces. xUnit and NSubstitute provide the testing framework and mocking library for .NET.
public class OrderServiceTests
{
private readonly IOrderRepository _orderRepository;
private readonly INotificationService _notificationService;
private readonly OrderService _sut;
public OrderServiceTests()
{
_orderRepository = Substitute.For<IOrderRepository>();
_notificationService = Substitute.For<INotificationService>();
_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<Order>());
await _notificationService.Received(1)
.SendOrderConfirmationAsync(Arg.Is<Order>(o => o.Id == order.Id));
}
[Fact]
public async Task CreateOrderAsync_EmptyItems_ThrowsDomainException()
{
// Arrange
var request = new CreateOrderRequest(
CustomerId: Guid.NewGuid(),
Items: Array.Empty<OrderItemDto>());
// Act & Assert
await Assert.ThrowsAsync<DomainException>(
() => _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.
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.
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<Order?> 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<Order>(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.
public record OrderDto(
Guid Id,
Guid CustomerId,
IReadOnlyList<OrderItemDto> 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.
// 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<InvoiceItem> 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.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
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
Can you spot the bug in .NET?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on August 24, 2026
Tags
Share
Related articles

Advanced C# LINQ in 2026: Operators, Performance and Interview Questions
Master advanced LINQ operators, deferred execution, and performance optimization. Covers GroupBy, SelectMany, query optimization, and common interview questions for .NET developers.

Clean Architecture with .NET: Practical Guide
Master Clean Architecture in .NET with C#. Learn SOLID principles, layer separation, and implementation patterns for building maintainable applications.

Top 25 ASP.NET Core Interview Questions: Middleware, DI and Minimal APIs
Master the most common ASP.NET Core interview questions on middleware pipelines, dependency injection lifetimes, and minimal APIs. Covers .NET 9 and .NET 10 features with code examples.